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
|