diff options
| author | blasty <blasty@local> | 2026-07-25 22:17:17 +0200 |
|---|---|---|
| committer | blasty <blasty@local> | 2026-07-25 22:17:17 +0200 |
| commit | 1d626740b52c9a4eba04cb7f1b2790a70bd9c627 (patch) | |
| tree | 7ae96fd9fc46120e21588d88b2c3af4f25643507 | |
| parent | nav: don't stack a history entry for the place you're already standing on (diff) | |
| download | ida-tui-1d626740b52c9a4eba04cb7f1b2790a70bd9c627.tar.gz ida-tui-1d626740b52c9a4eba04cb7f1b2790a70bd9c627.tar.xz ida-tui-1d626740b52c9a4eba04cb7f1b2790a70bd9c627.zip | |
nav: one notion of "which pane you're in" — delete _pref
The app carried three overlapping ideas of the current pane: _active, _pref, and
Textual focus. 190e28b tied focus to _active in split; this removes _pref, which
turns out never to have been a variable at all.
_pref was assigned "listing" in __init__ and "listing" on a project binary
switch. Nothing else ever wrote it. But _code_view() branched on it:
return self.query_one(DecompView if self._pref == "decomp" else ListingView)
so it always returned the listing, whatever you were reading. Its two callers put
focus back after the goto prompt closes — so cancelling `g` while in the
pseudocode focused the HIDDEN listing, and the pane you were looking at stopped
answering the keyboard. Arrows did nothing until you clicked. (It also explains
why routing follow through _code_view() earlier made Enter a dead key: the helper
had been quietly lying the whole time.)
_code_view() now returns the active code pane. _pref is gone from the app,
BinaryState and the RPC snapshot keeps "pref" for wire compat, sourced from
_code_mode() — the one place that answers "which code view do we return to from
hex", and a constant by design in the unified layout.
Verified on a live pane both ways: g then Esc in pseudocode, then two Downs.
Fixed, the cursor moves 0 -> 2; with the old lookup restored it sits at 0 with
focus=ListingView. Same check added to the view_toggle scenario, and it fails
without the fix. 195/0, project UI 23/23.
| -rw-r--r-- | idatui/app.py | 17 | ||||
| -rw-r--r-- | idatui/rpc.py | 2 | ||||
| -rw-r--r-- | tests/test_scenarios.py | 17 |
3 files changed, 29 insertions, 7 deletions
diff --git a/idatui/app.py b/idatui/app.py index a4a5bdd..ff383be 100644 --- a/idatui/app.py +++ b/idatui/app.py @@ -105,7 +105,6 @@ class BinaryState: func_index: object | None = None nav: list = field(default_factory=list) cur: object | None = None - pref: str = "listing" active: str = "listing" split: bool = False filter_term: str = "" @@ -3201,7 +3200,9 @@ class IdaTui(App): self._filter_timer = None self._sort_col = 0 # 0=addr, 1=name, 2=size self._sort_reverse = False - self._pref = "listing" # unified: the linear listing is the code view + # ONE notion of "which pane you're in": _active, kept in step with focus + # (on_descendant_focus does that while split). There used to be a second, + # _pref, but it was only ever assigned "listing" — see _code_mode(). self._active = "listing" # currently shown view (in split: the focused pane) self._split = False # side-by-side listing + pseudocode self._split_eamap: list[list[int]] = [] # split: decomp line -> instr EAs @@ -3774,7 +3775,7 @@ class IdaTui(App): self._states[self._binary] = BinaryState( label=self._binary, program=self.program, func_index=self._func_index, nav=list(self._nav), cur=self._cur, - pref=self._pref, active=self._active, split=self._split, + active=self._active, split=self._split, filter_term=self._filter_term, dirty=self._dirty) self._loading_screen = LoadingScreen(label, note="switching\u2026") self.push_screen(self._loading_screen) @@ -3805,7 +3806,6 @@ class IdaTui(App): self._binary = label self._pool.set_active(label) self._open_path = self._project.by_label(label).staged - self._pref = st.pref if st else "listing" self._active = st.active if st else "listing" self._split = st.split if st else False self._filter_term = st.filter_term if st else "" @@ -5210,7 +5210,12 @@ class IdaTui(App): self.query_one("#func-table", DataTable).focus() def _code_view(self): # type: ignore[no-untyped-def] - return self.query_one(DecompView if self._pref == "decomp" else ListingView) + """The code pane the user is in — where focus belongs after a prompt closes. + + This used to pick on _pref, which was always "listing", so closing the + goto prompt while reading pseudocode threw focus into the listing. + """ + return self._active_code_view() def _active_code_view(self): # type: ignore[no-untyped-def] """The currently-shown code widget (for reading the cursor address).""" @@ -5472,7 +5477,7 @@ class IdaTui(App): self._show_active() def on_hex_view_to_code(self, msg: HexView.ToCode) -> None: - self._active = self._pref + self._active = self._code_mode() self._goto_ea(msg.va, push=True) def _status_for_cur(self, mode: str) -> None: diff --git a/idatui/rpc.py b/idatui/rpc.py index 59eec86..dd0c9c6 100644 --- a/idatui/rpc.py +++ b/idatui/rpc.py @@ -168,7 +168,7 @@ def snapshot(app) -> dict[str, Any]: pass return { "active": app._active, - "pref": app._pref, + "pref": app._code_mode(), # kept for wire compat; a constant now "function": ({"ea": cur.ea, "name": cur.name} if cur else None), "cursor": _cursor_info(app, w), "status": st, diff --git a/tests/test_scenarios.py b/tests/test_scenarios.py index becd8b1..b7520d9 100644 --- a/tests/test_scenarios.py +++ b/tests/test_scenarios.py @@ -911,6 +911,23 @@ async def s_view_toggle(c: Ctx): styled = any(seg.style is not None and seg.style.color is not None for strip in dec._strips[:min(dec.total, 200)] for seg in strip) c.check("pseudocode is syntax-highlighted", styled) + # Cancelling a prompt must hand focus back to the pane you were READING. + # _code_view() used to choose on _pref, which was only ever "listing", so it + # focused the hidden listing and the pseudocode stopped answering the + # keyboard — arrows did nothing at all until you clicked. + dec.focus() + await c.pause(0.05) + line0 = dec.cursor + await c.press("g") + await c.wait(lambda: app.query_one("#goto", Input).display, 10) + await c.press("escape") + await c.wait(lambda: not app.query_one("#goto", Input).display, 10) + await c.press("down") + await c.press("down") + await c.pause(0.2) + c.check("cancelling goto leaves focus in the pseudocode (arrows still work)", + dec.cursor > line0, + f"cursor {line0} -> {dec.cursor} focus={type(app.focused).__name__}") dec.focus() # Tab only toggles the view from a code pane; elsewhere it's await c.pause(0.05) # focus-next, which would silently leave us in decomp await c.press("tab") |
