aboutsummaryrefslogtreecommitdiffstats
path: root/idatui/prompt.py
blob: 74956f623bcdeb90b81503fff7344694dd7d00dd (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
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