""" ``binder layout`` — PDF page-layout diagnostics. Subcommands: check — Scan a built PDF for pages with excessive bottom whitespace in the main body column, and guess the likely cause (the block at the top of the next page that probably forced the break). The planner is read-only by default: it inspects the PDF, prints a report, and exits. It does not modify source or PDF. Fixes happen in the QMD source during an explicit apply/rebuild/re-scan loop. """ import argparse import csv import json import math import re import shutil import subprocess import sys import unicodedata from collections import Counter, defaultdict from dataclasses import dataclass, field from pathlib import Path from typing import Any, Dict, List, Optional, Tuple from rich.console import Console from rich.panel import Panel from rich.rule import Rule from rich.table import Table console = Console() class _LayoutHelpFormatter( argparse.RawDescriptionHelpFormatter, ): """Argparse formatter that preserves examples.""" def _clickable_source(rel_path: str, repo_root: Path) -> str: """Wrap a repo-relative ``source_file`` as an OSC 8 terminal hyperlink. The visible text stays the relative path, but a cmd+click (Ghostty, iTerm2) opens the file via its absolute ``file://`` URL. The escape is emitted only when stdout is an interactive TTY, so a redirected/piped CSV stays plain and grep-able (the raw relative path, no control characters). Ghostty needs an absolute, schemed URL to resolve a path — a bare relative path is never clickable — so the link target is always the resolved absolute path. """ if not rel_path or not sys.stdout.isatty(): return rel_path try: target = (repo_root / rel_path).resolve().as_uri() except (OSError, ValueError): return rel_path esc = "\033" return f"{esc}]8;;{target}{esc}\\{rel_path}{esc}]8;;{esc}\\" # --- geometry constants --------------------------------------------------- # These are heuristic bands for the book's Tufte-style layout (main column # left, margin notes right). They are intentionally generous so the tool # works without per-volume calibration; tune later if needed. HEADER_FRAC = 0.06 # top 6% of page height treated as running header FOOTER_FRAC = 0.06 # bottom 6% treated as running footer / page number MAIN_COL_RIGHT_FRAC = 0.55 # main column ends ~55% across the page LEFT_MARGIN_RIGHT_FRAC = 0.22 RIGHT_MARGIN_LEFT_FRAC = 0.78 # Margin material alternates sides in the bound PDF: verso pages use the left # band and recto pages use the right band. PDF_BODY_WIDTH_PT = 8.0 * 72.0 - (0.875 + 1.75) * 72.0 TABLE_MARKER_RE = re.compile(r"\b(?P\d{3})\s+(?P