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.
@@ -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))