aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--idatui/app.py34
-rw-r--r--tests/test_scenarios.py39
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