diff options
| author | blasty <blasty@local> | 2026-07-24 16:17:58 +0200 |
|---|---|---|
| committer | blasty <blasty@local> | 2026-07-24 16:17:58 +0200 |
| commit | 31f00a82ab992add03dd3891c96a5e6292be2ed2 (patch) | |
| tree | e5656992556ff3dca02dc38b85883451ff8e59eb | |
| parent | ux: omit the splash logo when the terminal is too small for it (diff) | |
| download | ida-tui-31f00a82ab992add03dd3891c96a5e6292be2ed2.tar.gz ida-tui-31f00a82ab992add03dd3891c96a5e6292be2ed2.tar.xz ida-tui-31f00a82ab992add03dd3891c96a5e6292be2ed2.zip | |
palette: replace the stock Ctrl+P command palette with real ida-tui actions
The Ctrl+P palette was Textual's stock system commands (change theme / take
screenshot / quit) -- useless for RE. Add an IdaCommands command Provider and set
IdaTui.COMMANDS = {IdaCommands} so the palette lists real actions instead:
goto, find symbol, follow, xrefs, toggle disasm/pseudocode, continuous listing,
hex, rename, retype, comment, define code/func, make data/string, undefine,
toggle opcodes, structs editor, filter, names pane, save, quit -- each with its
keybind as help text, fuzzy-searchable.
App-level actions run directly; cursor-scoped ones (rename/xrefs/comment/...) are
dispatched to the active code view via IdaTui._palette_action (focus + run the
view's action_*), so a palette pick does exactly what the key does.
Verified live over RPC: 'hex' switches view, 'goto' opens the prompt, 'struct'
opens the StructEditor modal, and the stock 'theme' command is gone. Pilot
command_palette scenario: Ctrl+P opens it and a command executes (7/7).
Diffstat (limited to '')
| -rw-r--r-- | idatui/app.py | 79 | ||||
| -rw-r--r-- | tests/test_scenarios.py | 23 |
2 files changed, 102 insertions, 0 deletions
diff --git a/idatui/app.py b/idatui/app.py index ac55864..2a3e617 100644 --- a/idatui/app.py +++ b/idatui/app.py @@ -28,6 +28,7 @@ from rich.text import Text from textual import work from textual.app import App, ComposeResult from textual.binding import Binding +from textual.command import DiscoveryHit, Hit, Provider from textual.containers import Horizontal, Vertical from textual.geometry import Region, Size from textual.message import Message @@ -2342,7 +2343,71 @@ class StructEditor(ModalScreen): # --------------------------------------------------------------------------- # # The app # --------------------------------------------------------------------------- # +class IdaCommands(Provider): + """Fills the Ctrl+P command palette with real ida-tui actions instead of the + stock Textual system commands (change theme / take screenshot / …). + + App-level actions run directly; cursor-scoped ones (rename/xrefs/comment/…) + are dispatched to the active code view via ``IdaTui._palette_action``.""" + + def _commands(self): + app = self.app + va = app._palette_action # dispatch to the focused code view + return ( + ("Goto address / symbol…", "jump to an address or name (g)", + app.action_goto), + ("Find symbol…", "fuzzy function finder (Ctrl+N)", app.action_symbols), + ("Follow symbol under cursor", "jump to the referenced symbol (Enter)", + lambda: va("follow")), + ("Show xrefs to symbol", "cross-references to the cursor symbol (x)", + lambda: va("xrefs")), + ("Back", "navigation history (Esc)", app.action_back), + ("Toggle disassembly / pseudocode", "decompile / listing (F5, Tab)", + app.action_toggle_view), + ("Continuous listing here", "flat segment listing (L)", + app.action_continuous_here), + ("Hex view", "raw bytes at the cursor (\\)", app.action_hex), + ("Rename symbol…", "rename the symbol under the cursor (n)", + lambda: va("rename")), + ("Set type / prototype…", "retype the symbol under the cursor (y)", + lambda: va("retype")), + ("Add comment…", "comment at the cursor (;)", lambda: va("comment")), + ("Define code", "make code at the cursor (c)", lambda: va("define_code")), + ("Create function", "define a function at the cursor (p)", + lambda: va("define_func")), + ("Make data", "define a data item at the cursor (d)", + lambda: va("make_data")), + ("Make string", "define a string at the cursor (a)", + lambda: va("make_string")), + ("Undefine", "undefine the item at the cursor (u)", + lambda: va("undefine")), + ("Toggle opcode bytes", "cycle the opcode-bytes column (o)", + lambda: va("toggle_opcodes")), + ("Structs / types editor", "view + edit local types (Ctrl+T)", + app.action_structs), + ("Filter functions…", "glob-filter the function list (/)", + app.action_filter), + ("Toggle names pane", "function-list sidebar (Ctrl+B)", + app.action_toggle_functions), + ("Save database (.i64)", "persist changes (Ctrl+S)", app.action_save), + ("Quit", "exit ida-tui (q)", app.action_quit), + ) + + async def discover(self): + for title, help_text, cb in self._commands(): + yield DiscoveryHit(title, cb, help=help_text) + + async def search(self, query: str): + matcher = self.matcher(query) + for title, help_text, cb in self._commands(): + score = matcher.match(title) + if score > 0: + yield Hit(score, matcher.highlight(title), cb, help=help_text) + + class IdaTui(App): + COMMANDS = {IdaCommands} # replace the stock system-commands palette + CSS = """ Screen { layout: vertical; } #panes { height: 1fr; } @@ -4061,6 +4126,20 @@ class IdaTui(App): return self.query_one(DecompView) return None + def _palette_action(self, name: str) -> None: + """Run a cursor-scoped code-view action (rename/xrefs/follow/…) picked from + the command palette against the active code view.""" + view = self._active_code_view() + if view is None: + self._status("open a function first") + return + view.focus() + fn = getattr(view, f"action_{name}", None) + if fn is None: + self._status(f"'{name}' isn't available in this view") + return + fn() + def action_continuous_here(self) -> None: """'L': open the continuous segment listing at the cursor — one long flat view where functions, data and undefined bytes are interleaved, diff --git a/tests/test_scenarios.py b/tests/test_scenarios.py index 1a07fb4..94baf93 100644 --- a/tests/test_scenarios.py +++ b/tests/test_scenarios.py @@ -315,6 +315,29 @@ async def s_palette(c: Ctx): c.check("Esc closes the palette", not isinstance(app.screen, SymbolPalette)) +@scenario("command_palette") +async def s_command_palette(c: Ctx): + app = c.app + await c.open_biggest("listing") + await c.press("ctrl+p") + opened = await c.wait( + lambda: type(app.screen).__name__ == "CommandPalette", 10) + c.check("Ctrl+P opens the command palette", opened, + f"screen={type(app.screen).__name__}") + if not opened: + return + inp = app.screen.query_one(Input) + inp.value = "hex" # filter to the 'Hex view' command + await c.pause(0.5) # let the async search + option list settle + await c.press("enter") + landed = await c.wait(lambda: app._active == "hex", 10) + c.check("a palette command executes (Hex view opens)", landed, + f"active={app._active}") + if landed: + await c.press("backslash") # leave hex + await c.wait(lambda: app._active != "hex", 5) + + @scenario("decomp_fallback") async def s_fallback(c: Ctx): app = c.app |
