aboutsummaryrefslogtreecommitdiffstats
path: root/docs
diff options
context:
space:
mode:
authorblasty <blasty@local>2026-07-09 19:38:46 +0200
committerblasty <blasty@local>2026-07-09 19:38:46 +0200
commit6f1a2a2e73974e41ec893cf19ea9c30ebea1d865 (patch)
tree5e0fb1709bcf1a850f5e526297fda3794d9b596c /docs
parentfix: decomp follow fails on a just-renamed symbol (stale name) (diff)
downloadida-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.md65
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.