diff options
| author | blasty <blasty@local> | 2026-07-10 12:04:48 +0200 |
|---|---|---|
| committer | blasty <blasty@local> | 2026-07-10 12:04:48 +0200 |
| commit | bc03ad0a233927dfbcbe6d91c6ddd89d33e8ebc8 (patch) | |
| tree | 6c2f6b75fd3b5dfc780815c5b3d6099a3827bcec | |
| parent | server: inject a del_type tool into ida-pro-mcp (enables struct delete) (diff) | |
| download | ida-tui-bc03ad0a233927dfbcbe6d91c6ddd89d33e8ebc8.tar.gz ida-tui-bc03ad0a233927dfbcbe6d91c6ddd89d33e8ebc8.tar.xz ida-tui-bc03ad0a233927dfbcbe6d91c6ddd89d33e8ebc8.zip | |
struct editor: discoverable, confirmed delete (d/Del + ConfirmScreen)
Delete was wired to the Del key but undiscoverable (modal has no footer) and
unguarded. Add a 'd' alias (Del still works), and route delete through a small
ConfirmScreen (Enter/y confirm, Esc/n cancel) since it's destructive. 'd' in the
editor pane types normally; only the list triggers delete. Hint updated to
'd/Del delete'.
Pilot: 'd' opens the confirm, Esc cancels (kept), Enter deletes. full suite 85/85.
| -rw-r--r-- | idatui/app.py | 45 | ||||
| -rw-r--r-- | tests/test_tui.py | 18 |
2 files changed, 55 insertions, 8 deletions
diff --git a/idatui/app.py b/idatui/app.py index 9cb0db5..6d2f38d 100644 --- a/idatui/app.py +++ b/idatui/app.py @@ -1166,6 +1166,33 @@ class SymbolPalette(ModalScreen): # --------------------------------------------------------------------------- # +# Confirmation dialog +# --------------------------------------------------------------------------- # +class ConfirmScreen(ModalScreen): + """A tiny yes/no confirmation; dismisses True (confirm) or False (cancel).""" + + BINDINGS = [ + Binding("enter,y", "confirm", "Confirm"), + Binding("escape,n", "cancel", "Cancel"), + ] + + def __init__(self, message: str) -> None: + super().__init__() + self._message = message + + def compose(self) -> ComposeResult: + with Vertical(id="confirm-box"): + yield Static(self._message, id="confirm-msg") + yield Static("[Enter/y] confirm [Esc/n] cancel", id="confirm-help") + + def action_confirm(self) -> None: + self.dismiss(True) + + def action_cancel(self) -> None: + self.dismiss(False) + + +# --------------------------------------------------------------------------- # # Struct editor (C-style local type editor) # --------------------------------------------------------------------------- # class StructEditor(ModalScreen): @@ -1180,7 +1207,7 @@ class StructEditor(ModalScreen): BINDINGS = [ Binding("ctrl+s", "save", "Save", priority=True), Binding("ctrl+n", "new", "New", priority=True), - Binding("delete", "delete", "Delete", show=False), + Binding("delete,d", "delete", "Delete", show=False), Binding("escape", "close", "Close"), ] @@ -1202,7 +1229,7 @@ class StructEditor(ModalScreen): yield Static(" C definition", id="se-hint") yield TextArea("", id="se-edit") yield Static( - "Enter edit · Ctrl+S save · Ctrl+N new · Del delete · Esc back/close", + "Enter edit · Ctrl+S save · Ctrl+N new · d/Del delete · Esc back/close", id="se-status") def on_mount(self) -> None: @@ -1294,11 +1321,18 @@ class StructEditor(ModalScreen): self._set_status("new struct — edit and Ctrl+S") def action_delete(self) -> None: + # Delete is only meaningful from the list; a bare 'd' in the editor types. + if self.focused is self.query_one("#se-edit", TextArea): + return ol = self.query_one("#se-list", OptionList) i = ol.highlighted if i is None or not (0 <= i < len(self._structs)): return - self._delete(self._structs[i].name) + s = self._structs[i] + kind = "union" if s.is_union else "struct" + self.app.push_screen( + ConfirmScreen(f"Delete {kind} '{s.name}' ?"), + lambda ok, name=s.name: self._delete(name) if ok else None) @work(thread=True, exclusive=True, group="se-del") def _delete(self, name: str) -> None: @@ -1376,6 +1410,11 @@ class IdaTui(App): #se-list { height: 1fr; } #se-edit { height: 1fr; border: none; } #se-status { height: 1; background: $panel-darken-2; color: $text-muted; padding: 0 1; } + ConfirmScreen { align: center middle; } + #confirm-box { width: 60; height: auto; border: thick $warning; + background: $panel; padding: 1 2; } + #confirm-msg { height: auto; } + #confirm-help { height: 1; color: $text-muted; margin-top: 1; } """ BINDINGS = [ diff --git a/tests/test_tui.py b/tests/test_tui.py index f2e38ff..90c882b 100644 --- a/tests/test_tui.py +++ b/tests/test_tui.py @@ -19,8 +19,8 @@ import sys sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from idatui.app import ( # noqa: E402 - DecompView, DisasmView, FunctionsPanel, IdaTui, StructEditor, SymbolPalette, - XrefsScreen, + ConfirmScreen, DecompView, DisasmView, FunctionsPanel, IdaTui, StructEditor, + SymbolPalette, XrefsScreen, ) from textual.widgets import DataTable, Input, OptionList, Static, TextArea # noqa: E402 from rich.text import Text # noqa: E402 @@ -185,17 +185,25 @@ async def run(db): check("Ctrl+S updates an existing struct in place", next((s.members for s in se._structs if s.name == sname), 0) == 3, "member count not 3") - # delete: removes it if the server supports del_type, else reports it + # delete: 'd' asks to confirm, Enter deletes (server del_type). If the + # server lacks del_type, the status reports it instead. idx = next(i for i, s in enumerate(se._structs) if s.name == sname) + se.query_one(OptionList).focus() se.query_one(OptionList).highlighted = idx - se.action_delete() + await pilot.press("d") + confirmed = await wait_until( + pilot, lambda: isinstance(app.screen, ConfirmScreen), 10) + check("delete asks for confirmation", confirmed, + f"screen={type(app.screen).__name__}") + await pilot.press("enter") + await wait_until(pilot, lambda: isinstance(app.screen, StructEditor), 10) await wait_until( pilot, lambda: not any(s.name == sname for s in se._structs) or "del_type" in str(se.query_one("#se-status").render()), 15) st = str(se.query_one("#se-status").render()) gone = not any(s.name == sname for s in se._structs) - check("delete removes the struct (or reports the missing server tool)", + check("confirming delete removes the struct (or reports missing tool)", gone or "del_type" in st, f"gone={gone} status={st!r}") se.query_one(OptionList).focus() await pilot.press("escape") |
