backpack-backbone 0.3.2__tar.gz → 0.4.0__tar.gz
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.
- {backpack_backbone-0.3.2/backpack_backbone.egg-info → backpack_backbone-0.4.0}/PKG-INFO +13 -13
- {backpack_backbone-0.3.2 → backpack_backbone-0.4.0}/README.md +12 -12
- {backpack_backbone-0.3.2 → backpack_backbone-0.4.0}/backbone/__init__.py +0 -10
- {backpack_backbone-0.3.2 → backpack_backbone-0.4.0}/backbone/prompt/chrome.py +14 -4
- {backpack_backbone-0.3.2 → backpack_backbone-0.4.0}/backbone/prompt/core.py +1 -1
- {backpack_backbone-0.3.2 → backpack_backbone-0.4.0}/backbone/prompt/keymap.py +13 -8
- {backpack_backbone-0.3.2 → backpack_backbone-0.4.0}/backbone/ui.py +4 -113
- {backpack_backbone-0.3.2 → backpack_backbone-0.4.0/backpack_backbone.egg-info}/PKG-INFO +13 -13
- {backpack_backbone-0.3.2 → backpack_backbone-0.4.0}/pyproject.toml +1 -1
- {backpack_backbone-0.3.2 → backpack_backbone-0.4.0}/tests/test_keys.py +29 -0
- {backpack_backbone-0.3.2 → backpack_backbone-0.4.0}/LICENSE +0 -0
- {backpack_backbone-0.3.2 → backpack_backbone-0.4.0}/backbone/app.py +0 -0
- {backpack_backbone-0.3.2 → backpack_backbone-0.4.0}/backbone/datetime_parse.py +0 -0
- {backpack_backbone-0.3.2 → backpack_backbone-0.4.0}/backbone/deps.py +0 -0
- {backpack_backbone-0.3.2 → backpack_backbone-0.4.0}/backbone/files.py +0 -0
- {backpack_backbone-0.3.2 → backpack_backbone-0.4.0}/backbone/keyboard.py +0 -0
- {backpack_backbone-0.3.2 → backpack_backbone-0.4.0}/backbone/keys.py +0 -0
- {backpack_backbone-0.3.2 → backpack_backbone-0.4.0}/backbone/log.py +0 -0
- {backpack_backbone-0.3.2 → backpack_backbone-0.4.0}/backbone/nav.py +0 -0
- {backpack_backbone-0.3.2 → backpack_backbone-0.4.0}/backbone/notify.py +0 -0
- {backpack_backbone-0.3.2 → backpack_backbone-0.4.0}/backbone/numbering.py +0 -0
- {backpack_backbone-0.3.2 → backpack_backbone-0.4.0}/backbone/output.py +0 -0
- {backpack_backbone-0.3.2 → backpack_backbone-0.4.0}/backbone/procs.py +0 -0
- {backpack_backbone-0.3.2 → backpack_backbone-0.4.0}/backbone/prompt/__init__.py +0 -0
- {backpack_backbone-0.3.2 → backpack_backbone-0.4.0}/backbone/prompt/audio.py +0 -0
- {backpack_backbone-0.3.2 → backpack_backbone-0.4.0}/backbone/prompt/dates.py +0 -0
- {backpack_backbone-0.3.2 → backpack_backbone-0.4.0}/backbone/prompt/list_edit.py +0 -0
- {backpack_backbone-0.3.2 → backpack_backbone-0.4.0}/backbone/prompt/lists.py +0 -0
- {backpack_backbone-0.3.2 → backpack_backbone-0.4.0}/backbone/prompt/settings.py +0 -0
- {backpack_backbone-0.3.2 → backpack_backbone-0.4.0}/backbone/prompt/text.py +0 -0
- {backpack_backbone-0.3.2 → backpack_backbone-0.4.0}/backbone/prompt/timezone.py +0 -0
- {backpack_backbone-0.3.2 → backpack_backbone-0.4.0}/backbone/prompt/values.py +0 -0
- {backpack_backbone-0.3.2 → backpack_backbone-0.4.0}/backbone/terminal_input.py +0 -0
- {backpack_backbone-0.3.2 → backpack_backbone-0.4.0}/backbone/timefmt.py +0 -0
- {backpack_backbone-0.3.2 → backpack_backbone-0.4.0}/backpack_backbone.egg-info/SOURCES.txt +0 -0
- {backpack_backbone-0.3.2 → backpack_backbone-0.4.0}/backpack_backbone.egg-info/dependency_links.txt +0 -0
- {backpack_backbone-0.3.2 → backpack_backbone-0.4.0}/backpack_backbone.egg-info/top_level.txt +0 -0
- {backpack_backbone-0.3.2 → backpack_backbone-0.4.0}/setup.cfg +0 -0
- {backpack_backbone-0.3.2 → backpack_backbone-0.4.0}/tests/test_boxes.py +0 -0
- {backpack_backbone-0.3.2 → backpack_backbone-0.4.0}/tests/test_datetime_parse.py +0 -0
- {backpack_backbone-0.3.2 → backpack_backbone-0.4.0}/tests/test_deps.py +0 -0
- {backpack_backbone-0.3.2 → backpack_backbone-0.4.0}/tests/test_edit_line.py +0 -0
- {backpack_backbone-0.3.2 → backpack_backbone-0.4.0}/tests/test_hint_clicks.py +0 -0
- {backpack_backbone-0.3.2 → backpack_backbone-0.4.0}/tests/test_list_edit_render.py +0 -0
- {backpack_backbone-0.3.2 → backpack_backbone-0.4.0}/tests/test_log.py +0 -0
- {backpack_backbone-0.3.2 → backpack_backbone-0.4.0}/tests/test_multiline.py +0 -0
- {backpack_backbone-0.3.2 → backpack_backbone-0.4.0}/tests/test_overlay.py +0 -0
- {backpack_backbone-0.3.2 → backpack_backbone-0.4.0}/tests/test_procs.py +0 -0
- {backpack_backbone-0.3.2 → backpack_backbone-0.4.0}/tests/test_quietly.py +0 -0
- {backpack_backbone-0.3.2 → backpack_backbone-0.4.0}/tests/test_select_move.py +0 -0
- {backpack_backbone-0.3.2 → backpack_backbone-0.4.0}/tests/test_tabs.py +0 -0
- {backpack_backbone-0.3.2 → backpack_backbone-0.4.0}/tests/test_terminal_input.py +0 -0
- {backpack_backbone-0.3.2 → backpack_backbone-0.4.0}/tests/test_ui_progress.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: backpack-backbone
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Shared code for the back* tools: terminal UI, prompt widgets, live views, logging, dates, settings plumbing and small file helpers, implemented once.
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/RedEraRrow/backbone
|
|
@@ -50,9 +50,9 @@ how to install anything it can't run without.
|
|
|
50
50
|
## What's in it
|
|
51
51
|
|
|
52
52
|
**`ui`** - the visual layer. `Colors` (honours `NO_COLOR=1`), the global
|
|
53
|
-
`MARGIN_H` / `MARGIN_V` inset every frame is drawn inside,
|
|
54
|
-
|
|
55
|
-
the `SPIN` / `
|
|
53
|
+
`MARGIN_H` / `MARGIN_V` inset every frame is drawn inside, the progress
|
|
54
|
+
bar (`progress_cells`, `get_progress_bar`), `sparkline`, `spinner`,
|
|
55
|
+
`rate_of_change`, and the `SPIN` / `SPARK` glyph sets. Also the ANSI-aware text
|
|
56
56
|
measuring (`visual_len`, `truncate_text`, `clip_ansi`, `strip_ansi`) that
|
|
57
57
|
makes any of that survive colour codes and wide characters, and small
|
|
58
58
|
formatters: `plural`, `human_gb`, `dir_size_kb`. The accent colour is chosen
|
|
@@ -60,16 +60,9 @@ with `set_accent` (an `ACCENT_PRESETS` key or `#RRGGBB`); a tool calls it once
|
|
|
60
60
|
at startup from its own settings, and `accent_code` / `accent_label` let a
|
|
61
61
|
settings screen check and name a value.
|
|
62
62
|
|
|
63
|
-
**`prompt.settings`** - what a Settings screen is built from, so every
|
|
64
|
-
tool's reads the same: `SETTINGS_COLUMNS` (a name, then its state),
|
|
65
|
-
`state_glyph` (● / ○), `space_toggles` (space flips the on/off rows),
|
|
66
|
-
`index_of` (the cursor back on the row just changed), `pick_option` (one of
|
|
67
|
-
a fixed set of values) and `pick_accent` (the accent colour picker, each
|
|
68
|
-
colour shown in itself).
|
|
69
|
-
|
|
70
63
|
`from backbone import ...` exposes only a subset of `ui` (`Colors`, the
|
|
71
|
-
margins and glyph sets, `spinner`, `content_width
|
|
72
|
-
|
|
64
|
+
margins and glyph sets, `spinner`, `content_width`); import anything else
|
|
65
|
+
from its module, e.g.
|
|
73
66
|
`from backbone.ui import human_gb`.
|
|
74
67
|
|
|
75
68
|
**`prompt`** - the widgets. `select` (single or multi), `confirm`, `text`,
|
|
@@ -79,6 +72,13 @@ values: `calendar_select`, `datetime_edit`, `time_edit`,
|
|
|
79
72
|
`system_editor_edit`. All resize-aware, all mouse-aware, all rendered through
|
|
80
73
|
the same painter.
|
|
81
74
|
|
|
75
|
+
**`prompt.settings`** - what a Settings screen is built from, so every
|
|
76
|
+
tool's reads the same: `SETTINGS_COLUMNS` (a name, then its state),
|
|
77
|
+
`state_glyph` (● / ○), `space_toggles` (space flips the on/off rows),
|
|
78
|
+
`index_of` (the cursor back on the row just changed), `pick_option` (one of
|
|
79
|
+
a fixed set of values) and `pick_accent` (the accent colour picker, each
|
|
80
|
+
colour shown in itself).
|
|
81
|
+
|
|
82
82
|
**`prompt.core`** - the primitives underneath: the screen-diff painter, key
|
|
83
83
|
reading, the `Choice` and `Column` types, the footer hint bar (`hint`) and its
|
|
84
84
|
click mapping, and `run_dashboard`.
|
|
@@ -32,9 +32,9 @@ how to install anything it can't run without.
|
|
|
32
32
|
## What's in it
|
|
33
33
|
|
|
34
34
|
**`ui`** - the visual layer. `Colors` (honours `NO_COLOR=1`), the global
|
|
35
|
-
`MARGIN_H` / `MARGIN_V` inset every frame is drawn inside,
|
|
36
|
-
|
|
37
|
-
the `SPIN` / `
|
|
35
|
+
`MARGIN_H` / `MARGIN_V` inset every frame is drawn inside, the progress
|
|
36
|
+
bar (`progress_cells`, `get_progress_bar`), `sparkline`, `spinner`,
|
|
37
|
+
`rate_of_change`, and the `SPIN` / `SPARK` glyph sets. Also the ANSI-aware text
|
|
38
38
|
measuring (`visual_len`, `truncate_text`, `clip_ansi`, `strip_ansi`) that
|
|
39
39
|
makes any of that survive colour codes and wide characters, and small
|
|
40
40
|
formatters: `plural`, `human_gb`, `dir_size_kb`. The accent colour is chosen
|
|
@@ -42,16 +42,9 @@ with `set_accent` (an `ACCENT_PRESETS` key or `#RRGGBB`); a tool calls it once
|
|
|
42
42
|
at startup from its own settings, and `accent_code` / `accent_label` let a
|
|
43
43
|
settings screen check and name a value.
|
|
44
44
|
|
|
45
|
-
**`prompt.settings`** - what a Settings screen is built from, so every
|
|
46
|
-
tool's reads the same: `SETTINGS_COLUMNS` (a name, then its state),
|
|
47
|
-
`state_glyph` (● / ○), `space_toggles` (space flips the on/off rows),
|
|
48
|
-
`index_of` (the cursor back on the row just changed), `pick_option` (one of
|
|
49
|
-
a fixed set of values) and `pick_accent` (the accent colour picker, each
|
|
50
|
-
colour shown in itself).
|
|
51
|
-
|
|
52
45
|
`from backbone import ...` exposes only a subset of `ui` (`Colors`, the
|
|
53
|
-
margins and glyph sets, `spinner`, `content_width
|
|
54
|
-
|
|
46
|
+
margins and glyph sets, `spinner`, `content_width`); import anything else
|
|
47
|
+
from its module, e.g.
|
|
55
48
|
`from backbone.ui import human_gb`.
|
|
56
49
|
|
|
57
50
|
**`prompt`** - the widgets. `select` (single or multi), `confirm`, `text`,
|
|
@@ -61,6 +54,13 @@ values: `calendar_select`, `datetime_edit`, `time_edit`,
|
|
|
61
54
|
`system_editor_edit`. All resize-aware, all mouse-aware, all rendered through
|
|
62
55
|
the same painter.
|
|
63
56
|
|
|
57
|
+
**`prompt.settings`** - what a Settings screen is built from, so every
|
|
58
|
+
tool's reads the same: `SETTINGS_COLUMNS` (a name, then its state),
|
|
59
|
+
`state_glyph` (● / ○), `space_toggles` (space flips the on/off rows),
|
|
60
|
+
`index_of` (the cursor back on the row just changed), `pick_option` (one of
|
|
61
|
+
a fixed set of values) and `pick_accent` (the accent colour picker, each
|
|
62
|
+
colour shown in itself).
|
|
63
|
+
|
|
64
64
|
**`prompt.core`** - the primitives underneath: the screen-diff painter, key
|
|
65
65
|
reading, the `Choice` and `Column` types, the footer hint bar (`hint`) and its
|
|
66
66
|
click mapping, and `run_dashboard`.
|
|
@@ -4,14 +4,9 @@ from .ui import (
|
|
|
4
4
|
MARGIN_H,
|
|
5
5
|
MARGIN_V,
|
|
6
6
|
SPIN,
|
|
7
|
-
PARTS,
|
|
8
7
|
SPARK,
|
|
9
8
|
spinner,
|
|
10
9
|
content_width,
|
|
11
|
-
rule,
|
|
12
|
-
bar,
|
|
13
|
-
header_box,
|
|
14
|
-
wrap_margins,
|
|
15
10
|
)
|
|
16
11
|
|
|
17
12
|
__all__ = [
|
|
@@ -19,12 +14,7 @@ __all__ = [
|
|
|
19
14
|
"MARGIN_H",
|
|
20
15
|
"MARGIN_V",
|
|
21
16
|
"SPIN",
|
|
22
|
-
"PARTS",
|
|
23
17
|
"SPARK",
|
|
24
18
|
"spinner",
|
|
25
19
|
"content_width",
|
|
26
|
-
"rule",
|
|
27
|
-
"bar",
|
|
28
|
-
"header_box",
|
|
29
|
-
"wrap_margins",
|
|
30
20
|
]
|
|
@@ -115,20 +115,30 @@ CHROME_HANDLED = object() # the key was consumed; carry on with the loop
|
|
|
115
115
|
CHROME_REDRAW = object() # consumed, and the caller should repaint fully
|
|
116
116
|
|
|
117
117
|
|
|
118
|
+
def has_owner(action_id: str) -> bool:
|
|
119
|
+
"""Whether an action does anything in this app: the transport and volume
|
|
120
|
+
keys need a transport handler installed, ^O a player to reopen. Every
|
|
121
|
+
other action belongs to a screen, so it does."""
|
|
122
|
+
if action_id in ("global.playpause", "global.next", "global.prev") or action_id.startswith("volume."):
|
|
123
|
+
return _transport_handler is not None
|
|
124
|
+
if action_id == "global.player":
|
|
125
|
+
return _player_opener is not None
|
|
126
|
+
return True
|
|
127
|
+
|
|
128
|
+
|
|
118
129
|
def chrome_hint_pairs(pairs) -> list:
|
|
119
130
|
"""A widget's hint pairs plus the transport keys, while audio is playing.
|
|
120
131
|
|
|
121
|
-
Only keys that will actually do something are advertised
|
|
122
|
-
needs a handler installed and ^O needs a player to reopen. `unboxed` covers
|
|
132
|
+
Only keys that will actually do something are advertised (has_owner). `unboxed` covers
|
|
123
133
|
a terminal too narrow to draw the now-playing box: the keys are still live,
|
|
124
134
|
so they are still listed.
|
|
125
135
|
"""
|
|
126
136
|
items = list(pairs.items()) if isinstance(pairs, dict) else [tuple(p) for p in pairs]
|
|
127
137
|
if ui.footer_active() or ui.footer_unboxed():
|
|
128
|
-
if
|
|
138
|
+
if has_owner("global.playpause"):
|
|
129
139
|
items += [(keys.label("global.playpause"), "play/pause"),
|
|
130
140
|
(keys.label("global.next", "global.prev"), "next/prev")]
|
|
131
|
-
if
|
|
141
|
+
if has_owner("global.player"):
|
|
132
142
|
items += [(keys.label("global.player"), "player")]
|
|
133
143
|
return items
|
|
134
144
|
|
|
@@ -2499,7 +2499,7 @@ def run_dashboard(render, interval: float = 1.0, quit_action: str = "list.quit",
|
|
|
2499
2499
|
`quit_action` binding (see backbone.keys) is pressed.
|
|
2500
2500
|
|
|
2501
2501
|
`render()` takes no arguments and returns the whole frame as a list of
|
|
2502
|
-
lines, each carrying its own left-margin indent (
|
|
2502
|
+
lines, each carrying its own left-margin indent (box_lines() adds it). It
|
|
2503
2503
|
runs once every `interval` seconds; keypresses and resizes are checked
|
|
2504
2504
|
every `poll` seconds regardless, and a resize renders at once.
|
|
2505
2505
|
|
|
@@ -30,13 +30,17 @@ def _row(a: keys.Action) -> Choice:
|
|
|
30
30
|
"changed" if changed else ""])
|
|
31
31
|
|
|
32
32
|
|
|
33
|
-
def _choices() -> list:
|
|
33
|
+
def _choices(scopes) -> list:
|
|
34
|
+
"""Every action this app has a use for, by its screen: only `scopes`
|
|
35
|
+
(all, when None), and no transport or volume keys without a player."""
|
|
36
|
+
from backbone.prompt.chrome import has_owner
|
|
34
37
|
out: list = []
|
|
35
38
|
for scope, title in keys.scopes():
|
|
36
|
-
if scope == "keys_editor":
|
|
37
|
-
continue # listed last, below
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
if scope == "keys_editor" or (scopes is not None and scope not in scopes):
|
|
40
|
+
continue # this page's own are listed last, below
|
|
41
|
+
rows = [_row(a) for a in keys.actions(scope) if has_owner(a.id)]
|
|
42
|
+
if rows:
|
|
43
|
+
out += [separator(title), *rows]
|
|
40
44
|
out.append(separator("This page"))
|
|
41
45
|
out += [_row(a) for a in keys.actions("keys_editor")]
|
|
42
46
|
return out
|
|
@@ -102,8 +106,9 @@ def _remove_key(aid: str) -> None:
|
|
|
102
106
|
ui.show_status(f"{keys.glyph(key)} removed" + ("" if len(bound) > 1 else ": the action has no key now"))
|
|
103
107
|
|
|
104
108
|
|
|
105
|
-
def keys_editor() -> None:
|
|
106
|
-
"""The Key bindings page
|
|
109
|
+
def keys_editor(scopes: list | None = None) -> None:
|
|
110
|
+
"""The Key bindings page, for the key groups (keys.define scopes) in
|
|
111
|
+
`scopes`, or every one defined. Returns when backed out of."""
|
|
107
112
|
from backbone.prompt.lists import ListPlace, select
|
|
108
113
|
place = ListPlace()
|
|
109
114
|
while True:
|
|
@@ -124,7 +129,7 @@ def keys_editor() -> None:
|
|
|
124
129
|
hints[keys.label(aid)] = {"remove": "remove a key", "reset": "reset",
|
|
125
130
|
"reset_all": "reset all"}[name]
|
|
126
131
|
|
|
127
|
-
choice = select("", _choices(), columns=_COLUMNS, place=place,
|
|
132
|
+
choice = select("", _choices(scopes), columns=_COLUMNS, place=place,
|
|
128
133
|
header=lambda: rounded_header("Key bindings", "",
|
|
129
134
|
f"{changed} changed" if changed else "all default"),
|
|
130
135
|
extra_hints={"↵": "add a key"},
|
|
@@ -103,8 +103,7 @@ MARGIN_V = 1 # rows reserved on each vertical side (top and bottom)
|
|
|
103
103
|
FOOTER_GLYPH_COLS = ((0, 2), (4, 2))
|
|
104
104
|
|
|
105
105
|
class Colors:
|
|
106
|
-
"""The named palette every widget uses,
|
|
107
|
-
own views (FRAME, TEAL, AMBER, RED, TXT, MUTE) and the short aliases R and B.
|
|
106
|
+
"""The named palette every widget uses, and RED for what failed.
|
|
108
107
|
All empty when colour is off (NO_COLOR, or set_colour(False))."""
|
|
109
108
|
PRIMARY = "\033[1;37m" # Bold white
|
|
110
109
|
WHITE = "\033[37m" # Normal white
|
|
@@ -127,23 +126,14 @@ class Colors:
|
|
|
127
126
|
BAR_DIM = "\033[48;5;235m"
|
|
128
127
|
HIDE = "\033[?25l"
|
|
129
128
|
SHOW = "\033[?25h"
|
|
130
|
-
#
|
|
131
|
-
FRAME = "\033[38;5;239m"
|
|
132
|
-
TEAL = "\033[38;5;43m"
|
|
133
|
-
AMBER = "\033[38;5;179m"
|
|
134
|
-
RED = "\033[38;5;167m"
|
|
135
|
-
TXT = "\033[38;5;252m"
|
|
136
|
-
MUTE = "\033[38;5;243m"
|
|
137
|
-
R = RESET # short aliases
|
|
138
|
-
B = BOLD
|
|
129
|
+
RED = "\033[38;5;167m" # a failure: a text field's problem, backcrack's FAIL lines
|
|
139
130
|
|
|
140
131
|
|
|
141
132
|
# The styling half of Colors: everything that paints rather than moves the
|
|
142
133
|
# cursor. Suppressing colour must not suppress HIDE/SHOW, which are cursor
|
|
143
134
|
# control and still needed on a pipe.
|
|
144
135
|
_STYLE_NAMES = ('PRIMARY', 'WHITE', 'ACCENT', 'ACCENT2', 'CYAN', 'YELLOW', 'MAGENTA', 'GREEN',
|
|
145
|
-
'DIM', 'BOLD', 'ITALIC', 'UNDERLINE', 'RESET', 'BACK', 'INVERT', 'BAR', 'BAR_DIM',
|
|
146
|
-
'FRAME', 'TEAL', 'AMBER', 'RED', 'TXT', 'MUTE', 'R', 'B')
|
|
136
|
+
'DIM', 'BOLD', 'ITALIC', 'UNDERLINE', 'RESET', 'BACK', 'INVERT', 'BAR', 'BAR_DIM', 'RED')
|
|
147
137
|
_STYLE_CODES = {name: getattr(Colors, name) for name in _STYLE_NAMES}
|
|
148
138
|
|
|
149
139
|
|
|
@@ -1128,8 +1118,6 @@ C = Colors # the short name every widget uses
|
|
|
1128
1118
|
|
|
1129
1119
|
SPIN = list("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏")
|
|
1130
1120
|
|
|
1131
|
-
PARTS = ["", "▏", "▎", "▍", "▌", "▋", "▊", "▉"] # eighth-block fill steps
|
|
1132
|
-
|
|
1133
1121
|
SPARK = "▁▂▃▄▅▆▇█"
|
|
1134
1122
|
|
|
1135
1123
|
def content_width(min_width: int = 1) -> int:
|
|
@@ -1140,21 +1128,6 @@ def content_width(min_width: int = 1) -> int:
|
|
|
1140
1128
|
"""
|
|
1141
1129
|
return max(min_width, get_terminal_width() - 2 * MARGIN_H)
|
|
1142
1130
|
|
|
1143
|
-
def rule(n: int) -> str:
|
|
1144
|
-
return "─" * n
|
|
1145
|
-
|
|
1146
|
-
def bar(pct: int, width: int, color: str = "") -> str:
|
|
1147
|
-
color = color or Colors.TEAL
|
|
1148
|
-
pct = max(0, min(100, pct))
|
|
1149
|
-
eighths = pct * width * 8 // 100
|
|
1150
|
-
full, rem = divmod(eighths, 8)
|
|
1151
|
-
out = color + "█" * full
|
|
1152
|
-
if rem and full < width:
|
|
1153
|
-
out += PARTS[rem]
|
|
1154
|
-
full += 1
|
|
1155
|
-
out += Colors.MUTE + "─" * (width - full) + Colors.R
|
|
1156
|
-
return out
|
|
1157
|
-
|
|
1158
1131
|
def rate_of_change(history: list, now: float, value: float, window: float = 20.0, max_len: int = 60):
|
|
1159
1132
|
"""Tracks `value` over time in `history` (a list of (ts, value) pairs,
|
|
1160
1133
|
mutated in place and capped to `max_len` entries) and returns its rate
|
|
@@ -1189,68 +1162,6 @@ def sparkline(rate_history: list, rate: float, max_len: int = 14) -> str:
|
|
|
1189
1162
|
mx = max(rate_history, default=1) or 1
|
|
1190
1163
|
return "".join(SPARK[max(0, min(7, int(r / mx * 7.99)))] for r in rate_history)
|
|
1191
1164
|
|
|
1192
|
-
def header_box(left: str, right: str, cols: int, spin: str = "") -> list:
|
|
1193
|
-
"""The 3-line rounded header frame (top rule, title row, bottom rule)
|
|
1194
|
-
for a live view - `cols` is total frame width, i.e. content_width()'s
|
|
1195
|
-
return value. The title row's padding is computed from the *actual*
|
|
1196
|
-
rendered pieces (left, right, spin), so the right border always lands
|
|
1197
|
-
exactly under the corners no matter how any of the three are sized -
|
|
1198
|
-
this single spot is the only place that math needs to be right.
|
|
1199
|
-
|
|
1200
|
-
Truncates `left` (then `right`, if even that isn't enough) so the row
|
|
1201
|
-
never runs past `cols` regardless of terminal width - `right` (typically
|
|
1202
|
-
a short, fixed-format clock) is kept whole for as long as it can be;
|
|
1203
|
-
`left` (the variable, more compressible piece - a title/library name)
|
|
1204
|
-
gives way first.
|
|
1205
|
-
|
|
1206
|
-
Bakes in its own MARGIN_H left indent (matching every hand-written
|
|
1207
|
-
widget line in backbone/prompt/ - e.g. confirm()'s
|
|
1208
|
-
f" {message}") rather than relying on a wrapper to add it: a caller
|
|
1209
|
-
driving its view through _Widget.render() (prompt/core.py) gets no
|
|
1210
|
-
such wrapper, since _Widget only manages the vertical margin itself.
|
|
1211
|
-
"""
|
|
1212
|
-
interior = cols - 2
|
|
1213
|
-
# Reserve the spinner plus one pad column *before* sizing left/right, so
|
|
1214
|
-
# truncating to fit `budget` always leaves room for pad >= 1 - flooring
|
|
1215
|
-
# pad afterward instead (max(1, ...)) can push the row a column past the
|
|
1216
|
-
# border once left+right already exactly fill the interior.
|
|
1217
|
-
budget = max(0, interior - len(spin) - 1)
|
|
1218
|
-
if visual_len(left) + visual_len(right) > budget:
|
|
1219
|
-
right = truncate_text(right, min(visual_len(right), budget))
|
|
1220
|
-
left = truncate_text(left, max(0, budget - visual_len(right)))
|
|
1221
|
-
pad = max(0, interior - visual_len(left) - visual_len(right) - len(spin))
|
|
1222
|
-
C = Colors
|
|
1223
|
-
hpad = " " * MARGIN_H
|
|
1224
|
-
return [
|
|
1225
|
-
f"{hpad}{C.FRAME}╭{rule(interior)}╮{C.R}",
|
|
1226
|
-
f"{hpad}{C.FRAME}│{C.B}{left}{C.R}{' ' * pad}{C.TXT}{right}{C.R}{spin}{C.FRAME}│{C.R}",
|
|
1227
|
-
f"{hpad}{C.FRAME}╰{rule(interior)}╯{C.R}",
|
|
1228
|
-
]
|
|
1229
|
-
|
|
1230
|
-
def wrap_margins(lines: list, width: int = None) -> str:
|
|
1231
|
-
"""Applies the global MARGIN_H/MARGIN_V inset plus per-line
|
|
1232
|
-
clear-to-end-of-line, ready for one `sys.stdout.write` - the standard
|
|
1233
|
-
back* frame render (pair with an `ESC[H` cursor-home beforehand).
|
|
1234
|
-
|
|
1235
|
-
Joins with \\r\\n, not \\n: raw terminal mode (tty.setraw, used by
|
|
1236
|
-
backbone.prompt.core for key reading) clears OPOST, so the terminal
|
|
1237
|
-
stops translating a bare \\n into a carriage return - every line after
|
|
1238
|
-
the first would otherwise start wherever the previous one ended instead
|
|
1239
|
-
of column 1.
|
|
1240
|
-
|
|
1241
|
-
`width` (typically content_width()'s return value), if given, clips
|
|
1242
|
-
every line to it first - a safety net so one field a caller forgot to
|
|
1243
|
-
size itself can't overflow the whole frame. Hand-tuned per-field
|
|
1244
|
-
truncation still reads better (an ellipsis where it makes sense, not a
|
|
1245
|
-
hard cut mid-word); this is the guarantee behind it, not a replacement.
|
|
1246
|
-
"""
|
|
1247
|
-
if width is not None:
|
|
1248
|
-
lines = [clip_ansi(line, width) for line in lines]
|
|
1249
|
-
hpad = " " * MARGIN_H
|
|
1250
|
-
vpad = ["\033[K"] * MARGIN_V
|
|
1251
|
-
out = vpad + [hpad + line + "\033[K" for line in lines] + vpad
|
|
1252
|
-
return "\r\n".join(out)
|
|
1253
|
-
|
|
1254
1165
|
def spinner(frame: int) -> str:
|
|
1255
1166
|
return SPIN[frame % len(SPIN)]
|
|
1256
1167
|
|
|
@@ -1276,33 +1187,13 @@ _ANSI_DEMO = re.compile(r"\033\[[0-9;]*[a-zA-Z]")
|
|
|
1276
1187
|
|
|
1277
1188
|
|
|
1278
1189
|
def _demo() -> None:
|
|
1279
|
-
"""Self-check for the pure logic here
|
|
1190
|
+
"""Self-check for the pure logic here.
|
|
1280
1191
|
Runs without a terminal: `python3 -m backbone.ui`.
|
|
1281
1192
|
"""
|
|
1282
|
-
for cols in (70, 100, 137):
|
|
1283
|
-
for left, right, spin in ((" SHORT", "12:00:00 ", "X"), ("", "", ""), ("a" * 20, "b", "Y")):
|
|
1284
|
-
top, mid, bot = header_box(left, right, cols, spin)
|
|
1285
|
-
widths = {len(strip_ansi(top)), len(strip_ansi(mid)), len(strip_ansi(bot))}
|
|
1286
|
-
assert len(widths) == 1, (cols, left, right, spin, widths)
|
|
1287
|
-
# A left piece far longer than the frame must truncate, not overflow -
|
|
1288
|
-
# the border still lines up at a width too narrow for it whole.
|
|
1289
|
-
for cols in (10, 20, 40):
|
|
1290
|
-
top, mid, bot = header_box("a" * 200, "12:00:00 ", cols, "X")
|
|
1291
|
-
widths = {len(strip_ansi(top)), len(strip_ansi(mid)), len(strip_ansi(bot))}
|
|
1292
|
-
assert len(widths) == 1, (cols, widths)
|
|
1293
|
-
assert len(strip_ansi(mid)) == cols + MARGIN_H, (cols, len(strip_ansi(mid)))
|
|
1294
1193
|
assert visual_len("plain") == 5
|
|
1295
1194
|
assert visual_len(f"{Colors.BOLD}x{Colors.RESET}") == 1
|
|
1296
1195
|
assert truncate_text("abcdefgh", 4) == "abc…"
|
|
1297
1196
|
assert plural(1, "disc") == "1 disc" and plural(2, "disc") == "2 discs"
|
|
1298
|
-
# Regression guard: wrap_margins must join with \r\n, not \n - under raw
|
|
1299
|
-
# terminal mode (OPOST cleared) a bare \n never returns to column 1, and
|
|
1300
|
-
# every line after the first starts wherever the previous one ended.
|
|
1301
|
-
assert "\r\n" in wrap_margins(["a", "b"])
|
|
1302
|
-
# wrap_margins(width=...) must clip an oversized line rather than let it
|
|
1303
|
-
# overflow - the safety net behind every tool's own per-field sizing.
|
|
1304
|
-
clipped = wrap_margins(["a" * 200], width=10).split("\r\n")[1]
|
|
1305
|
-
assert len(strip_ansi(clipped)) <= 10 + MARGIN_H, clipped
|
|
1306
1197
|
assert human_gb(1048576) == "1.0"
|
|
1307
1198
|
hist = []
|
|
1308
1199
|
assert rate_of_change(hist, 0.0, 0) is None # first sample - no window yet
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: backpack-backbone
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Shared code for the back* tools: terminal UI, prompt widgets, live views, logging, dates, settings plumbing and small file helpers, implemented once.
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/RedEraRrow/backbone
|
|
@@ -50,9 +50,9 @@ how to install anything it can't run without.
|
|
|
50
50
|
## What's in it
|
|
51
51
|
|
|
52
52
|
**`ui`** - the visual layer. `Colors` (honours `NO_COLOR=1`), the global
|
|
53
|
-
`MARGIN_H` / `MARGIN_V` inset every frame is drawn inside,
|
|
54
|
-
|
|
55
|
-
the `SPIN` / `
|
|
53
|
+
`MARGIN_H` / `MARGIN_V` inset every frame is drawn inside, the progress
|
|
54
|
+
bar (`progress_cells`, `get_progress_bar`), `sparkline`, `spinner`,
|
|
55
|
+
`rate_of_change`, and the `SPIN` / `SPARK` glyph sets. Also the ANSI-aware text
|
|
56
56
|
measuring (`visual_len`, `truncate_text`, `clip_ansi`, `strip_ansi`) that
|
|
57
57
|
makes any of that survive colour codes and wide characters, and small
|
|
58
58
|
formatters: `plural`, `human_gb`, `dir_size_kb`. The accent colour is chosen
|
|
@@ -60,16 +60,9 @@ with `set_accent` (an `ACCENT_PRESETS` key or `#RRGGBB`); a tool calls it once
|
|
|
60
60
|
at startup from its own settings, and `accent_code` / `accent_label` let a
|
|
61
61
|
settings screen check and name a value.
|
|
62
62
|
|
|
63
|
-
**`prompt.settings`** - what a Settings screen is built from, so every
|
|
64
|
-
tool's reads the same: `SETTINGS_COLUMNS` (a name, then its state),
|
|
65
|
-
`state_glyph` (● / ○), `space_toggles` (space flips the on/off rows),
|
|
66
|
-
`index_of` (the cursor back on the row just changed), `pick_option` (one of
|
|
67
|
-
a fixed set of values) and `pick_accent` (the accent colour picker, each
|
|
68
|
-
colour shown in itself).
|
|
69
|
-
|
|
70
63
|
`from backbone import ...` exposes only a subset of `ui` (`Colors`, the
|
|
71
|
-
margins and glyph sets, `spinner`, `content_width
|
|
72
|
-
|
|
64
|
+
margins and glyph sets, `spinner`, `content_width`); import anything else
|
|
65
|
+
from its module, e.g.
|
|
73
66
|
`from backbone.ui import human_gb`.
|
|
74
67
|
|
|
75
68
|
**`prompt`** - the widgets. `select` (single or multi), `confirm`, `text`,
|
|
@@ -79,6 +72,13 @@ values: `calendar_select`, `datetime_edit`, `time_edit`,
|
|
|
79
72
|
`system_editor_edit`. All resize-aware, all mouse-aware, all rendered through
|
|
80
73
|
the same painter.
|
|
81
74
|
|
|
75
|
+
**`prompt.settings`** - what a Settings screen is built from, so every
|
|
76
|
+
tool's reads the same: `SETTINGS_COLUMNS` (a name, then its state),
|
|
77
|
+
`state_glyph` (● / ○), `space_toggles` (space flips the on/off rows),
|
|
78
|
+
`index_of` (the cursor back on the row just changed), `pick_option` (one of
|
|
79
|
+
a fixed set of values) and `pick_accent` (the accent colour picker, each
|
|
80
|
+
colour shown in itself).
|
|
81
|
+
|
|
82
82
|
**`prompt.core`** - the primitives underneath: the screen-diff painter, key
|
|
83
83
|
reading, the `Choice` and `Column` types, the footer hint bar (`hint`) and its
|
|
84
84
|
click mapping, and `run_dashboard`.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "backpack-backbone"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.0"
|
|
8
8
|
description = "Shared code for the back* tools: terminal UI, prompt widgets, live views, logging, dates, settings plumbing and small file helpers, implemented once."
|
|
9
9
|
requires-python = ">=3.10"
|
|
10
10
|
readme = "README.md"
|
|
@@ -105,5 +105,34 @@ class VolumeKeysTest(unittest.TestCase):
|
|
|
105
105
|
self.assertEqual(got, ["vol_up", "vol_down"])
|
|
106
106
|
|
|
107
107
|
|
|
108
|
+
class KeyBindingsPageTest(unittest.TestCase):
|
|
109
|
+
"""The Key bindings page lists what the app has a use for: the screens it
|
|
110
|
+
names, and the transport and volume keys only with a player."""
|
|
111
|
+
|
|
112
|
+
def rows(self, scopes=None, handler=None, opener=None):
|
|
113
|
+
from backbone.prompt import chrome, keymap
|
|
114
|
+
with patch.object(chrome, "_transport_handler", handler), patch.object(chrome, "_player_opener", opener):
|
|
115
|
+
return [c.value or c.title for c in keymap._choices(scopes)]
|
|
116
|
+
|
|
117
|
+
def test_no_player_no_transport_keys(self):
|
|
118
|
+
rows = self.rows()
|
|
119
|
+
self.assertNotIn("global.playpause", rows)
|
|
120
|
+
self.assertNotIn("global.player", rows)
|
|
121
|
+
self.assertNotIn("Volume", rows) # its group goes with its keys
|
|
122
|
+
self.assertIn("global.help", rows)
|
|
123
|
+
|
|
124
|
+
def test_with_a_player_they_are_there(self):
|
|
125
|
+
rows = self.rows(handler=print, opener=print)
|
|
126
|
+
self.assertIn("global.playpause", rows)
|
|
127
|
+
self.assertIn("global.player", rows)
|
|
128
|
+
self.assertIn("volume.up", rows)
|
|
129
|
+
|
|
130
|
+
def test_only_the_named_groups(self):
|
|
131
|
+
rows = self.rows(["list"])
|
|
132
|
+
self.assertIn("list.up", rows)
|
|
133
|
+
self.assertNotIn("confirm.yes", rows)
|
|
134
|
+
self.assertEqual(rows[-4:], ["This page", "keys_editor.remove", "keys_editor.reset", "keys_editor.reset_all"])
|
|
135
|
+
|
|
136
|
+
|
|
108
137
|
if __name__ == "__main__":
|
|
109
138
|
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{backpack_backbone-0.3.2 → backpack_backbone-0.4.0}/backpack_backbone.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{backpack_backbone-0.3.2 → backpack_backbone-0.4.0}/backpack_backbone.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|