aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorblasty <blasty@local>2026-07-25 22:17:17 +0200
committerblasty <blasty@local>2026-07-25 22:17:17 +0200
commit1d626740b52c9a4eba04cb7f1b2790a70bd9c627 (patch)
tree7ae96fd9fc46120e21588d88b2c3af4f25643507
parentnav: don't stack a history entry for the place you're already standing on (diff)
downloadida-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.py17
-rw-r--r--idatui/rpc.py2
-rw-r--r--tests/test_scenarios.py17
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")