termcore 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- termcore/__init__.py +6 -0
- termcore/cli/__init__.py +3 -0
- termcore/cli/output.py +70 -0
- termcore/io/__init__.py +7 -0
- termcore/io/app_state_storage.py +185 -0
- termcore/io/database.py +655 -0
- termcore/io/errors.py +71 -0
- termcore/io/file.py +263 -0
- termcore/io/textfile.py +92 -0
- termcore/py.typed +0 -0
- termcore/tui/__init__.py +9 -0
- termcore/tui/custom_bindings.py +551 -0
- termcore/tui/custom_widgets/__init__.py +1 -0
- termcore/tui/custom_widgets/custom_checkbox.py +41 -0
- termcore/tui/custom_widgets/custom_data_table.py +247 -0
- termcore/tui/custom_widgets/custom_selection.py +80 -0
- termcore/tui/custom_widgets/multiline_footer.py +240 -0
- termcore/tui/question_screen.py +107 -0
- termcore/tui/theme_loader.py +495 -0
- termcore/tui/themes/__init__.py +2 -0
- termcore/tui/themes/classic-black-saturated/style.css +57 -0
- termcore/tui/themes/classic-black-saturated/theme.py +22 -0
- termcore/tui/themes/classic-black-v1/style.css +57 -0
- termcore/tui/themes/classic-black-v1/theme.py +22 -0
- termcore/tui/themes/classic-black-v2/style.css +57 -0
- termcore/tui/themes/classic-black-v2/theme.py +24 -0
- termcore/tui/themes/classic-blue/theme.py +21 -0
- termcore/tui/themes/compact-gray/style.tcss +175 -0
- termcore/tui/themes/compact-gray/theme.py +22 -0
- termcore/tui/themes/mnml-black/style.css +57 -0
- termcore/tui/themes/mnml-black/theme.py +21 -0
- termcore/tui/themes/mnml-deepblack/style.css +57 -0
- termcore/tui/themes/mnml-deepblack/theme.py +21 -0
- termcore/tui/themes/pure-amber/theme.py +18 -0
- termcore/tui/themes/pure-black/theme.py +18 -0
- termcore/tui/themes/pure-blue/theme.py +18 -0
- termcore/tui/themes/pure-green/theme.py +18 -0
- termcore/tui/themes/pure-sweet16/theme.py +20 -0
- termcore/tui/themes/xplore-black/theme.py +18 -0
- termcore/tui/themes/xplore-blue/theme.py +18 -0
- termcore/tui/themes/xplore-blue-muted/theme.py +18 -0
- termcore/tui/themes/xplore-teal/theme.py +18 -0
- termcore/util/__init__.py +10 -0
- termcore/util/datetime.py +173 -0
- termcore/util/debug.py +196 -0
- termcore/util/index.py +112 -0
- termcore/util/logger.py +36 -0
- termcore/util/singleton.py +46 -0
- termcore/util/string.py +263 -0
- termcore/util/validation.py +31 -0
- termcore/util/version.py +56 -0
- termcore-0.2.0.dist-info/METADATA +504 -0
- termcore-0.2.0.dist-info/RECORD +55 -0
- termcore-0.2.0.dist-info/WHEEL +4 -0
- termcore-0.2.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Key bindings for Textual applications, declared in YAML.
|
|
3
|
+
|
|
4
|
+
This module defines the class `CustomBindings` for managing custom keyboard
|
|
5
|
+
bindings in a Textual application. It loads key binding definitions from
|
|
6
|
+
a YAML file and exposes them as Textual `Binding` objects.
|
|
7
|
+
|
|
8
|
+
Reserved group naming conventions:
|
|
9
|
+
|
|
10
|
+
- `_global` Always-visible bindings; action is used as-is (no prefix)
|
|
11
|
+
- `<name>_tab` Shown only when that tab is active; action is prefixed with
|
|
12
|
+
the full group name, e.g. `tasks_tab_add_task`
|
|
13
|
+
- `<name>_screen` Screen-specific bindings; action is used as-is (no prefix)
|
|
14
|
+
|
|
15
|
+
YAML structure
|
|
16
|
+
--------------
|
|
17
|
+
The YAML file is a mapping of group names to lists of binding definitions.
|
|
18
|
+
Each binding supports the following fields:
|
|
19
|
+
|
|
20
|
+
key (required) Key to bind, e.g. `q`, `f1`, `ctrl+s`
|
|
21
|
+
action (required) Action name (see group-specific prefixing below)
|
|
22
|
+
description (required) Short label shown in the footer
|
|
23
|
+
tooltip Longer description shown on hover
|
|
24
|
+
key_display Override how the key is rendered in the footer
|
|
25
|
+
row Footer row index (0-based, default: 0)
|
|
26
|
+
priority bool - show binding even when a widget captures input
|
|
27
|
+
show bool - whether to show in the footer (default: true)
|
|
28
|
+
id Optional binding ID
|
|
29
|
+
system bool - mark as a system binding
|
|
30
|
+
|
|
31
|
+
Group naming rules
|
|
32
|
+
------------------
|
|
33
|
+
`_global`
|
|
34
|
+
Bindings that are always visible. The action is used as-is (no prefix),
|
|
35
|
+
e.g. `action: quit` → `quit`. When included in a Screen's `BINDINGS`
|
|
36
|
+
via `get_bindings(for_screen=True)`, these actions are automatically
|
|
37
|
+
prefixed with `app.` so Textual dispatches them on the App.
|
|
38
|
+
|
|
39
|
+
`<name>_tab`
|
|
40
|
+
Tab-specific bindings, shown only when that tab is active. The action is
|
|
41
|
+
prefixed with the group name, e.g. group `tasks_tab` + `action: add`
|
|
42
|
+
→ `tasks_tab_add`. Use `check_action` with the active tab name as
|
|
43
|
+
`active_group` to control visibility.
|
|
44
|
+
|
|
45
|
+
`<name>_screen`
|
|
46
|
+
Screen-specific bindings. The action is used as-is (no prefix), so it maps
|
|
47
|
+
directly to an `action_<name>` method on the Screen.
|
|
48
|
+
|
|
49
|
+
Example
|
|
50
|
+
-------
|
|
51
|
+
.. code-block:: yaml
|
|
52
|
+
|
|
53
|
+
# Always shown (action = <action>, no prefix)
|
|
54
|
+
_global:
|
|
55
|
+
- key: q
|
|
56
|
+
action: quit
|
|
57
|
+
description: Quit
|
|
58
|
+
tooltip: Exit the application
|
|
59
|
+
priority: true
|
|
60
|
+
row: 1
|
|
61
|
+
|
|
62
|
+
# Shown only when "tasks_tab" is active (action = tasks_tab_<action>)
|
|
63
|
+
tasks_tab:
|
|
64
|
+
- key: a
|
|
65
|
+
action: add_task
|
|
66
|
+
description: Add
|
|
67
|
+
tooltip: Add a new task
|
|
68
|
+
row: 0
|
|
69
|
+
- key: d
|
|
70
|
+
action: mark_done
|
|
71
|
+
description: Done
|
|
72
|
+
tooltip: Mark the selected task as done
|
|
73
|
+
row: 0
|
|
74
|
+
|
|
75
|
+
# Shown only on AddScreen (action used as-is)
|
|
76
|
+
add_screen:
|
|
77
|
+
- key: escape
|
|
78
|
+
action: cancel
|
|
79
|
+
description: Cancel
|
|
80
|
+
tooltip: Cancel and close
|
|
81
|
+
row: 0
|
|
82
|
+
"""
|
|
83
|
+
import logging
|
|
84
|
+
import re
|
|
85
|
+
from dataclasses import replace
|
|
86
|
+
from pathlib import Path
|
|
87
|
+
|
|
88
|
+
import yaml
|
|
89
|
+
from textual.binding import Binding, BindingType
|
|
90
|
+
|
|
91
|
+
__all__ = [
|
|
92
|
+
"CustomBindings",
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
_logger = logging.getLogger(__name__)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _sort_key(binding: Binding) -> str:
|
|
99
|
+
"""Turns a key like "F1" or "f1" into "f01", so f2 sorts before f10."""
|
|
100
|
+
match = re.match(r"(f)(\d+)", binding.key.lower())
|
|
101
|
+
if match:
|
|
102
|
+
return f"{match.group(1)}{int(match.group(2)):02d}"
|
|
103
|
+
|
|
104
|
+
return binding.key.lower()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class CustomBindings:
|
|
108
|
+
"""
|
|
109
|
+
Manages the key bindings loaded from a YAML file.
|
|
110
|
+
|
|
111
|
+
See the module docstring for the YAML structure and the group naming
|
|
112
|
+
rules.
|
|
113
|
+
|
|
114
|
+
Attributes
|
|
115
|
+
----------
|
|
116
|
+
yaml_file_path : str
|
|
117
|
+
Path to the YAML file containing key bindings.
|
|
118
|
+
sort_alphabetically : bool
|
|
119
|
+
Whether to sort bindings alphabetically by key.
|
|
120
|
+
If false, they are sorted in the order they appear in the YAML file.
|
|
121
|
+
bindings_dict_raw : dict[str, list[dict[str, str]]]
|
|
122
|
+
Raw data loaded from the YAML file.
|
|
123
|
+
bindings_dict : dict[str, list[Binding]]
|
|
124
|
+
Processed key bindings grouped by their group name.
|
|
125
|
+
action_to_groups : dict[str, list[str]]
|
|
126
|
+
Maps actions to the groups they belong to.
|
|
127
|
+
action_row_map : dict[str, int]
|
|
128
|
+
Maps actions to their specified footer row index.
|
|
129
|
+
global_actions : list[str]
|
|
130
|
+
List of actions that are always shown globally.
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
_yaml_file_path: str
|
|
134
|
+
_sort_alphabetically: bool
|
|
135
|
+
_bindings_dict_raw: dict[str, list[dict[str, str]]]
|
|
136
|
+
_bindings_dict: dict[str, list[Binding]]
|
|
137
|
+
_action_to_groups: dict[str, list[str]]
|
|
138
|
+
_action_row_map: dict[str, int]
|
|
139
|
+
_global_actions: list[str]
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def __init__(self, yaml_file: str) -> None:
|
|
143
|
+
"""
|
|
144
|
+
Reads the YAML file, keeping the order it declares.
|
|
145
|
+
|
|
146
|
+
Use `CustomBindings.sorted_by_key` for a set sorted by key instead.
|
|
147
|
+
"""
|
|
148
|
+
self._yaml_file_path = yaml_file
|
|
149
|
+
self._sort_alphabetically = False
|
|
150
|
+
self._bindings_dict = {}
|
|
151
|
+
self._action_to_groups = {}
|
|
152
|
+
self._action_row_map = {}
|
|
153
|
+
self._global_actions = []
|
|
154
|
+
self._read_yaml_file()
|
|
155
|
+
self._process_bindings()
|
|
156
|
+
|
|
157
|
+
@classmethod
|
|
158
|
+
def sorted_by_key(cls, yaml_file: str) -> "CustomBindings":
|
|
159
|
+
"""
|
|
160
|
+
Returns the bindings of the file, sorted by key within each group.
|
|
161
|
+
|
|
162
|
+
Function keys sort numerically, so f2 comes before f10.
|
|
163
|
+
|
|
164
|
+
Parameters
|
|
165
|
+
----------
|
|
166
|
+
yaml_file : str
|
|
167
|
+
Path to the YAML file containing the key bindings.
|
|
168
|
+
|
|
169
|
+
Returns
|
|
170
|
+
-------
|
|
171
|
+
CustomBindings
|
|
172
|
+
The loaded bindings, ordered by key rather than by file order.
|
|
173
|
+
"""
|
|
174
|
+
bindings = cls(yaml_file)
|
|
175
|
+
bindings._sort_alphabetically = True
|
|
176
|
+
|
|
177
|
+
return bindings
|
|
178
|
+
|
|
179
|
+
def _read_yaml_file(self) -> None:
|
|
180
|
+
"""Loads the binding definitions from the YAML file."""
|
|
181
|
+
with Path(self._yaml_file_path).open(encoding="utf-8") as file:
|
|
182
|
+
self._bindings_dict_raw = yaml.safe_load(file)
|
|
183
|
+
|
|
184
|
+
def _process_bindings(self) -> None:
|
|
185
|
+
"""
|
|
186
|
+
Turns the raw YAML data into `Binding` instances.
|
|
187
|
+
|
|
188
|
+
Groups them by their group name, builds the `action_to_groups`
|
|
189
|
+
mapping and records the global actions in `global_actions`.
|
|
190
|
+
"""
|
|
191
|
+
# Loop groups
|
|
192
|
+
for group, bindings in self._bindings_dict_raw.items():
|
|
193
|
+
if group not in self._bindings_dict:
|
|
194
|
+
self._bindings_dict[group] = []
|
|
195
|
+
|
|
196
|
+
# Loop bindings
|
|
197
|
+
for binding in bindings:
|
|
198
|
+
key = self._parse_key(binding.get("key"))
|
|
199
|
+
action = self._parse_action(binding.get("action"), group)
|
|
200
|
+
description = self._parse_description(
|
|
201
|
+
binding.get("description")
|
|
202
|
+
)
|
|
203
|
+
show = self._parse_show(binding.get("show"))
|
|
204
|
+
key_display = self._parse_key_display(
|
|
205
|
+
key, binding.get("key_display")
|
|
206
|
+
)
|
|
207
|
+
priority = self._parse_priority(binding.get("priority"))
|
|
208
|
+
tooltip = self._parse_tooltip(binding.get("tooltip"))
|
|
209
|
+
binding_id = self._parse_id(binding.get("id"))
|
|
210
|
+
system = self._parse_system(binding.get("system"))
|
|
211
|
+
|
|
212
|
+
# Skip if any required field is missing. Spelled out rather
|
|
213
|
+
# than derived from the list below, so the type checker can
|
|
214
|
+
# narrow the three values for the constructor call.
|
|
215
|
+
if key is None or action is None or description is None:
|
|
216
|
+
missing = [
|
|
217
|
+
name for name, value in (
|
|
218
|
+
("key", key),
|
|
219
|
+
("action", action),
|
|
220
|
+
("description", description),
|
|
221
|
+
) if value is None
|
|
222
|
+
]
|
|
223
|
+
_logger.warning(
|
|
224
|
+
"Skipping a binding in group %r: %s missing",
|
|
225
|
+
group, ", ".join(missing)
|
|
226
|
+
)
|
|
227
|
+
continue
|
|
228
|
+
|
|
229
|
+
binding_instance = Binding(
|
|
230
|
+
key =key,
|
|
231
|
+
action =action,
|
|
232
|
+
description=description,
|
|
233
|
+
show =show,
|
|
234
|
+
key_display=key_display,
|
|
235
|
+
priority =priority,
|
|
236
|
+
tooltip =tooltip,
|
|
237
|
+
id =binding_id,
|
|
238
|
+
system =system
|
|
239
|
+
)
|
|
240
|
+
self._bindings_dict[group].append(binding_instance)
|
|
241
|
+
|
|
242
|
+
# Add action to action_to_groups mapping
|
|
243
|
+
if action not in self._action_to_groups:
|
|
244
|
+
self._action_to_groups[action] = [group]
|
|
245
|
+
else:
|
|
246
|
+
self._action_to_groups[action].append(group)
|
|
247
|
+
|
|
248
|
+
# Store row for this action. A second group claiming the
|
|
249
|
+
# same action overwrites the first one's footer row, which
|
|
250
|
+
# is worth saying rather than doing quietly.
|
|
251
|
+
if action in self._action_row_map:
|
|
252
|
+
_logger.warning(
|
|
253
|
+
"Action %r is declared more than once (%s); the "
|
|
254
|
+
"footer row of the earlier binding is overwritten",
|
|
255
|
+
action, ", ".join(self._action_to_groups[action])
|
|
256
|
+
)
|
|
257
|
+
self._action_row_map[action] = int(binding.get("row", 0))
|
|
258
|
+
|
|
259
|
+
# Add action to global actions if applicable
|
|
260
|
+
if group.startswith("_global"):
|
|
261
|
+
self._global_actions.append(action)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def get_row_map(self) -> dict[str, int]:
|
|
265
|
+
"""
|
|
266
|
+
Returns a row map for use with `MultiLineFooter(auto_wrap=False)`.
|
|
267
|
+
|
|
268
|
+
Uses the `row` values defined in the YAML file. Pair this with
|
|
269
|
+
`get_bindings`; a Screen wants `get_screen_row_map` instead.
|
|
270
|
+
|
|
271
|
+
Returns
|
|
272
|
+
-------
|
|
273
|
+
dict[str, int]
|
|
274
|
+
A mapping of action names to row numbers (0-based).
|
|
275
|
+
"""
|
|
276
|
+
return dict(self._action_row_map)
|
|
277
|
+
|
|
278
|
+
def get_screen_row_map(self) -> dict[str, int]:
|
|
279
|
+
"""
|
|
280
|
+
Returns the row map as a Screen needs it.
|
|
281
|
+
|
|
282
|
+
Global actions are keyed with the `app.` prefix, matching the actions
|
|
283
|
+
`get_screen_bindings` produces. If the two disagree, the footer
|
|
284
|
+
silently loses the row of every global binding.
|
|
285
|
+
|
|
286
|
+
Returns
|
|
287
|
+
-------
|
|
288
|
+
dict[str, int]
|
|
289
|
+
A mapping of action names to row numbers (0-based).
|
|
290
|
+
"""
|
|
291
|
+
return {
|
|
292
|
+
self._dispatch_name(action): row
|
|
293
|
+
for action, row in self._action_row_map.items()
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
def get_bindings(self, tab_name: str | None = None) -> list[BindingType]:
|
|
297
|
+
"""
|
|
298
|
+
Returns the bindings for the App itself.
|
|
299
|
+
|
|
300
|
+
Includes every tab group, or just one when `tab_name` is given, plus
|
|
301
|
+
the global bindings. Screen groups are left out; a Screen uses
|
|
302
|
+
`get_screen_bindings`.
|
|
303
|
+
|
|
304
|
+
Parameters
|
|
305
|
+
----------
|
|
306
|
+
tab_name : str or None, optional
|
|
307
|
+
Restricts the result to the bindings of that one tab.
|
|
308
|
+
|
|
309
|
+
Returns
|
|
310
|
+
-------
|
|
311
|
+
list[BindingType]
|
|
312
|
+
The bindings, globals last.
|
|
313
|
+
"""
|
|
314
|
+
self._sort_groups_by_key()
|
|
315
|
+
bindings: list[BindingType] = self._tab_bindings(tab_name)
|
|
316
|
+
bindings.extend(self._global_bindings())
|
|
317
|
+
|
|
318
|
+
return bindings
|
|
319
|
+
|
|
320
|
+
def get_screen_bindings(
|
|
321
|
+
self, screen_name: str | None = None
|
|
322
|
+
) -> list[BindingType]:
|
|
323
|
+
"""
|
|
324
|
+
Returns the bindings for a Screen, with globals dispatched on the App.
|
|
325
|
+
|
|
326
|
+
Without a name the tab bindings are included, which is what a screen
|
|
327
|
+
holding a `TabbedContent` needs. With one, only that screen's own
|
|
328
|
+
group is.
|
|
329
|
+
|
|
330
|
+
Parameters
|
|
331
|
+
----------
|
|
332
|
+
screen_name : str or None, optional
|
|
333
|
+
Name of the screen, without the `_screen` suffix its group
|
|
334
|
+
carries in the YAML file.
|
|
335
|
+
|
|
336
|
+
Returns
|
|
337
|
+
-------
|
|
338
|
+
list[BindingType]
|
|
339
|
+
The bindings, globals last and prefixed with `app.`.
|
|
340
|
+
"""
|
|
341
|
+
self._sort_groups_by_key()
|
|
342
|
+
|
|
343
|
+
bindings: list[BindingType] = (
|
|
344
|
+
self._tab_bindings(None) if screen_name is None
|
|
345
|
+
else self._screen_bindings(screen_name)
|
|
346
|
+
)
|
|
347
|
+
bindings.extend(self._app_dispatched_globals())
|
|
348
|
+
|
|
349
|
+
return bindings
|
|
350
|
+
|
|
351
|
+
def _sort_groups_by_key(self) -> None:
|
|
352
|
+
"""Sorts every group by key, if this instance was built that way."""
|
|
353
|
+
if not self._sort_alphabetically:
|
|
354
|
+
return
|
|
355
|
+
|
|
356
|
+
for group, bindings in self._bindings_dict.items():
|
|
357
|
+
self._bindings_dict[group] = sorted(bindings, key=_sort_key)
|
|
358
|
+
|
|
359
|
+
def _tab_bindings(self, tab_name: str | None) -> list[BindingType]:
|
|
360
|
+
"""Returns the bindings of every tab group, or of one named tab."""
|
|
361
|
+
if tab_name is not None:
|
|
362
|
+
self._warn_if_unknown(tab_name.lower(), tab_name)
|
|
363
|
+
|
|
364
|
+
bindings: list[BindingType] = []
|
|
365
|
+
for group, group_bindings in self._bindings_dict.items():
|
|
366
|
+
# Global and screen groups are appended by the callers instead
|
|
367
|
+
if group.startswith("_global") or group.endswith("_screen"):
|
|
368
|
+
continue
|
|
369
|
+
if tab_name and group != tab_name.lower():
|
|
370
|
+
continue
|
|
371
|
+
|
|
372
|
+
bindings.extend(group_bindings)
|
|
373
|
+
|
|
374
|
+
return bindings
|
|
375
|
+
|
|
376
|
+
def _screen_bindings(self, screen_name: str) -> list[BindingType]:
|
|
377
|
+
"""Returns the bindings of one screen group."""
|
|
378
|
+
group = f"{screen_name.lower()}_screen"
|
|
379
|
+
self._warn_if_unknown(group, screen_name)
|
|
380
|
+
|
|
381
|
+
return list(self._bindings_dict.get(group, []))
|
|
382
|
+
|
|
383
|
+
def _warn_if_unknown(self, group: str, given_name: str) -> None:
|
|
384
|
+
"""
|
|
385
|
+
Warns when a name matches no group in the YAML file.
|
|
386
|
+
|
|
387
|
+
Without this a typo is indistinguishable from a tab or screen that
|
|
388
|
+
genuinely declares no shortcuts of its own.
|
|
389
|
+
"""
|
|
390
|
+
if group in self._bindings_dict:
|
|
391
|
+
return
|
|
392
|
+
|
|
393
|
+
_logger.warning(
|
|
394
|
+
"No binding group %r for name %r; returning only the global "
|
|
395
|
+
"bindings", group, given_name
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
def _global_bindings(self) -> list[BindingType]:
|
|
399
|
+
"""Returns the always-visible bindings, as they were declared."""
|
|
400
|
+
bindings: list[BindingType] = []
|
|
401
|
+
for group, group_bindings in self._bindings_dict.items():
|
|
402
|
+
if group.startswith("_global"):
|
|
403
|
+
bindings.extend(group_bindings)
|
|
404
|
+
|
|
405
|
+
return bindings
|
|
406
|
+
|
|
407
|
+
def _app_dispatched_globals(self) -> list[BindingType]:
|
|
408
|
+
"""
|
|
409
|
+
Returns the global bindings with their action prefixed by `app.`.
|
|
410
|
+
|
|
411
|
+
A Screen would otherwise look for `action_quit` on itself, find
|
|
412
|
+
nothing, and the key would quietly do nothing.
|
|
413
|
+
"""
|
|
414
|
+
return [
|
|
415
|
+
replace(binding, action=f"app.{binding.action}")
|
|
416
|
+
for binding in self._global_bindings()
|
|
417
|
+
if isinstance(binding, Binding)
|
|
418
|
+
]
|
|
419
|
+
|
|
420
|
+
def _dispatch_name(self, action: str) -> str:
|
|
421
|
+
"""Returns the name a Screen dispatches the given action under."""
|
|
422
|
+
if action in self._global_actions:
|
|
423
|
+
return f"app.{action}"
|
|
424
|
+
|
|
425
|
+
return action
|
|
426
|
+
|
|
427
|
+
def handle_check_action(
|
|
428
|
+
self, action: str,
|
|
429
|
+
_parameters: tuple[object, ...],
|
|
430
|
+
active_group: str
|
|
431
|
+
) -> bool | None:
|
|
432
|
+
"""
|
|
433
|
+
Checks whether an action should be shown in the active group.
|
|
434
|
+
|
|
435
|
+
This is meant to be used in the check_action method of a Textual app.
|
|
436
|
+
|
|
437
|
+
Parameters
|
|
438
|
+
----------
|
|
439
|
+
action : str
|
|
440
|
+
The action to check.
|
|
441
|
+
parameters : tuple[object, ...]
|
|
442
|
+
Parameters for the action (not used).
|
|
443
|
+
active_group : str
|
|
444
|
+
The currently active group or tab.
|
|
445
|
+
|
|
446
|
+
Returns
|
|
447
|
+
-------
|
|
448
|
+
bool or None
|
|
449
|
+
True if the action should be displayed, False otherwise.
|
|
450
|
+
"""
|
|
451
|
+
# Ignore actions that are not defined in custom bindings
|
|
452
|
+
if not self._is_custom_action(action):
|
|
453
|
+
return True
|
|
454
|
+
|
|
455
|
+
# Global actions are always shown
|
|
456
|
+
if self._is_global_key(action):
|
|
457
|
+
return True
|
|
458
|
+
|
|
459
|
+
# Check if the action belongs to the current tab/group
|
|
460
|
+
return active_group in self._action_to_groups[action]
|
|
461
|
+
|
|
462
|
+
def _is_global_key(self, action: str) -> bool:
|
|
463
|
+
"""Checks if the given action belongs to a global key binding."""
|
|
464
|
+
return action in self._global_actions
|
|
465
|
+
|
|
466
|
+
def _is_custom_action(self, action: str) -> bool:
|
|
467
|
+
"""Returns True if the action is one the bindings declare."""
|
|
468
|
+
return action in self._action_to_groups
|
|
469
|
+
|
|
470
|
+
def _parse_key(self, key: str | None) -> str | None:
|
|
471
|
+
"""Parses the key field from the YAML binding definition."""
|
|
472
|
+
return key
|
|
473
|
+
|
|
474
|
+
def _parse_action(self, action: str | None, group: str) -> str | None:
|
|
475
|
+
"""Parses the action field, applying the group prefixing rules."""
|
|
476
|
+
if action is None:
|
|
477
|
+
return None
|
|
478
|
+
if group.startswith("_global") or group.endswith("_screen"):
|
|
479
|
+
return action
|
|
480
|
+
return f"{group}_{action}"
|
|
481
|
+
|
|
482
|
+
def _parse_description(self, description: str | None) -> str | None:
|
|
483
|
+
"""Parses the description field from the YAML binding definition."""
|
|
484
|
+
return description
|
|
485
|
+
|
|
486
|
+
def _parse_show(self, show: object) -> bool:
|
|
487
|
+
"""Parses the show field, defaulting to True."""
|
|
488
|
+
return self._parse_bool(show, field="show", default=True)
|
|
489
|
+
|
|
490
|
+
def _parse_key_display(
|
|
491
|
+
self, key: str | None, key_display: str | None
|
|
492
|
+
) -> str | None:
|
|
493
|
+
"""
|
|
494
|
+
Parses the key_display field of a binding.
|
|
495
|
+
|
|
496
|
+
A declared value wins - the field exists to override how the key is
|
|
497
|
+
rendered. Without one, a function key such as "f1" becomes "F1", and
|
|
498
|
+
anything else returns None, which leaves the rendering to Textual.
|
|
499
|
+
"""
|
|
500
|
+
if key_display is not None:
|
|
501
|
+
return key_display
|
|
502
|
+
if key is None:
|
|
503
|
+
return None
|
|
504
|
+
|
|
505
|
+
match = re.fullmatch(r"(f)(\d+)", key.lower())
|
|
506
|
+
if match:
|
|
507
|
+
return f"F{int(match.group(2))}"
|
|
508
|
+
|
|
509
|
+
return None
|
|
510
|
+
|
|
511
|
+
def _parse_priority(self, priority: object) -> bool:
|
|
512
|
+
"""Parses the priority field, defaulting to False."""
|
|
513
|
+
return self._parse_bool(priority, field="priority", default=False)
|
|
514
|
+
|
|
515
|
+
def _parse_tooltip(self, tooltip: str | None) -> str:
|
|
516
|
+
"""Parses the tooltip field, defaulting to an empty string."""
|
|
517
|
+
if tooltip is None:
|
|
518
|
+
return ""
|
|
519
|
+
return tooltip
|
|
520
|
+
|
|
521
|
+
def _parse_id(self, binding_id: str | None) -> str | None:
|
|
522
|
+
"""Parses the id field from the YAML binding definition."""
|
|
523
|
+
return binding_id
|
|
524
|
+
|
|
525
|
+
def _parse_system(self, system: object) -> bool:
|
|
526
|
+
"""
|
|
527
|
+
Parses the system field, defaulting to False.
|
|
528
|
+
|
|
529
|
+
A system binding is not overridden by a widget that captures input.
|
|
530
|
+
"""
|
|
531
|
+
return self._parse_bool(system, field="system", default=False)
|
|
532
|
+
|
|
533
|
+
def _parse_bool(self, value: object, field: str, *, default: bool) -> bool:
|
|
534
|
+
"""
|
|
535
|
+
Reads a YAML boolean, falling back to the documented default.
|
|
536
|
+
|
|
537
|
+
Only a genuine boolean counts. `bool(value)` would turn the quoted
|
|
538
|
+
string "false" into True, so a quoted boolean would silently mean the
|
|
539
|
+
opposite of what it says.
|
|
540
|
+
"""
|
|
541
|
+
if value is None:
|
|
542
|
+
return default
|
|
543
|
+
if isinstance(value, bool):
|
|
544
|
+
return value
|
|
545
|
+
|
|
546
|
+
_logger.warning(
|
|
547
|
+
"Expected true or false for %r, got %r; using the default %r",
|
|
548
|
+
field, value, default
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
return default
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Reusable Textual widgets: tables, inputs, selections and the footer."""
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""A Textual Checkbox that shows a check mark instead of an X."""
|
|
2
|
+
|
|
3
|
+
from textual.events import Callback, Mount
|
|
4
|
+
from textual.widgets import Checkbox
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"CustomCheckbox",
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CustomCheckbox(Checkbox):
|
|
12
|
+
"""
|
|
13
|
+
A custom Textual Checkbox that displays a check mark when selected.
|
|
14
|
+
|
|
15
|
+
When nothing is selected, it shows an empty box - instead of the default
|
|
16
|
+
behavior of showing an "X".
|
|
17
|
+
|
|
18
|
+
Attributes
|
|
19
|
+
----------
|
|
20
|
+
CUSTOM_BUTTON_INNER : str
|
|
21
|
+
The character to display when the checkbox is checked.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
CUSTOM_BUTTON_INNER = "✔"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _on_mount(self, event: Mount) -> None:
|
|
28
|
+
"""Applies the custom glyph as soon as the widget is mounted."""
|
|
29
|
+
self.toggle_button_inner()
|
|
30
|
+
return super()._on_mount(event)
|
|
31
|
+
|
|
32
|
+
def on_checkbox_changed(self, _event: Callback) -> None:
|
|
33
|
+
"""Swaps the glyph whenever the checkbox is toggled."""
|
|
34
|
+
self.toggle_button_inner()
|
|
35
|
+
|
|
36
|
+
def toggle_button_inner(self) -> None:
|
|
37
|
+
"""Sets the inner glyph to match the current value."""
|
|
38
|
+
if self.value:
|
|
39
|
+
self.BUTTON_INNER = self.CUSTOM_BUTTON_INNER
|
|
40
|
+
else:
|
|
41
|
+
self.BUTTON_INNER = " " # pyright:ignore[reportConstantRedefinition]
|