aboutsummaryrefslogtreecommitdiffstats
path: root/idatui/prompt.py
diff options
context:
space:
mode:
authorblasty <blasty@local>2026-08-06 23:07:30 +0200
committerblasty <blasty@local>2026-08-06 23:07:30 +0200
commite0e65b1a26f96b304b098490da77ed1722d2260d (patch)
treed950058ab8a1607bfca2384d9cf6a86bb427076f /idatui/prompt.py
parentapp: lift trace navigation out of IdaTui (diff)
downloadida-tui-e0e65b1a26f96b304b098490da77ed1722d2260d.tar.gz
ida-tui-e0e65b1a26f96b304b098490da77ed1722d2260d.tar.xz
ida-tui-e0e65b1a26f96b304b098490da77ed1722d2260d.zip
app: lift database edits out of IdaTui, and one prompt abstraction
Second cut at the 4000-line class, and the biggest: 702 contiguous lines of rename/comment/retype/make-data/literal-format/define, now EditController. What stays on IdaTui is what Textual insists on owning -- on_<Message> handlers, which it dispatches by name on the DOMNode, and @work entry points, whose worker machinery wants a DOMNode host. Both are one-line delegates. Underneath them was the duplication that made this worth doing rather than just moving lines. Opening a prompt (hide the status bar, set placeholder, can_focus, display, value, focus) was written out five times; closing it four; and Esc was a six-branch ladder in on_key with one copy of the same four lines per prompt. They had drifted -- search cancelled its highlight, goto restored focus, the edit prompts did neither consistently. Prompt/PromptBar own that discipline once, and close() HANDS BACK the context it was holding, so it can't be read twice or go stale: the listing's rename had to capture _rename_addr by hand before _end_rename cleared it, or the name went to address 0. _line_ea_for stays on the app -- rpc.py and the goto readback ask the same question, so it was never an edit helper.
Diffstat (limited to 'idatui/prompt.py')
-rw-r--r--idatui/prompt.py117
1 files changed, 117 insertions, 0 deletions
diff --git a/idatui/prompt.py b/idatui/prompt.py
new file mode 100644
index 0000000..74956f6
--- /dev/null
+++ b/idatui/prompt.py
@@ -0,0 +1,117 @@
+"""The one-line prompts along the bottom of the app.
+
+There are six (`search` `rename` `comment` `retype` `makedata` `goto`) and every
+one of them was opened, closed and escaped by its own copy of the same six
+lines. The copies had drifted: some restored focus to the view, some didn't;
+some cleared their context on close, some left it for the next caller to trip
+over.
+
+Opening a prompt is: hide the status bar (they share a row), set the
+placeholder and prefill, make it focusable, show it, focus it. Closing is the
+same in reverse, plus handing focus back to whatever view asked for it. That is
+all `Prompt` is -- but having it in one place is what makes "Esc closes whatever
+is open" a loop instead of a six-branch ladder in `on_key`.
+
+Note the `can_focus` toggling: a hidden `Input` that stays focusable still takes
+part in Tab focus-nav, so tabbing around a closed prompt used to land the cursor
+in an invisible widget and swallow every subsequent keystroke.
+"""
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+from textual.widgets import Input, Static
+
+if TYPE_CHECKING: # pragma: no cover
+ from textual.app import App
+
+
+class Prompt:
+ """One `Input` at the bottom of the screen, plus who to give focus back to.
+
+ ``ctx`` is whatever the opener needs when the value is submitted (the view
+ that asked, the address being commented, the kind of retype...). It is held
+ here rather than in a parallel ``_rename_ctx`` attribute on the app so that
+ it cannot outlive the prompt that owns it.
+ """
+
+ def __init__(self, app: "App", ident: str) -> None:
+ self.app = app
+ self.id = ident
+ self.ctx: object = None
+
+ @property
+ def input(self) -> Input:
+ return self.app.query_one(f"#{self.id}", Input)
+
+ @property
+ def open(self) -> bool:
+ """Is this prompt on screen? Cheap enough to poll in `on_key`."""
+ try:
+ return bool(self.input.display)
+ except Exception: # noqa: BLE001 -- a modal owns the screen
+ return False
+
+ def show(self, placeholder: str, value: str = "", ctx: object = None) -> Input:
+ """Put the prompt up, prefilled and focused."""
+ self.ctx = ctx
+ self.app.query_one("#status", Static).display = False
+ inp = self.input
+ inp.placeholder = placeholder
+ inp.can_focus = True
+ inp.display = True
+ inp.value = value
+ inp.focus()
+ return inp
+
+ def close(self, refocus: bool = True) -> object:
+ """Take the prompt down and return the context it was holding.
+
+ Returned rather than left readable, because every caller wants it
+ exactly once and the old parallel-attribute version kept handing the
+ next caller a stale one (the listing's `_rename_addr` had to be captured
+ by hand before `_end_rename` cleared it, or the name went to address 0).
+ """
+ ctx, self.ctx = self.ctx, None
+ inp = self.input
+ inp.display = False
+ inp.can_focus = False
+ self.app.query_one("#status", Static).display = True
+ if refocus:
+ view = ctx[0] if isinstance(ctx, tuple) and ctx else None
+ if view is not None and hasattr(view, "focus"):
+ view.focus()
+ return ctx
+
+
+class PromptBar:
+ """Every prompt the app owns, so "close whatever is open" is one call."""
+
+ def __init__(self, app: "App", *idents: str) -> None:
+ self.app = app
+ self._order = list(idents)
+ self._by_id = {i: Prompt(app, i) for i in idents}
+
+ def __getitem__(self, ident: str) -> Prompt:
+ return self._by_id[ident]
+
+ def __getattr__(self, name: str) -> Prompt:
+ try:
+ return self.__dict__["_by_id"][name]
+ except KeyError:
+ raise AttributeError(name) from None
+
+ def active(self) -> Prompt | None:
+ """The prompt currently on screen, in declaration order.
+
+ Only one is ever up -- they share the row above the footer -- but the
+ order is fixed anyway so this can't depend on dict iteration.
+ """
+ for ident in self._order:
+ p = self._by_id[ident]
+ if p.open:
+ return p
+ return None
+
+ def any_open(self) -> bool:
+ return self.active() is not None