diff options
| author | blasty <blasty@local> | 2026-08-06 23:07:30 +0200 |
|---|---|---|
| committer | blasty <blasty@local> | 2026-08-06 23:07:30 +0200 |
| commit | e0e65b1a26f96b304b098490da77ed1722d2260d (patch) | |
| tree | d950058ab8a1607bfca2384d9cf6a86bb427076f /idatui/prompt.py | |
| parent | app: lift trace navigation out of IdaTui (diff) | |
| download | ida-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.py | 117 |
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 |
