aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorblasty <blasty@local>2026-07-25 21:29:37 +0200
committerblasty <blasty@local>2026-07-25 21:29:37 +0200
commitf0a30e2f1bfca81753a0807262de9901005d8a6a (patch)
tree9d9ffe27722ae86c7fc03ec33b06e5a593a21143
parentpalette: match case-insensitively in BOTH directions (diff)
downloadida-tui-f0a30e2f1bfca81753a0807262de9901005d8a6a.tar.gz
ida-tui-f0a30e2f1bfca81753a0807262de9901005d8a6a.tar.xz
ida-tui-f0a30e2f1bfca81753a0807262de9901005d8a6a.zip
nav: don't stack a history entry for the place you're already standing on
Walking back with Esc ended in a dead keypress: nav_depth dropped but the screen didn't change. Measured on a live pane, four Escs after one follow: Esc #1 -> sub_3500 L7 (where we followed from) good Esc #2 -> main L54 decomp (where we followed from) good Esc #3 -> main L344 listing good Esc #4 -> main L344 listing -- nav 2 -> 1, nothing moved The app auto-lands on main at startup, and opening main again from Ctrl+N appended a second, identical entry. Every "navigate to where you already are" did this; the extra Esc it bought is invisible except that it does nothing, which is exactly what "back is broken" feels like from the keyboard. Both push sites now go through _push_nav, which replaces the top entry instead of appending when the target is the same (ea, view, line) — the newer entry's metadata still wins. _same_spot compares dec_cursor for pseudocode and cursor for the listing. Note what is NOT a duplicate: the first follow after Tab-to-pseudocode still pushes twice (nav 1 -> 3). That's deliberate — Tab leaves _cur transient and off the stack, so the follow records the pseudocode position first, which is why Esc #1 above returns to main L54 rather than dropping you into the listing. Verified that subsequent follows push exactly one each. tests: the palette scenario re-opens the function it is already on and asserts nav depth is unchanged. Fails without the fix (nav 2 -> 3). 194/0.
-rw-r--r--idatui/app.py30
-rw-r--r--tests/test_scenarios.py15
2 files changed, 42 insertions, 3 deletions
diff --git a/idatui/app.py b/idatui/app.py
index eacdbe8..a4a5bdd 100644
--- a/idatui/app.py
+++ b/idatui/app.py
@@ -4968,14 +4968,14 @@ class IdaTui(App):
src.dec_cursor_x = dv.cursor_x
src.dec_scroll_y = round(dv.scroll_offset.y)
if not self._nav or self._nav[-1] is not src:
- self._nav.append(src)
+ self._push_nav(src) # never stack a second copy of a spot
else:
self._save_current_pos()
self._decomp_return = None # a real navigation abandons the F5 return
entry = NavEntry(ea=fn_addr, name=fn_name, view="decomp",
dec_cursor=dec_idx, dec_cursor_x=dec_cursor_x)
if push:
- self._nav.append(entry)
+ self._push_nav(entry)
self._open_entry(entry, push=False)
def _decomp_col_for(self, fn_addr: int, line_idx: int, name: str) -> int:
@@ -5047,6 +5047,30 @@ class IdaTui(App):
best_ea, best_idx = e, i
return best_idx
+ @staticmethod
+ def _same_spot(a: NavEntry, b: NavEntry) -> bool:
+ """Same function, same view, same line — i.e. going from a to b is not a
+ move, so it has no business being a step in the history."""
+ if a.ea != b.ea or a.view != b.view:
+ return False
+ return (a.dec_cursor == b.dec_cursor if a.view == "decomp"
+ else a.cursor == b.cursor)
+
+ def _push_nav(self, entry: NavEntry) -> None:
+ """Append to the nav stack unless that would duplicate where we already are.
+
+ Opening the place you're standing on — the app auto-lands on main, then
+ you pick main out of Ctrl+N — used to append an identical entry. The Esc
+ it created popped the stack without changing anything on screen: a dead
+ keypress, which is exactly what "back doesn't work" feels like. Replace
+ the top instead, so the newer entry's metadata still wins.
+ """
+ top = self._nav[-1] if self._nav else None
+ if top is not None and self._same_spot(top, entry):
+ self._nav[-1] = entry
+ return
+ self._nav.append(entry)
+
def _open_at(self, ea: int, name: str, cursor: int, push: bool,
dec_cursor: int = -1, dec_cursor_x: int = 0,
is_region: bool = False, focus_name: str | None = None) -> None:
@@ -5061,7 +5085,7 @@ class IdaTui(App):
entry.dec_cursor = dec_cursor
entry.dec_cursor_x = dec_cursor_x
if push:
- self._nav.append(entry)
+ self._push_nav(entry)
self._open_entry(entry, push=False)
def on_input_changed(self, event: Input.Changed) -> None:
diff --git a/tests/test_scenarios.py b/tests/test_scenarios.py
index 4024c07..becd8b1 100644
--- a/tests/test_scenarios.py
+++ b/tests/test_scenarios.py
@@ -327,6 +327,21 @@ async def s_palette(c: Ctx):
c.check("selecting a palette entry opens that function",
bool(app._cur) and app._cur.ea == want,
f"cur={app._cur.ea if app._cur else None}")
+ # Re-open the function we are ALREADY standing on. That used to append an
+ # identical nav entry, and the extra Esc it bought popped the stack without
+ # changing anything on screen — a dead keypress, which is precisely what
+ # "back is broken" feels like from the keyboard.
+ depth = len(app._nav)
+ await c.press("ctrl+n")
+ await c.wait(lambda: isinstance(app.screen, SymbolPalette), 10)
+ pal2 = app.screen
+ pal2.query_one(Input).value = "main"
+ await c.wait(lambda: pal2._results and pal2._results[0][2] == "main", 10)
+ await c.press("enter")
+ await c.wait(lambda: not isinstance(app.screen, SymbolPalette), 10)
+ await c.pause(0.4)
+ c.check("re-opening the current function doesn't stack a duplicate",
+ len(app._nav) == depth, f"nav {depth} -> {len(app._nav)}")
await c.press("ctrl+n")
await c.wait(lambda: isinstance(app.screen, SymbolPalette), 10)
await c.press("escape")