backpack-backbone 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.
- backbone/__init__.py +30 -0
- backbone/app.py +26 -0
- backbone/datetime_parse.py +224 -0
- backbone/deps.py +98 -0
- backbone/files.py +61 -0
- backbone/keyboard.py +148 -0
- backbone/keys.py +271 -0
- backbone/log.py +61 -0
- backbone/nav.py +17 -0
- backbone/notify.py +36 -0
- backbone/numbering.py +182 -0
- backbone/output.py +216 -0
- backbone/procs.py +60 -0
- backbone/prompt/__init__.py +20 -0
- backbone/prompt/audio.py +496 -0
- backbone/prompt/chrome.py +238 -0
- backbone/prompt/core.py +1562 -0
- backbone/prompt/dates.py +555 -0
- backbone/prompt/keymap.py +143 -0
- backbone/prompt/list_edit.py +974 -0
- backbone/prompt/lists.py +1280 -0
- backbone/prompt/text.py +356 -0
- backbone/prompt/timezone.py +1543 -0
- backbone/prompt/values.py +581 -0
- backbone/terminal_input.py +60 -0
- backbone/timefmt.py +34 -0
- backbone/ui.py +1044 -0
- backpack_backbone-0.2.0.dist-info/METADATA +177 -0
- backpack_backbone-0.2.0.dist-info/RECORD +32 -0
- backpack_backbone-0.2.0.dist-info/WHEEL +5 -0
- backpack_backbone-0.2.0.dist-info/licenses/LICENSE +21 -0
- backpack_backbone-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""What every prompt widget shares: the hint bar and help corner, the global
|
|
2
|
+
player and transport keys, mouse reporting, and the per-edit raw-text toggle."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
import sys
|
|
5
|
+
from backbone.prompt.core import (
|
|
6
|
+
_IS_WINDOWS, _hint, add_hint_click_cells, footer_click_action, _hint_pin_target,
|
|
7
|
+
screen_invalidate, HINTS_CLICK, is_hints_key, toggle_hints, place_help_toggle,
|
|
8
|
+
)
|
|
9
|
+
from backbone import keys, ui
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# Live on every screen: the background-audio transport (routed through
|
|
13
|
+
# registered callbacks so prompt need not import the playback layer), reopening
|
|
14
|
+
# the player, and the per-edit raw-text toggle.
|
|
15
|
+
keys.define("global", "Everywhere", [
|
|
16
|
+
("playpause", ("\x10",), "play / pause"),
|
|
17
|
+
("next", ("\x0e",), "next track"),
|
|
18
|
+
("prev", ("\x02",), "previous track"),
|
|
19
|
+
("player", ("\x0f",), "open the player"),
|
|
20
|
+
("raw_text", ("\x14",), "switch a value between its editor and raw text"),
|
|
21
|
+
], within=())
|
|
22
|
+
|
|
23
|
+
# The one key pair that moves a row up or down, wherever a list's order can be
|
|
24
|
+
# changed (select's on_move, list_edit).
|
|
25
|
+
keys.define("list", "Lists", [
|
|
26
|
+
("move_up", ("J",), "move the row up"),
|
|
27
|
+
("move_down", ("K",), "move the row down"),
|
|
28
|
+
])
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def move_hint() -> tuple:
|
|
32
|
+
return (keys.label("list.move_up", "list.move_down"), "move up/down")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# Per-edit "raw text ↔ smart widget" toggle. prompt_for_value enables the
|
|
36
|
+
# flag around a value edit; the value widgets then treat Ctrl-T as a request to
|
|
37
|
+
# switch modes by returning MODE_TOGGLE, and advertise it in their hint bar.
|
|
38
|
+
MODE_TOGGLE = object()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _with_toggle_hint(pairs, label: str = 'raw text'):
|
|
42
|
+
"""Append the Ctrl-T hint to a widget's hint bar while the per-edit raw-text
|
|
43
|
+
toggle is live.
|
|
44
|
+
|
|
45
|
+
Every widget that *accepts* ^t advertises it through this, so the key is never
|
|
46
|
+
silently available on one screen and absent from the bar on another.
|
|
47
|
+
"""
|
|
48
|
+
return list(pairs) + [(keys.label("global.raw_text"), label)] if _value_toggle_enabled else list(pairs)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def is_mode_toggle(key: str) -> bool:
|
|
52
|
+
"""Whether `key` asks a value editor to switch to raw text (or back)."""
|
|
53
|
+
return _value_toggle_enabled and keys.pressed(key, "global.raw_text")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
_value_toggle_enabled = False
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
_toggle_hint_label = 'widget' # what text()'s ^t hint calls the alternate mode
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
_toggle_carry: str | None = None # in-progress text buffer handed across a Ctrl-T toggle
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
_player_opener = None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
_transport_handler = None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def set_player_opener(fn) -> None:
|
|
72
|
+
"""Register a ``callable()`` that opens the background player's full view."""
|
|
73
|
+
global _player_opener
|
|
74
|
+
_player_opener = fn
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# --- shared widget chrome -------------------------------------------------
|
|
78
|
+
# Every screen owes the user the same four things: a hint bar pinned above the
|
|
79
|
+
# now-playing box and status bar so its keys never move, those keys clickable, the
|
|
80
|
+
# background-audio transport keys listed whenever the now-playing box is up, and
|
|
81
|
+
# clicks on the now-playing box itself doing something. These two helpers are
|
|
82
|
+
# that contract in one place.
|
|
83
|
+
|
|
84
|
+
CHROME_HANDLED = object() # the key was consumed; carry on with the loop
|
|
85
|
+
CHROME_REDRAW = object() # consumed, and the caller should repaint fully
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def chrome_hint_pairs(pairs) -> list:
|
|
89
|
+
"""A widget's hint pairs plus the transport keys, while audio is playing.
|
|
90
|
+
|
|
91
|
+
Only keys that will actually do something are advertised: the transport trio
|
|
92
|
+
needs a handler installed and ^O needs a player to reopen. `unboxed` covers
|
|
93
|
+
a terminal too narrow to draw the now-playing box: the keys are still live,
|
|
94
|
+
so they are still listed.
|
|
95
|
+
"""
|
|
96
|
+
items = list(pairs.items()) if isinstance(pairs, dict) else [tuple(p) for p in pairs]
|
|
97
|
+
if ui.footer_active() or ui.footer_unboxed():
|
|
98
|
+
if _transport_handler is not None:
|
|
99
|
+
items += [(keys.label("global.playpause"), "play/pause"),
|
|
100
|
+
(keys.label("global.next", "global.prev"), "next/prev")]
|
|
101
|
+
if _player_opener is not None:
|
|
102
|
+
items += [(keys.label("global.player"), "player")]
|
|
103
|
+
return items
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def chrome_hint_lines(pairs, *, extra: str = "") -> list:
|
|
107
|
+
"""The hint bar as rendered lines: widgets that size a viewport need the
|
|
108
|
+
row count before they lay their content out."""
|
|
109
|
+
return _hint(*chrome_hint_pairs(pairs), extra=extra).splitlines()
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def append_chrome(out: list, pairs, cells: dict, *, extra: str = "",
|
|
113
|
+
pin: bool = True, help_key: bool = False) -> list:
|
|
114
|
+
"""Append the hint bar to a widget's rendered `out` lines, in place.
|
|
115
|
+
|
|
116
|
+
Pads down to :func:`_hint_pin_target` so the bar sits just above the
|
|
117
|
+
now-playing box and status bar and its keys keep the same screen position across
|
|
118
|
+
redraws; otherwise a repeated click chases the bar as the content changes
|
|
119
|
+
height. Records each bright key's screen cell in `cells` for
|
|
120
|
+
:func:`consume_chrome` to look up.
|
|
121
|
+
|
|
122
|
+
The bar is empty unless hints are switched on; either way the top line
|
|
123
|
+
(`out[0]`) carries the corner toggle. `help_key`: this screen leaves `?` free,
|
|
124
|
+
so `?` toggles and the corner says so; otherwise it names Ctrl-/.
|
|
125
|
+
"""
|
|
126
|
+
items = chrome_hint_pairs(pairs)
|
|
127
|
+
hint_lines = _hint(*items, extra=extra).splitlines()
|
|
128
|
+
if pin:
|
|
129
|
+
filler = _hint_pin_target() - len(out) - len(hint_lines)
|
|
130
|
+
if filler > 0:
|
|
131
|
+
out.extend([""] * filler)
|
|
132
|
+
out.extend(f"{' ' * ui.MARGIN_H}{h}" for h in hint_lines)
|
|
133
|
+
|
|
134
|
+
cells.clear()
|
|
135
|
+
if hint_lines:
|
|
136
|
+
start = len(out) - len(hint_lines)
|
|
137
|
+
for k in range(len(hint_lines)):
|
|
138
|
+
# `_Widget.render` lays line j at terminal row anchor(1) + MARGIN_V + j.
|
|
139
|
+
add_hint_click_cells(cells, out[start + k],
|
|
140
|
+
1 + ui.MARGIN_V + (start + k), items)
|
|
141
|
+
place_help_toggle(out, 1 + ui.MARGIN_V, cells, help_key)
|
|
142
|
+
return out
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def consume_chrome(key: str, cells: dict):
|
|
146
|
+
"""Handle a transport key, a now-playing box click, or a click on a hint key.
|
|
147
|
+
|
|
148
|
+
Returns :data:`CHROME_HANDLED` when the key is fully dealt with,
|
|
149
|
+
:data:`CHROME_REDRAW` when the caller should also repaint, the synthesised
|
|
150
|
+
key string when a hint was clicked (replay it through the widget's own
|
|
151
|
+
switch), or None when the key is not ours.
|
|
152
|
+
"""
|
|
153
|
+
if key == 'FOCUS_IN':
|
|
154
|
+
# Back in focus: the terminal may not have painted us meanwhile (or a
|
|
155
|
+
# background track change went by), so repaint everything.
|
|
156
|
+
screen_invalidate()
|
|
157
|
+
return CHROME_REDRAW
|
|
158
|
+
if is_hints_key(key, bool(cells.get('__help_key__'))):
|
|
159
|
+
toggle_hints()
|
|
160
|
+
return CHROME_REDRAW # the bar appeared or went: re-lay the screen
|
|
161
|
+
act = keys.action(key, "global") if isinstance(key, str) else None
|
|
162
|
+
if act == "global.player" and _player_opener is not None:
|
|
163
|
+
_player_opener()
|
|
164
|
+
if not _IS_WINDOWS:
|
|
165
|
+
sys.stdout.write("\033[?1000h\033[?1006h") # the player took the mouse
|
|
166
|
+
sys.stdout.flush()
|
|
167
|
+
return CHROME_REDRAW
|
|
168
|
+
if act in ("global.playpause", "global.next", "global.prev") and _transport_handler is not None:
|
|
169
|
+
_transport_handler(act.split(".")[1])
|
|
170
|
+
return CHROME_HANDLED
|
|
171
|
+
|
|
172
|
+
if isinstance(key, str) and key.startswith('MOUSE_CLICK:'):
|
|
173
|
+
parts = key.split(':')
|
|
174
|
+
try:
|
|
175
|
+
row = int(parts[2])
|
|
176
|
+
col = int(parts[3]) if len(parts) > 3 else 1
|
|
177
|
+
except (IndexError, ValueError):
|
|
178
|
+
return None
|
|
179
|
+
act = footer_click_action(row, col)
|
|
180
|
+
if act == 'open' and _player_opener is not None:
|
|
181
|
+
_player_opener()
|
|
182
|
+
if not _IS_WINDOWS:
|
|
183
|
+
sys.stdout.write("\033[?1000h\033[?1006h")
|
|
184
|
+
sys.stdout.flush()
|
|
185
|
+
return CHROME_REDRAW
|
|
186
|
+
if act in ('playpause', 'next', 'prev') and _transport_handler is not None:
|
|
187
|
+
_transport_handler(act)
|
|
188
|
+
return CHROME_HANDLED
|
|
189
|
+
hit = cells.get((row, col))
|
|
190
|
+
if hit == HINTS_CLICK or keys.action(hit or "", "global") in (
|
|
191
|
+
"global.player", "global.playpause", "global.next", "global.prev"):
|
|
192
|
+
return consume_chrome(hit, cells) # a transport hint: act on it here
|
|
193
|
+
if hit is not None:
|
|
194
|
+
return hit # replay the clicked hint's key
|
|
195
|
+
return None
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def enable_mouse() -> None:
|
|
199
|
+
"""Turn on click + scroll reporting for a widget that wants clickable hints."""
|
|
200
|
+
if not _IS_WINDOWS:
|
|
201
|
+
sys.stdout.write("\033[?1000h\033[?1006h")
|
|
202
|
+
sys.stdout.flush()
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def disable_mouse() -> None:
|
|
206
|
+
"""Turn click reporting back off on the way out."""
|
|
207
|
+
if not _IS_WINDOWS:
|
|
208
|
+
sys.stdout.write("\033[?1000l\033[?1006l")
|
|
209
|
+
sys.stdout.flush()
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def set_transport_handler(fn) -> None:
|
|
213
|
+
"""Register ``callable(action)`` for the global transport hotkeys, where
|
|
214
|
+
action is 'playpause', 'next', or 'prev'. Kept as a registered callback so
|
|
215
|
+
prompt need not import the playback layer (mirrors set_player_opener)."""
|
|
216
|
+
global _transport_handler
|
|
217
|
+
_transport_handler = fn
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
_activity_opener = None
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def set_activity_opener(fn) -> None:
|
|
224
|
+
"""Register a ``callable()`` that opens the activity centre,
|
|
225
|
+
invoked when the status-bar ● beacon is clicked."""
|
|
226
|
+
global _activity_opener
|
|
227
|
+
_activity_opener = fn
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _plain(s: str) -> str:
|
|
231
|
+
"""The row's printed characters, one entry per *terminal column*.
|
|
232
|
+
|
|
233
|
+
A two-cell glyph is repeated so that an index into the result is the
|
|
234
|
+
column it sits in, which is what a click hit-test assumes when it asks
|
|
235
|
+
whether column `col` holds a character or blank padding. Shared by
|
|
236
|
+
`select` and `live_select`, so the two widgets' click behaviour can't drift.
|
|
237
|
+
"""
|
|
238
|
+
return "".join(ch * ui.char_cols(ch) for ch in ui.display_text(s))
|