From a0c90c11f2e73ea2f01b4b8c22efde596f5d3553 Mon Sep 17 00:00:00 2001 From: blasty Date: Thu, 23 Jul 2026 21:47:40 +0200 Subject: fix: don't let a late navigation steal focus from an open prompt Real, data-loss-capable bug: opening a function that is already the current one (e.g. after scrolling away from it) schedules an async re-navigation. If the user opens the search prompt ('/') before that navigation lands, the navigation's _show_active() called lst.focus() and yanked focus OUT of the search box. The next typed characters then routed to the listing as verbs -- and 'u' = undefine, silently deleting a function. (This is what produced the flaky "search finds nothing" + "no function for " cascade in the pilot.) Fix: _show_active() no longer grabs focus for the code view while any prompt overlay (search/rename/comment/retype/goto/func-filter) is visible -- those own the keyboard until dismissed. View visibility is unchanged; only the focus grab is suppressed. Also harden the pilot's open(): wait for the listing cursor to actually LAND on the target address, not merely for _cur.ea to match (which is stale-true when re-opening the same function), so tests can't proceed on a not-yet-moved cursor. Verified: the exact failing order disasm_nav->search->follow_xrefs is green; broad focus-sensitive sweep (startup/disasm_nav/view_toggle/hex/search/rename/ comment_func/retype/follow_xrefs/decomp_nav/mouse/listing*/continuous_view/ func_banners/region_define) 89/0. --- idatui/app.py | 26 +++++++++++++++++++++++--- tests/test_scenarios.py | 6 +++++- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/idatui/app.py b/idatui/app.py index 3fa6515..abe50fb 100644 --- a/idatui/app.py +++ b/idatui/app.py @@ -3691,18 +3691,37 @@ class IdaTui(App): self._active = "listing" self._show_active() + # Prompt overlays that own the keyboard while visible; a background + # navigation must not yank focus out from under them (else typed keys leak + # into a code view as destructive verbs — e.g. 'u' = undefine). + _PROMPT_IDS = ("search", "rename", "comment", "retype", "goto", "func-filter") + + def _prompt_active(self) -> bool: + for iid in self._PROMPT_IDS: + try: + if self.query_one(f"#{iid}", Input).display: + return True + except Exception: # noqa: BLE001 — widget not mounted yet + pass + return False + def _show_active(self) -> None: dec = self.query_one(DecompView) lst = self.query_one(ListingView) hx = self.query_one(HexView) + # Don't steal focus from an open prompt (search/rename/…) — a late async + # navigation completing here would otherwise pull it into the code view. + grab = not self._prompt_active() dec.display = lst.display = hx.display = False if self._active in ("listing", "disasm"): lst.display = True - lst.focus() + if grab: + lst.focus() self._status_for_cur("listing") elif self._active == "hex": hx.display = True - hx.focus() + if grab: + hx.focus() ea = getattr(self, "_hex_pending_ea", None) self._hex_pending_ea = None if hx.model is None: @@ -3714,7 +3733,8 @@ class IdaTui(App): self._hex_status(hx.cursor_va()) else: dec.display = True - dec.focus() + if grab: + dec.focus() if self._cur is not None and dec.loaded_ea != self._cur.ea: self._status(f"{self._cur.name} — decompiling…") dec.loading = True # gray out + 'decompiling…' overlay diff --git a/tests/test_scenarios.py b/tests/test_scenarios.py index f72e340..7372c02 100644 --- a/tests/test_scenarios.py +++ b/tests/test_scenarios.py @@ -164,8 +164,12 @@ class Ctx: raise RuntimeError(f"no function for {target!r}") self.app._open_function(fn.addr, fn.name) await self.wait(lambda: self.app._cur and self.app._cur.ea == fn.addr, t) + # Wait for the cursor to actually LAND on the target, not merely for + # _cur.ea to match: re-opening a function we already navigated to (its + # _cur.ea is stale-true) schedules an async re-navigation, and proceeding + # before it lands would leave the cursor parked wherever we last were. await self.wait(lambda: self.lst.total > 0 - and self.lst._cursor_ea() is not None, t) + and self.lst._cursor_ea() == fn.addr, t) if view == "decomp": self.lst.focus() await self.press("tab") # F5/Tab -> decompile the function at cursor -- cgit v1.3.1-sl0p