diff options
| author | blasty <blasty@local> | 2026-07-25 16:08:36 +0200 |
|---|---|---|
| committer | blasty <blasty@local> | 2026-07-25 16:08:36 +0200 |
| commit | 59d9aff1b9c216686fd7b8007aff00b974a17233 (patch) | |
| tree | fdc9cfcacd5c07a9d7ce847dd05092d8e80e9a86 | |
| parent | docs: refresh PROJECTS.md phase 2/3 to match what shipped (diff) | |
| download | ida-tui-59d9aff1b9c216686fd7b8007aff00b974a17233.tar.gz ida-tui-59d9aff1b9c216686fd7b8007aff00b974a17233.tar.xz ida-tui-59d9aff1b9c216686fd7b8007aff00b974a17233.zip | |
help: lay the cheatsheet out as fluid columns of cards
Five sections stacked vertically ran ~45 rows, so F1 scrolled on any terminal
under ~50 lines. Each section is now its own bordered card (title in the border),
and the cards flow into as many columns as the width allows.
Textual CSS has no media queries, so the column count is computed in compose from
the real app width. Sizing off the WIDEST section would let one long row (Move's
"Ctrl+Home / Ctrl+End") inflate every column and cost a column that would
otherwise fit, so _columns() measures the actual layout instead: chunk the
sections, sum the per-chunk maxima, take the most columns that fit. Keys are
right-aligned per card rather than globally, so a card of short keys stays narrow.
200x50 -> 3 cols 140x44 -> 2 cols 100x36 -> 1 col (scrolls)
160x44 -> 3 cols 120x40 -> 2 cols
The scroll container stays, so a very small terminal degrades to scrolling
instead of clipping — but at any normal size nothing scrolls. The box hugs its
content (width:auto all the way down; VerticalScroll needed width:auto too or it
filled to max-width) and stays centred.
Tried a real CSS Grid first: it collapsed to zero height at narrow widths, and
grid sizing from on_mount read a stale app width. Explicit columns are
predictable. Also shortened one description that was pushing the widest card out.
help scenario now asserts the five cards exist and that the content fits without
a scrollbar. Suite 192/0.
| -rw-r--r-- | idatui/app.py | 79 | ||||
| -rw-r--r-- | tests/test_scenarios.py | 13 |
2 files changed, 75 insertions, 17 deletions
diff --git a/idatui/app.py b/idatui/app.py index 3ef4ac8..dbe86e0 100644 --- a/idatui/app.py +++ b/idatui/app.py @@ -30,7 +30,7 @@ from textual import work from textual.app import App, ComposeResult from textual.binding import Binding from textual.command import DiscoveryHit, Hit, Provider -from textual.containers import Horizontal, Vertical, VerticalScroll +from textual.containers import Grid, Horizontal, Vertical, VerticalScroll from textual.geometry import Region, Size from textual.message import Message from textual.reactive import reactive @@ -2348,7 +2348,7 @@ _HELP = ( )), ("Views", ( ("Tab / F5", "disassembly \u21c4 pseudocode"), - ("s", "split view (listing + pseudocode, synced)"), + ("s", "split view: listing + pseudocode"), ("Tab", "in split: switch the driving pane"), ("\\", "hex view"), ("o", "cycle the opcode-bytes column"), @@ -2429,24 +2429,73 @@ class HelpScreen(ModalScreen): BINDINGS = [Binding("escape,f1,q,question_mark", "close", "Close")] + #: widest cell content, +2 for the card's border, +2 for its padding + _CARD_PAD = 4 + def compose(self) -> ComposeResult: + # Fluid: as many columns as the terminal can hold. Textual CSS has no + # media queries, so the split is computed here from the real width. + avail = max(self.app.size.width - 6, 20) + cols = self._columns(avail) + per = -(-len(_HELP) // cols) # ceil, so the columns stay balanced with Vertical(id="help-box"): - yield Static(" keys", id="help-title") + yield Static(" keys", id="help-title", markup=False) + # Still inside a scroll container, so a genuinely tiny terminal + # degrades to scrolling rather than clipping — but spread across the + # width it shouldn't come to that. with VerticalScroll(id="help-body"): - yield Static(self._cheatsheet(), id="help-text") + with Horizontal(id="help-cols"): + for c in range(cols): + chunk = _HELP[c * per:(c + 1) * per] + if not chunk: + continue + with Vertical(classes="help-col"): + for title, rows in chunk: + card = Static(self._card(rows), + classes="help-card", markup=False) + card.border_title = title + yield card yield Static("Esc / F1 to close", id="help-foot") @staticmethod - def _cheatsheet() -> Text: # NB: not _render — that's a Widget internal - width = max(len(k) for _, rows in _HELP for k, _ in rows) + def _section_widths() -> list[int]: + """Rendered width of each section's card (keys right-aligned per card).""" + out = [] + for _, rows in _HELP: + kw = max(len(k) for k, _ in rows) + out.append(max(kw + 2 + len(d) for _, d in rows) + HelpScreen._CARD_PAD) + return out + + @classmethod + def _columns(cls, avail: int) -> int: + """Most columns that actually fit in ``avail``. + + Sizing off the widest section would let one long row (Move's + "Ctrl+Home / Ctrl+End") inflate every column and cost a column that would + otherwise fit. Each column hugs its own content, so measure the real + layout: chunk the sections and sum the per-chunk maxima. + """ + ws = cls._section_widths() + n = len(ws) + for cols in range(min(n, 4), 1, -1): + per = -(-n // cols) + chunks = [ws[c * per:(c + 1) * per] for c in range(cols)] + total = sum(max(c) for c in chunks if c) + (cols - 1) + if total <= avail: + return cols + return 1 + + @staticmethod + def _card(rows) -> Text: # NB: not _render — that's a Widget internal + """One section's keys. The key column is sized per section, so a card of + short keys stays narrow instead of padding out to the global maximum.""" + width = max(len(k) for k, _ in rows) out = Text() - for i, (title, rows) in enumerate(_HELP): + for i, (key, desc) in enumerate(rows): if i: out.append("\n") - out.append(f"{title}\n", _S_FUNCHDR) - for key, desc in rows: - out.append(f" {key:>{width}}", _S_MNEM) - out.append(f" {desc}\n", _S_INSN) + out.append(f"{key:>{width}}", _S_MNEM) + out.append(f" {desc}", _S_INSN) return out def action_close(self) -> None: @@ -3006,10 +3055,14 @@ class IdaTui(App): #quit-list { height: auto; padding: 1 2 0 2; } #quit-help { height: 1; color: $text-muted; padding: 0 2; margin-top: 1; } HelpScreen { align: center middle; } - #help-box { width: 76; max-width: 92%; height: auto; max-height: 85%; + #help-box { width: auto; max-width: 98%; height: auto; max-height: 90%; border: thick $accent; background: $panel; } #help-title { dock: top; height: 1; background: $accent; color: $text; padding: 0 1; } - #help-body { height: auto; max-height: 100%; padding: 1 2; } + #help-body { height: auto; max-height: 100%; width: auto; padding: 1 1; } + #help-cols { height: auto; width: auto; } + .help-col { height: auto; width: auto; margin-right: 1; } + .help-card { height: auto; width: auto; padding: 0 1; + border: round $panel-lighten-2; } #help-foot { dock: bottom; height: 1; color: $text-muted; padding: 0 2; } XrefsScreen { align: center middle; } #xref-box { width: 84; max-height: 70%; height: auto; border: thick $accent; background: $panel; } diff --git a/tests/test_scenarios.py b/tests/test_scenarios.py index 49bb6b9..091ddc8 100644 --- a/tests/test_scenarios.py +++ b/tests/test_scenarios.py @@ -384,12 +384,17 @@ async def s_help(c: Ctx): f"screen={type(app.screen).__name__}") if not opened: return - txt = str(app.screen.query_one("#help-text", Static).render()) - c.check("it lists the key groups", - all(s in txt for s in ("Navigate", "Views", "Move", "Edit", "Search")), - txt[:70]) + cards = app.screen.query(".help-card") + titles = {str(w.border_title) for w in cards} + txt = " ".join(str(w.render()) for w in cards) + c.check("each key group gets its own card", + titles == {"Navigate", "Views", "Move", "Edit", "Search"}, f"{titles}") c.check("it documents real bindings", "set type" in txt and "split view" in txt and "cross-references" in txt) + body = app.screen.query_one("#help-body") + c.check("the cards fit without a scrollbar at a normal size", + body.virtual_size.height <= body.size.height, + f"content={body.virtual_size.height} view={body.size.height}") await c.press("escape") await c.wait(lambda: not isinstance(app.screen, HelpScreen), 10) c.check("Esc closes it", not isinstance(app.screen, HelpScreen)) |
