diff options
| author | blasty <blasty@local> | 2026-07-23 01:00:19 +0200 |
|---|---|---|
| committer | blasty <blasty@local> | 2026-07-23 01:00:19 +0200 |
| commit | cdf1a241e5f9af21d002ec19c97ce62ec016425f (patch) | |
| tree | f9e838dac9ee1ad6faf86787f93ebec05defb6c5 | |
| parent | fix: crash on an unnamed function (None name) in the symbol palette (diff) | |
| download | ida-tui-cdf1a241e5f9af21d002ec19c97ce62ec016425f.tar.gz ida-tui-cdf1a241e5f9af21d002ec19c97ce62ec016425f.tar.xz ida-tui-cdf1a241e5f9af21d002ec19c97ce62ec016425f.zip | |
app: on startup, land on main() (else pop the symbol picker)
Instead of staring at an empty pane after the function list loads, jump
somewhere useful: once the index is complete and nothing's open yet, open
main() (tries main/_main/wmain/WinMain/wWinMain in order); if there's no entry
function, pop the fuzzy symbol picker so you can pick one. Fires exactly once
(guarded by _cur + _did_auto_land) and never steals focus from a user who
already navigated during load.
Tests: new auto_land scenario covers both branches (jump-to-main when present,
picker fallback otherwise) + the idempotency guard. Boot/startup now key "load
complete" off _func_index.complete instead of the status string (auto-land
legitimately overwrites the "N functions" status with the landed fn). Full
suite 120 pass / 1 pre-existing flaky (filter).
| -rw-r--r-- | idatui/app.py | 34 | ||||
| -rw-r--r-- | tests/test_scenarios.py | 39 |
2 files changed, 70 insertions, 3 deletions
diff --git a/idatui/app.py b/idatui/app.py index 38d2411..409e35a 100644 --- a/idatui/app.py +++ b/idatui/app.py @@ -2045,6 +2045,7 @@ class IdaTui(App): self._ka = None self._nav: list[NavEntry] = [] self._func_index = None # the unfiltered FunctionIndex (source of truth) + self._did_auto_land = False # startup jump-to-main/picker fires once self._filter_term = "" self._pending_filter = "" self._filter_timer = None @@ -2215,6 +2216,39 @@ class IdaTui(App): else: self.app.call_from_thread( self._status, f"{module} — {len(idx)} functions (Ctrl+N: find symbol)") + # Land somewhere useful instead of an empty pane: main() if present, + # otherwise pop the fuzzy symbol picker. + self.app.call_from_thread(self._auto_land) + + # -- initial landing --------------------------------------------------- # + #: function names tried (in order) as the startup landing spot + ENTRY_NAMES = ("main", "_main", "wmain", "WinMain", "wWinMain") + + def _auto_land(self) -> None: + """On startup, once functions are loaded and nothing is open yet, jump to + main() if it exists; else open the symbol picker so you're never staring + at a blank pane. Runs once (guarded by ``_cur``) and never steals focus + from a user who already navigated.""" + if self._cur is not None or self._did_auto_land or self._func_index is None: + return + self._did_auto_land = True + fn = self._entry_func() + if fn is not None: + self._open_function(fn.addr, fn.name) + else: + self.action_symbols() + + def _entry_func(self) -> Func | None: + """The best startup landing function (exact-name match against + ENTRY_NAMES, in priority order), or None.""" + idx = self._func_index + if idx is None: + return None + by_name = {f.name: f for f in idx.all_loaded()} + for nm in self.ENTRY_NAMES: + if nm in by_name: + return by_name[nm] + return None def _module(self) -> str: try: diff --git a/tests/test_scenarios.py b/tests/test_scenarios.py index f947888..3d23ef0 100644 --- a/tests/test_scenarios.py +++ b/tests/test_scenarios.py @@ -188,8 +188,10 @@ class Ctx: async def boot(self): app = self.app await self.wait(lambda: self.table.row_count > 0, 60) + # Load is done when the index is complete (robust vs the status line, + # which startup auto-land immediately overwrites with the landed fn). await self.wait( - lambda: "functions" in self.status() and "…" not in self.status(), 60) + lambda: app._func_index is not None and app._func_index.complete, 60) if app._func_index is not None and not app._func_index.complete: app._func_index.load_all() @@ -231,11 +233,42 @@ async def s_startup(c: Ctx): await c.reveal_pane() c.check("function pane width is capped (doesn't eat the screen)", left.size.width <= 44, f"width={left.size.width}") - c.check("function load completed (status settled)", - "functions" in c.status() and "…" not in c.status(), c.status()) + c.check("function load completed (index complete)", + c.app._func_index is not None and c.app._func_index.complete, c.status()) print(f" {c.table.row_count} functions loaded") +@scenario("auto_land") +async def s_auto_land(c: Ctx): + """Startup lands on main()/an entry function when present, else pops the + symbol picker — never a blank pane.""" + app = c.app + # simulate a fresh startup landing + for _ in range(3): + if len(app.screen_stack) > 1: + app.pop_screen() + await c.pause(0.05) + app._cur = None + app._did_auto_land = False + fn = app._entry_func() + app._auto_land() + await c.pause(0.2) + if fn is not None: + await c.wait(lambda: app._cur is not None and app._cur.ea == fn.addr, 20) + c.check("auto-land jumps to the entry function (main) when present", + app._cur is not None and app._cur.ea == fn.addr, + f"entry={fn.name}@{fn.addr:#x} cur={app._cur}") + else: + await c.wait(lambda: isinstance(app.screen, SymbolPalette), 10) + c.check("auto-land pops the symbol picker when there's no entry fn", + isinstance(app.screen, SymbolPalette), f"screen={app.screen}") + app.pop_screen() + # guard fires once: a second call is a no-op + prev = app._cur + app._auto_land() + c.check("auto-land is idempotent (guarded)", app._cur is prev) + + @scenario("palette") async def s_palette(c: Ctx): app, pilot = c.app, c.pilot |
