diff options
| author | blasty <blasty@local> | 2026-07-09 19:38:46 +0200 |
|---|---|---|
| committer | blasty <blasty@local> | 2026-07-09 19:38:46 +0200 |
| commit | 6f1a2a2e73974e41ec893cf19ea9c30ebea1d865 (patch) | |
| tree | 5e0fb1709bcf1a850f5e526297fda3794d9b596c /docs | |
| parent | fix: decomp follow fails on a just-renamed symbol (stale name) (diff) | |
| download | ida-tui-6f1a2a2e73974e41ec893cf19ea9c30ebea1d865.tar.gz ida-tui-6f1a2a2e73974e41ec893cf19ea9c30ebea1d865.tar.xz ida-tui-6f1a2a2e73974e41ec893cf19ea9c30ebea1d865.zip | |
docs: TEXTUAL_NOTES.md — Textual pitfalls, app patterns, testing gotchas
Companion to PAGING_FINDINGS.md capturing the hard-won Textual behaviour
(bindings/mixins, ScrollView repaint & scroll-clamp, loading cover blurs focus,
mouse offset mapping, Input/Footer overlap, no cpp grammar) and the app patterns
(name-gen cache invalidation, cursor+scroll history, name+address follow).
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/TEXTUAL_NOTES.md | 65 |
1 files changed, 65 insertions, 0 deletions
diff --git a/docs/TEXTUAL_NOTES.md b/docs/TEXTUAL_NOTES.md new file mode 100644 index 0000000..ed2e696 --- /dev/null +++ b/docs/TEXTUAL_NOTES.md @@ -0,0 +1,65 @@ +# Textual notes & app patterns + +Hard-won Textual behaviour and the patterns this app relies on. Pairs with +`PAGING_FINDINGS.md` (the ida-pro-mcp/idalib side). + +## Textual pitfalls + +- **`BINDINGS` only merge from `DOMNode` subclasses.** A plain mixin's `BINDINGS` + are silently ignored — each view lists them explicitly (e.g. + `*SearchMixin.SEARCH_BINDINGS`, `*ColumnCursor.COL_BINDINGS`). +- **`ScrollView` + `render_line`**: `y` is the SCREEN line; we add + `scroll_offset.y` ourselves. `ScrollView.watch_scroll_y` only repaints when the + **rounded** scroll value changes. +- **`scroll_to` right after setting `virtual_size` clamps to 0** — `max_scroll_y` + isn't recomputed until layout. Apply the scroll now AND again via + `call_after_refresh` with `refresh(layout=True)`; don't zero `virtual_size` on + load (snaps scroll to 0 → visible flash). See `ColumnCursor._apply_scroll`, + `DisasmView._on_primed`, `DecompView.show`. +- **Pilot lays out synchronously**, so programmatic scroll always has a valid + range in tests — it MASKS the real-terminal clamp/no-repaint bug. Assert the + **render** (trace `render_line`'s scroll at paint), not just `scroll_offset.y`. + (`test_tui.py`: "pane is repainted at the restored scroll".) +- **`widget.loading = True`** covers the widget via `_cover_widget` (NOT a normal + child — `query()` won't find it; check `widget._cover_widget`) and **blurs + focus** (focus → None), so `Tab` falls back to focus-navigation. Fix: make the + app `Tab`/`Shift+Tab` binding `priority=True`; restore focus when loading ends. +- **Mouse**: `event.get_content_offset(widget)` → offset past padding/border; add + `scroll_offset` for the virtual (line, col). `event.chain >= 2` = double-click. + Subtract any left gutter (`ColumnCursor._col_offset`). +- **Cheap repaints**: `refresh(Region(0, row, w, 1))` for just the changed rows; + `reactive(x, repaint=False)` to avoid an implicit full refresh on assignment. +- **`Input`** defaults to a 3-row bordered widget. For a 1-row prompt use + `border: none`, and do NOT `dock: bottom` it next to the `Footer` — they land on + the same row and the Footer paints over it. Keep prompts in normal flow above + the Footer (see `#search`/`#rename`/`#status`). +- Textual ships **no C/C++ tree-sitter grammar** — `TextArea(language="cpp")` is a + silent no-op. We highlight pseudocode with Pygments (`idatui/highlight.py`). + +## App patterns + +- **Name-generation invalidation** (`Program._name_gen` / `bump_names`): a rename + bumps the gen; disasm block caches are cleared (disasm names are live in the + IDB), and `decompile` is gen-checked and `force_recompile`d lazily on mismatch. + `force_recompile` needs `items=[{addr}]`, not `addr`. +- **Cursor + scroll history**: `NavEntry` stores `cursor/cursor_x/scroll_y` + (disasm) and `dec_cursor/dec_cursor_x/dec_scroll_y/dec_scroll_x` (pseudocode). + `_save_current_pos()` snapshots both views right before a push; restore on + `_open_entry` / `DecompView.show`. +- **Follow**: name-based (decompiler refs → `resolve`) THEN an **address-based + fallback** (parse the line's `/*0xEA*/` marker → `xrefs_from` → first code + target). The address path is immune to a stale token right after a rename. +- Views only ever render a viewport-sized slice; scale lives in the domain + cache/paging layer, never in a widget. + +## Testing gotchas + +- `goto` to a non-existent function name is a no-op → "jump back" tests then pass + trivially. Navigate to a REAL second function. +- Scroll-restore tests with the cursor at the viewport top (`rel = 0`) are + degenerate (scroll-into-view derives the same scroll). Use a mid-viewport + cursor (`rel > 0`). +- Cold session ⇒ slow first analysis ⇒ `wait_until` timeouts ⇒ flaky "want 0" + failures. Re-run on a warm session before trusting a regression. +- Edits mutate the `.i64` — revert renames (via API) for idempotency; use a + PID-unique temp name to dodge collisions from a prior crashed run. |
