backpack-backbone 0.3.0__tar.gz → 0.3.1__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.0/backpack_backbone.egg-info → backpack_backbone-0.3.1}/PKG-INFO +1 -1
- {backpack_backbone-0.3.0 → backpack_backbone-0.3.1}/backbone/prompt/audio.py +6 -6
- {backpack_backbone-0.3.0 → backpack_backbone-0.3.1}/backbone/prompt/chrome.py +11 -9
- {backpack_backbone-0.3.0 → backpack_backbone-0.3.1}/backbone/prompt/core.py +255 -38
- {backpack_backbone-0.3.0 → backpack_backbone-0.3.1}/backbone/prompt/lists.py +27 -15
- {backpack_backbone-0.3.0 → backpack_backbone-0.3.1}/backbone/prompt/text.py +56 -25
- {backpack_backbone-0.3.0 → backpack_backbone-0.3.1}/backbone/ui.py +11 -3
- {backpack_backbone-0.3.0 → backpack_backbone-0.3.1/backpack_backbone.egg-info}/PKG-INFO +1 -1
- {backpack_backbone-0.3.0 → backpack_backbone-0.3.1}/pyproject.toml +1 -1
- {backpack_backbone-0.3.0 → backpack_backbone-0.3.1}/tests/test_boxes.py +158 -9
- {backpack_backbone-0.3.0 → backpack_backbone-0.3.1}/tests/test_hint_clicks.py +1 -1
- {backpack_backbone-0.3.0 → backpack_backbone-0.3.1}/tests/test_keys.py +3 -3
- {backpack_backbone-0.3.0 → backpack_backbone-0.3.1}/tests/test_overlay.py +49 -0
- {backpack_backbone-0.3.0 → backpack_backbone-0.3.1}/tests/test_tabs.py +14 -4
- {backpack_backbone-0.3.0 → backpack_backbone-0.3.1}/LICENSE +0 -0
- {backpack_backbone-0.3.0 → backpack_backbone-0.3.1}/README.md +0 -0
- {backpack_backbone-0.3.0 → backpack_backbone-0.3.1}/backbone/__init__.py +0 -0
- {backpack_backbone-0.3.0 → backpack_backbone-0.3.1}/backbone/app.py +0 -0
- {backpack_backbone-0.3.0 → backpack_backbone-0.3.1}/backbone/datetime_parse.py +0 -0
- {backpack_backbone-0.3.0 → backpack_backbone-0.3.1}/backbone/deps.py +0 -0
- {backpack_backbone-0.3.0 → backpack_backbone-0.3.1}/backbone/files.py +0 -0
- {backpack_backbone-0.3.0 → backpack_backbone-0.3.1}/backbone/keyboard.py +0 -0
- {backpack_backbone-0.3.0 → backpack_backbone-0.3.1}/backbone/keys.py +0 -0
- {backpack_backbone-0.3.0 → backpack_backbone-0.3.1}/backbone/log.py +0 -0
- {backpack_backbone-0.3.0 → backpack_backbone-0.3.1}/backbone/nav.py +0 -0
- {backpack_backbone-0.3.0 → backpack_backbone-0.3.1}/backbone/notify.py +0 -0
- {backpack_backbone-0.3.0 → backpack_backbone-0.3.1}/backbone/numbering.py +0 -0
- {backpack_backbone-0.3.0 → backpack_backbone-0.3.1}/backbone/output.py +0 -0
- {backpack_backbone-0.3.0 → backpack_backbone-0.3.1}/backbone/procs.py +0 -0
- {backpack_backbone-0.3.0 → backpack_backbone-0.3.1}/backbone/prompt/__init__.py +0 -0
- {backpack_backbone-0.3.0 → backpack_backbone-0.3.1}/backbone/prompt/dates.py +0 -0
- {backpack_backbone-0.3.0 → backpack_backbone-0.3.1}/backbone/prompt/keymap.py +0 -0
- {backpack_backbone-0.3.0 → backpack_backbone-0.3.1}/backbone/prompt/list_edit.py +0 -0
- {backpack_backbone-0.3.0 → backpack_backbone-0.3.1}/backbone/prompt/timezone.py +0 -0
- {backpack_backbone-0.3.0 → backpack_backbone-0.3.1}/backbone/prompt/values.py +0 -0
- {backpack_backbone-0.3.0 → backpack_backbone-0.3.1}/backbone/terminal_input.py +0 -0
- {backpack_backbone-0.3.0 → backpack_backbone-0.3.1}/backbone/timefmt.py +0 -0
- {backpack_backbone-0.3.0 → backpack_backbone-0.3.1}/backpack_backbone.egg-info/SOURCES.txt +0 -0
- {backpack_backbone-0.3.0 → backpack_backbone-0.3.1}/backpack_backbone.egg-info/dependency_links.txt +0 -0
- {backpack_backbone-0.3.0 → backpack_backbone-0.3.1}/backpack_backbone.egg-info/top_level.txt +0 -0
- {backpack_backbone-0.3.0 → backpack_backbone-0.3.1}/setup.cfg +0 -0
- {backpack_backbone-0.3.0 → backpack_backbone-0.3.1}/tests/test_datetime_parse.py +0 -0
- {backpack_backbone-0.3.0 → backpack_backbone-0.3.1}/tests/test_deps.py +0 -0
- {backpack_backbone-0.3.0 → backpack_backbone-0.3.1}/tests/test_edit_line.py +0 -0
- {backpack_backbone-0.3.0 → backpack_backbone-0.3.1}/tests/test_list_edit_render.py +0 -0
- {backpack_backbone-0.3.0 → backpack_backbone-0.3.1}/tests/test_log.py +0 -0
- {backpack_backbone-0.3.0 → backpack_backbone-0.3.1}/tests/test_multiline.py +0 -0
- {backpack_backbone-0.3.0 → backpack_backbone-0.3.1}/tests/test_procs.py +0 -0
- {backpack_backbone-0.3.0 → backpack_backbone-0.3.1}/tests/test_quietly.py +0 -0
- {backpack_backbone-0.3.0 → backpack_backbone-0.3.1}/tests/test_select_move.py +0 -0
- {backpack_backbone-0.3.0 → backpack_backbone-0.3.1}/tests/test_terminal_input.py +0 -0
- {backpack_backbone-0.3.0 → backpack_backbone-0.3.1}/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.
|
|
3
|
+
Version: 0.3.1
|
|
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
|
|
@@ -67,12 +67,12 @@ keys.define("levels", "Volume and equaliser", [
|
|
|
67
67
|
("up_big", ("PGUP",), "raise by 3"),
|
|
68
68
|
("down_big", ("PGDN",), "lower by 3"),
|
|
69
69
|
("zero", ("0",), "back to zero"),
|
|
70
|
-
("prev_band", ("LEFT",), "previous band
|
|
71
|
-
("next_band", ("RIGHT",), "next band
|
|
72
|
-
("add", ("a", "A"), "add a band
|
|
73
|
-
("delete", ("d", "D", "BACKSPACE", "DELETE"), "delete the band
|
|
74
|
-
("flat", ("f", "F"), "flatten every band
|
|
75
|
-
("preset", ("p", "P"), "next preset
|
|
70
|
+
("prev_band", ("LEFT",), "previous band"),
|
|
71
|
+
("next_band", ("RIGHT",), "next band"),
|
|
72
|
+
("add", ("a", "A"), "add a band"),
|
|
73
|
+
("delete", ("d", "D", "BACKSPACE", "DELETE"), "delete the band"),
|
|
74
|
+
("flat", ("f", "F"), "flatten every band"),
|
|
75
|
+
("preset", ("p", "P"), "next preset"),
|
|
76
76
|
("save", ("ENTER",), "save"),
|
|
77
77
|
("back", ("ESC",), "back"),
|
|
78
78
|
], within=("list", "global"))
|
|
@@ -79,20 +79,22 @@ _command_line = None
|
|
|
79
79
|
|
|
80
80
|
|
|
81
81
|
def set_command_line(fn) -> None:
|
|
82
|
-
"""Register a ``callable()`` that `:` opens, where a screen
|
|
83
|
-
It returns whether it opened a screen of its own (the
|
|
84
|
-
needs drawing again)
|
|
82
|
+
"""Register a ``callable(around=None)`` that `:` opens, where a screen
|
|
83
|
+
leaves it free. It returns whether it opened a screen of its own (the
|
|
84
|
+
screen under it then needs drawing again), opening it inside `around()`
|
|
85
|
+
when given."""
|
|
85
86
|
global _command_line
|
|
86
87
|
_command_line = fn
|
|
87
88
|
|
|
88
89
|
|
|
89
|
-
def open_command_line() -> bool | None:
|
|
90
|
-
"""Run the registered command line: whether it opened a screen of its own
|
|
91
|
-
|
|
92
|
-
|
|
90
|
+
def open_command_line(around=None) -> bool | None:
|
|
91
|
+
"""Run the registered command line: whether it opened a screen of its own
|
|
92
|
+
(inside the context `around()`, when given: the player steps out of view
|
|
93
|
+
only for that); None when there is none to run here (none registered, or
|
|
94
|
+
a screen that keeps you in it, nav.modal())."""
|
|
93
95
|
if _command_line is None or nav.is_modal():
|
|
94
96
|
return None
|
|
95
|
-
return bool(_command_line())
|
|
97
|
+
return bool(_command_line(around=around) if around else _command_line())
|
|
96
98
|
|
|
97
99
|
|
|
98
100
|
# --- shared widget chrome -------------------------------------------------
|
|
@@ -142,7 +144,7 @@ def append_chrome(out: list, pairs, cells: dict, *, extra: str = "",
|
|
|
142
144
|
|
|
143
145
|
The bar is empty unless hints are switched on; either way the top line
|
|
144
146
|
(`out[0]`) carries the corner toggle. `help_key`: this screen leaves `?` free,
|
|
145
|
-
so `?` toggles and the corner says so; otherwise it names Ctrl
|
|
147
|
+
so `?` toggles and the corner says so; otherwise it names Ctrl-/ (or Ctrl-G).
|
|
146
148
|
"""
|
|
147
149
|
items = chrome_hint_pairs(pairs)
|
|
148
150
|
hint_lines = _hint(*items, extra=extra).splitlines()
|
|
@@ -8,7 +8,7 @@ import textwrap
|
|
|
8
8
|
import time
|
|
9
9
|
import select as _sel
|
|
10
10
|
from dataclasses import dataclass, field
|
|
11
|
-
from typing import Any
|
|
11
|
+
from typing import Any, Callable
|
|
12
12
|
|
|
13
13
|
from backbone import keys, nav, ui
|
|
14
14
|
from backbone.log import log, enabled as _logging, quietly
|
|
@@ -110,6 +110,7 @@ def screen_invalidate() -> None:
|
|
|
110
110
|
_screen_size[0] = size
|
|
111
111
|
_reserved.clear() # a frame after this reserves afresh
|
|
112
112
|
_shown_pictures.clear() # a clear took them with it
|
|
113
|
+
_picture_areas.clear()
|
|
113
114
|
_float.clear()
|
|
114
115
|
_screen.clear()
|
|
115
116
|
_cells.clear()
|
|
@@ -140,6 +141,7 @@ def _resize_wipe() -> str:
|
|
|
140
141
|
_screen.clear()
|
|
141
142
|
_cells.clear()
|
|
142
143
|
_shown_pictures.clear()
|
|
144
|
+
_float.clear() # the wipe takes a floating box with it; it isn't drawn back at the old place
|
|
143
145
|
return "\033[H\033[2J" if was is not None else ""
|
|
144
146
|
|
|
145
147
|
|
|
@@ -301,7 +303,8 @@ def _notched(cells: list, notch: tuple) -> list:
|
|
|
301
303
|
def _title_right(cells: list, left: int, right: int, after: int) -> None:
|
|
302
304
|
"""Move a box's top-border title (" Title ", just after its corner) to
|
|
303
305
|
the right end of the border, before what's already there (a subtitle,
|
|
304
|
-
the help toggle), past column `after
|
|
306
|
+
the help toggle), past column `after`: shortened by whole " · " parts
|
|
307
|
+
where it won't fit, and dropped where even its first won't."""
|
|
305
308
|
if left + 2 >= right or cells[left + 1][1] not in _EDGE or cells[left + 2][1] != " ":
|
|
306
309
|
return
|
|
307
310
|
end = next((i for i in range(left + 3, right) if cells[i][1] in _EDGE and cells[i - 1][1] == " "), None)
|
|
@@ -312,11 +315,10 @@ def _title_right(cells: list, left: int, right: int, after: int) -> None:
|
|
|
312
315
|
cells[i] = dash
|
|
313
316
|
stop = next((i for i in range(end, right) if cells[i][1] not in _EDGE), right) - 1
|
|
314
317
|
room = stop - (after + 2) # columns between the tab and what's at the right
|
|
315
|
-
if len(title) > room
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
title.insert(-2, (title[-2][0], " "))
|
|
318
|
+
if len(title) > room: # never cut short: whole " · " parts off the end, else none
|
|
319
|
+
cuts = [i for i in range(len(title) - 2, 0, -1)
|
|
320
|
+
if (title[i - 1][1], title[i][1], title[i + 1][1]) == (" ", "·", " ") and i <= room]
|
|
321
|
+
title = title[:cuts[0]] if cuts else [] # " Queue · 1 of 28 " → " Queue "
|
|
320
322
|
start = stop - len(title)
|
|
321
323
|
if start > after + 1:
|
|
322
324
|
cells[start:stop] = title
|
|
@@ -356,6 +358,9 @@ def screen_span_paint(row: int, col: int, text: str) -> str:
|
|
|
356
358
|
before = list(new)
|
|
357
359
|
if old is None: # unknown row: the span's cells all written
|
|
358
360
|
before = [_UNKNOWN] * len(new)
|
|
361
|
+
for first, last in _picture_cols(row): # an image is there, whatever the cells say:
|
|
362
|
+
for k in range(max(first, col) - 1, min(last, col - 1 + len(span))):
|
|
363
|
+
before[k] = _UNKNOWN # write them all, or it shows through the blanks
|
|
359
364
|
new[col - 1:col - 1 + len(span)] = span
|
|
360
365
|
_cells[row] = new
|
|
361
366
|
_screen[row] = _screen.get(row, "") + "\x00span" # the row's text no longer says it all
|
|
@@ -507,14 +512,67 @@ def screen_float(lines: list, seconds: float) -> None:
|
|
|
507
512
|
cols, rows = ui.get_terminal_size()
|
|
508
513
|
lines = [ui.clip_ansi(ln, cols) for ln in lines[:rows]]
|
|
509
514
|
w = max((ui.visual_len(ln) for ln in lines), default=0)
|
|
510
|
-
place = dict(w=w, top=max(1, (rows - len(lines)) // 2 + 1), left=max(1, (cols - w) // 2 + 1))
|
|
511
|
-
if _float and (len(_float['lines']), _float['w'], _float['top'], _float['left']) != (
|
|
515
|
+
place = dict(w=w, top=max(1, (rows - len(lines)) // 2 + 1), left=max(1, (cols - w) // 2 + 1), size=(cols, rows))
|
|
516
|
+
if _float and _float['size'] == (cols, rows) and (len(_float['lines']), _float['w'], _float['top'], _float['left']) != (
|
|
512
517
|
len(lines), place['w'], place['top'], place['left']):
|
|
513
|
-
_float_gone() # a new shape: put back what the old one covered
|
|
518
|
+
_float_gone() # a new shape: put back what the old one covered (not at a new size: those cells are gone)
|
|
514
519
|
_float.update(lines=lines, until=time.time() + seconds, **place)
|
|
515
520
|
_float_draw()
|
|
516
521
|
|
|
517
522
|
|
|
523
|
+
_backdrops: list = [] # the screens showing, innermost last: their redraw, and how they meet a small window
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def screen_backdrop(redraw, small: bool = False) -> Callable[[], None]:
|
|
527
|
+
"""Say the screen now showing is this one, till the returned call (for
|
|
528
|
+
its `finally`) takes it back. `redraw()` lays it out afresh at the current
|
|
529
|
+
size, what an overlay over it calls on a resize; None: it notices a new
|
|
530
|
+
size itself (asked to with ui.request_relayout). `small`: it lays out a
|
|
531
|
+
window too small for boxes itself (the player, the miniplayer), so no
|
|
532
|
+
miniplayer takes over from it."""
|
|
533
|
+
entry = {'redraw': redraw, 'small': small, 'overlay': False}
|
|
534
|
+
_backdrops.append(entry)
|
|
535
|
+
return lambda: _backdrops.remove(entry) if entry in _backdrops else None
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def overlay_layer() -> Callable[[], None]:
|
|
539
|
+
"""An overlay opening over the screen showing: it meets any size itself,
|
|
540
|
+
and what it redraws under it is that screen. Returns the call that closes
|
|
541
|
+
the layer, made before the screen under it is put back."""
|
|
542
|
+
entry = {'redraw': None, 'small': True, 'overlay': True}
|
|
543
|
+
_backdrops.append(entry)
|
|
544
|
+
return lambda: _backdrops.remove(entry) if entry in _backdrops else None
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def _screen_under_overlay() -> dict | None:
|
|
548
|
+
"""The screen an overlay is over (or the top one, with none open)."""
|
|
549
|
+
return next((e for e in reversed(_backdrops) if not e['overlay']), None)
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def relayout_under_overlay() -> None:
|
|
553
|
+
"""The window was resized while an overlay is open: the screen under it
|
|
554
|
+
lays itself out at the new size, where it can keep its boxes (smaller,
|
|
555
|
+
the overlay has the window to itself). The overlay then places itself
|
|
556
|
+
afresh over what's there now."""
|
|
557
|
+
_float.clear() # a floating box goes; its owner shows it again if it's still wanted
|
|
558
|
+
if box_fits():
|
|
559
|
+
redraw_under_overlay()
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def redraw_under_overlay() -> None:
|
|
563
|
+
"""The screen under an overlay drawn again, whole, at the current size:
|
|
564
|
+
by its redraw, or, with none to call, cleared and asked to lay itself out
|
|
565
|
+
(it notices at its next resize check)."""
|
|
566
|
+
under = _screen_under_overlay()
|
|
567
|
+
if under and under['redraw'] is not None:
|
|
568
|
+
under['redraw']()
|
|
569
|
+
return
|
|
570
|
+
sys.stdout.write("\033[H\033[2J")
|
|
571
|
+
sys.stdout.flush()
|
|
572
|
+
screen_invalidate()
|
|
573
|
+
ui.request_relayout()
|
|
574
|
+
|
|
575
|
+
|
|
518
576
|
def screen_float_close() -> None:
|
|
519
577
|
"""Take a floating box away now, putting back what it covered."""
|
|
520
578
|
if _float:
|
|
@@ -578,6 +636,26 @@ def float_tick() -> bool:
|
|
|
578
636
|
return True
|
|
579
637
|
|
|
580
638
|
|
|
639
|
+
_picture_areas: dict = {} # pictures other screens draw themselves: name → (top, left, rows, cols)
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
def screen_picture_area(name: str, area: tuple | None) -> None:
|
|
643
|
+
"""Say a screen drawing its own picture (the player's cover) has one at
|
|
644
|
+
`area` (top, left, rows, cols; 1-based), or None now it's gone: anything
|
|
645
|
+
drawn over it then writes every cell, so none of the image shows through."""
|
|
646
|
+
if area:
|
|
647
|
+
_picture_areas[name] = area
|
|
648
|
+
else:
|
|
649
|
+
_picture_areas.pop(name, None)
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
def _picture_cols(row: int) -> list[tuple[int, int]]:
|
|
653
|
+
"""The columns (first, last; 1-based) of `row` a picture covers."""
|
|
654
|
+
return ([(c, c + size[0] - 1 if size else 10 ** 6)
|
|
655
|
+
for r, c, n, _k, _esc, *size in _shown_pictures if r <= row < r + n]
|
|
656
|
+
+ [(c, c + w - 1) for r, c, n, w in _picture_areas.values() if r <= row < r + n])
|
|
657
|
+
|
|
658
|
+
|
|
581
659
|
def screen_forget_pictures() -> None:
|
|
582
660
|
"""For a screen that draws without _Widget (a player view): forget the
|
|
583
661
|
cells under the pictures the last widget drew, so this screen's paint
|
|
@@ -590,6 +668,21 @@ def screen_forget_pictures() -> None:
|
|
|
590
668
|
_shown_pictures.clear()
|
|
591
669
|
|
|
592
670
|
|
|
671
|
+
_BORDER_CHARS = set("╭╮╰╯│─├┤┬┴┼┊┈")
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
def screen_crosses_box(first_row: int, last_row: int, first_col: int, last_col: int) -> bool:
|
|
675
|
+
"""Whether any box's border on screen lies in the rectangle (1-based,
|
|
676
|
+
inclusive): something drawn there would cut that box, leaving it half a
|
|
677
|
+
box. A rectangle wholly inside one box's inside crosses nothing."""
|
|
678
|
+
for r in range(first_row, last_row + 1):
|
|
679
|
+
cells = _cells.get(r) or []
|
|
680
|
+
for c in range(first_col - 1, min(last_col, len(cells))):
|
|
681
|
+
if cells[c] is not _UNKNOWN and cells[c][1] in _BORDER_CHARS:
|
|
682
|
+
return True
|
|
683
|
+
return False
|
|
684
|
+
|
|
685
|
+
|
|
593
686
|
def screen_save(first_row: int, last_row: int, first_col: int, last_col: int) -> dict:
|
|
594
687
|
"""What the painter holds in a rectangle, to put back with screen_restore
|
|
595
688
|
once something drawn over it (an overlay) is gone."""
|
|
@@ -640,6 +733,8 @@ def screen_paint(rows: dict, *, cursor: tuple | None = None,
|
|
|
640
733
|
inputs); `save_cursor` wraps the frame in DEC save/restore so a caret
|
|
641
734
|
elsewhere is left alone (background repaints).
|
|
642
735
|
"""
|
|
736
|
+
if _cramped(): # a frame without its boxes: the miniplayer shows instead
|
|
737
|
+
return
|
|
643
738
|
rows = _takeover_rows(rows)
|
|
644
739
|
parts: list[str] = []
|
|
645
740
|
for row in sorted(rows):
|
|
@@ -722,6 +817,8 @@ def _render_footer_bar() -> None:
|
|
|
722
817
|
change lands) when nothing else redraws. Repaints when either the track
|
|
723
818
|
identity or the styled rows changed, so an idle screen never flickers yet a
|
|
724
819
|
new song is never missed."""
|
|
820
|
+
if _chrome_held(): # the miniplayer has the window
|
|
821
|
+
return
|
|
725
822
|
rows = ui.get_terminal_height()
|
|
726
823
|
cols = ui.get_terminal_width()
|
|
727
824
|
lines = ui.footer_lines(cols)
|
|
@@ -736,11 +833,97 @@ def _render_footer_bar() -> None:
|
|
|
736
833
|
sys.stdout.flush()
|
|
737
834
|
|
|
738
835
|
|
|
836
|
+
# A window too small for a screen's boxes: the screen gives way to the
|
|
837
|
+
# miniplayer (a tool registers it), which keeps the window till the boxes fit
|
|
838
|
+
# again. A screen that lays out small windows itself (the player) says so
|
|
839
|
+
# when it registers (screen_backdrop small=True).
|
|
840
|
+
_cramped_view = [None]
|
|
841
|
+
_cramped_showing = [0] # the miniplayer is up (nested: a screen opened from it, then it again)
|
|
842
|
+
|
|
843
|
+
|
|
844
|
+
def set_cramped_view(fn) -> None:
|
|
845
|
+
"""Register ``fn()``: what shows while a window is too small for any
|
|
846
|
+
screen's boxes, returning once they fit again (the miniplayer). With
|
|
847
|
+
none, a box saying the window's too small (_too_small_notice)."""
|
|
848
|
+
_cramped_view[0] = fn
|
|
849
|
+
|
|
850
|
+
|
|
851
|
+
def _too_small() -> bool:
|
|
852
|
+
"""The screen on top can't keep its boxes (it doesn't lay out small
|
|
853
|
+
windows itself), so the miniplayer, or the notice, shows instead."""
|
|
854
|
+
top = _backdrops[-1] if _backdrops else None
|
|
855
|
+
return not box_fits() and not (top and top['small'])
|
|
856
|
+
|
|
857
|
+
|
|
858
|
+
_TOO_SMALL_NOTE = "Make the window bigger"
|
|
859
|
+
|
|
860
|
+
|
|
861
|
+
def _too_small_notice() -> None:
|
|
862
|
+
"""A tool with no miniplayer, in a window too small for its screen's
|
|
863
|
+
boxes: a box saying so where one fits, else nothing (no box, no
|
|
864
|
+
content), till the window grows. Keys wait; Ctrl-C still stops."""
|
|
865
|
+
fd = sys.stdin.fileno()
|
|
866
|
+
drawn = None
|
|
867
|
+
while not box_fits():
|
|
868
|
+
size = ui.get_terminal_size()
|
|
869
|
+
if size != drawn:
|
|
870
|
+
cols, rows = size
|
|
871
|
+
w = ui.visual_len(_TOO_SMALL_NOTE) + 4
|
|
872
|
+
sys.stdout.write("\033[H\033[2J")
|
|
873
|
+
if rows >= 3 and cols >= w:
|
|
874
|
+
top, left = (rows - 3) // 2 + 1, (cols - w) // 2 + 1
|
|
875
|
+
box = box_lines([f"{C.DIM}{_TOO_SMALL_NOTE}{C.RESET}"], w, 3)
|
|
876
|
+
sys.stdout.write("".join(f"\033[{top + k};{left}H{ln[ui.MARGIN_H:]}" for k, ln in enumerate(box)))
|
|
877
|
+
sys.stdout.flush()
|
|
878
|
+
drawn = size
|
|
879
|
+
if _byte_ready(fd, 0.1) and _read_key(fd) == 'CTRL_C':
|
|
880
|
+
raise KeyboardInterrupt
|
|
881
|
+
|
|
882
|
+
|
|
883
|
+
_cramped = _too_small # a screen's paint is held back: the miniplayer shows instead
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
def _chrome_held() -> bool:
|
|
887
|
+
"""The now-playing box and status line stay off: the miniplayer has the window."""
|
|
888
|
+
return _too_small() or _cramped_showing[0] > 0
|
|
889
|
+
|
|
890
|
+
|
|
891
|
+
def _run_cramped() -> None:
|
|
892
|
+
"""The miniplayer until the boxes fit, then the screen under it laid out
|
|
893
|
+
afresh (its next resize check says yes)."""
|
|
894
|
+
tabs_were = ui.set_tabs_hidden(True) # the miniplayer alone
|
|
895
|
+
_cramped_showing[0] += 1
|
|
896
|
+
log.debug("window too small for boxes: the miniplayer")
|
|
897
|
+
try:
|
|
898
|
+
(_cramped_view[0] or _too_small_notice)()
|
|
899
|
+
finally:
|
|
900
|
+
_cramped_showing[0] -= 1
|
|
901
|
+
ui.set_tabs_hidden(tabs_were)
|
|
902
|
+
sys.stdout.write("\033[H\033[2J")
|
|
903
|
+
sys.stdout.flush()
|
|
904
|
+
screen_invalidate()
|
|
905
|
+
ui.request_relayout()
|
|
906
|
+
|
|
907
|
+
|
|
908
|
+
_wheel_at: list = [None] # (row, col) of the last wheel or trackpad scroll
|
|
909
|
+
|
|
910
|
+
|
|
911
|
+
def wheel_at() -> tuple | None:
|
|
912
|
+
"""Where the last SCROLL_UP / SCROLL_DOWN happened, (row, col) 1-based:
|
|
913
|
+
for a screen with more than one thing to scroll."""
|
|
914
|
+
return _wheel_at[0]
|
|
915
|
+
|
|
916
|
+
|
|
739
917
|
def _wait_for_keypress(timeout: float = 0.05) -> bool:
|
|
740
918
|
"""Block up to `timeout` seconds for a keypress; return whether one arrived.
|
|
741
919
|
|
|
742
920
|
Also refreshes the now-playing box at ~4 Hz so background-audio status
|
|
743
|
-
stays live on every widget/menu without each one needing its own tick.
|
|
921
|
+
stays live on every widget/menu without each one needing its own tick.
|
|
922
|
+
In a window too small for the screen's boxes, the miniplayer instead
|
|
923
|
+
(returning no key once the screen's back)."""
|
|
924
|
+
if _cramped():
|
|
925
|
+
_run_cramped()
|
|
926
|
+
return False
|
|
744
927
|
now = time.time()
|
|
745
928
|
if now - _footer_last_draw[0] >= 0.12:
|
|
746
929
|
_footer_last_draw[0] = now
|
|
@@ -783,17 +966,20 @@ def _cols() -> int:
|
|
|
783
966
|
|
|
784
967
|
|
|
785
968
|
# --- Hint bar visibility ------------------------------------------------------
|
|
786
|
-
# One switch for every screen's hint bar, off until turned on (`?`, Ctrl-/
|
|
787
|
-
# `?` is typed, or a click on the corner toggle each screen shows on its top
|
|
969
|
+
# One switch for every screen's hint bar, off until turned on (`?`, Ctrl-/ or
|
|
970
|
+
# Ctrl-G where `?` is typed, or a click on the corner toggle each screen shows on its top
|
|
788
971
|
# line), remembered between runs.
|
|
789
972
|
# Kept in its own small file rather than config.json: screens hold a loaded
|
|
790
973
|
# config and save it back later, which would quietly undo a toggle made meanwhile.
|
|
791
974
|
HINTS_CLICK = '\x00hints' # the key a click on the corner toggle replays
|
|
975
|
+
_TYPED_HELP_KEYS = (("\x07", "\x1f") if os.environ.get("TERM_PROGRAM") == "Apple_Terminal"
|
|
976
|
+
else ("\x1f", "\x07"))
|
|
792
977
|
keys.define("global", "Everywhere", [
|
|
793
|
-
# Wherever it isn't typed or bound; Ctrl-/ (the same key with
|
|
794
|
-
#
|
|
978
|
+
# Wherever it isn't typed or bound; where it is, Ctrl-/ (the same key with
|
|
979
|
+
# Ctrl), and Ctrl-G (nano's help key) for macOS's Terminal, which sends
|
|
980
|
+
# nothing for Ctrl-/. Both work everywhere; what's shown is the first.
|
|
795
981
|
("help", ("?",), "show or hide the key hints"),
|
|
796
|
-
("help_typed",
|
|
982
|
+
("help_typed", _TYPED_HELP_KEYS, "show or hide the key hints, in a text field too"),
|
|
797
983
|
], within=())
|
|
798
984
|
_hints_on: list = [None] # None until first read
|
|
799
985
|
|
|
@@ -862,7 +1048,7 @@ def set_help_toggle_shown(shown: bool) -> None:
|
|
|
862
1048
|
|
|
863
1049
|
|
|
864
1050
|
def is_hints_key(key: str, key_free: bool) -> bool:
|
|
865
|
-
"""Whether `key` toggles the hints: a click on the corner, Ctrl
|
|
1051
|
+
"""Whether `key` toggles the hints: a click on the corner, Ctrl-/ or Ctrl-G, or `?`
|
|
866
1052
|
where the screen leaves `?` free (not typed or bound)."""
|
|
867
1053
|
return (key == HINTS_CLICK or keys.pressed(key, "global.help_typed")
|
|
868
1054
|
or (key_free and keys.pressed(key, "global.help")))
|
|
@@ -870,7 +1056,8 @@ def is_hints_key(key: str, key_free: bool) -> bool:
|
|
|
870
1056
|
|
|
871
1057
|
def help_corner_text(help_key: bool = True, shown: bool | None = None) -> tuple[str, int]:
|
|
872
1058
|
"""The header toggle, styled, and its width: `[?] help` / `[?] hide help`,
|
|
873
|
-
or `[^/] …` where `?` is typed (a text field) and Ctrl-/ is the key
|
|
1059
|
+
or `[^/] …` where `?` is typed (a text field) and Ctrl-/ is the key
|
|
1060
|
+
(`[^g]`, Ctrl-G, in macOS's Terminal).
|
|
874
1061
|
`shown`: the hints' state to describe (default: as they are now).
|
|
875
1062
|
Nothing, when the toggle is switched off (help_toggle_shown)."""
|
|
876
1063
|
if not help_toggle_shown():
|
|
@@ -896,7 +1083,7 @@ def add_help_corner(line: str, row: int, cells: dict, help_key: bool = False) ->
|
|
|
896
1083
|
"""`line` (a screen's top line, drawn on screen row `row`) with the toggle
|
|
897
1084
|
right-aligned on it, clipping the line if the two would meet. Only the key
|
|
898
1085
|
is clickable (a click replays HINTS_CLICK). `help_key`: pressing `?` toggles
|
|
899
|
-
here; elsewhere `?` is typed or bound, and the toggle names Ctrl
|
|
1086
|
+
here; elsewhere `?` is typed or bound, and the toggle names Ctrl-/ (or Ctrl-G)."""
|
|
900
1087
|
text, width = help_corner_text(help_key)
|
|
901
1088
|
if not width: # the toggle is switched off
|
|
902
1089
|
if help_key:
|
|
@@ -947,25 +1134,34 @@ def box_lines(lines: list[str], width: int, height: int, title: str = "", right:
|
|
|
947
1134
|
h, v = ("┈", "┊") if dotted else ("─", "│")
|
|
948
1135
|
inner = max(1, width - 4) # "│ " + content + " │"
|
|
949
1136
|
pad = " " * ui.MARGIN_H
|
|
1137
|
+
# Nothing in a border is ever cut short. Short of room, whole parts give
|
|
1138
|
+
# way, least first: the title's " · " parts after its name, then the
|
|
1139
|
+
# right end's (a subtitle, a message), then the help toggle, then the name.
|
|
1140
|
+
toggle = next((t for t, _w in _toggle_variants() if t and right.endswith(t)), "") if right else ""
|
|
1141
|
+
sub = ui.strip_ansi(right[:len(right) - len(toggle)]).rstrip(" ─") if right else ""
|
|
1142
|
+
tparts, sparts = (title.split(" · ") if title else []), (sub.split(" · ") if sub else [])
|
|
1143
|
+
|
|
1144
|
+
def fits(tp: list, sp: list, tg: str) -> bool:
|
|
1145
|
+
name_w = ui.visual_len(" · ".join(tp)) + 2 if tp else 0
|
|
1146
|
+
r = " ─ ".join(x for x in (" · ".join(sp), tg) if x)
|
|
1147
|
+
return name_w + (ui.visual_len(r) + 3 if r else 0) <= width - 4
|
|
1148
|
+
|
|
1149
|
+
while not fits(tparts, sparts, toggle):
|
|
1150
|
+
if len(tparts) > 1:
|
|
1151
|
+
tparts = tparts[:-1]
|
|
1152
|
+
elif sparts:
|
|
1153
|
+
sparts = sparts[:-1]
|
|
1154
|
+
elif toggle:
|
|
1155
|
+
toggle = ""
|
|
1156
|
+
elif tparts:
|
|
1157
|
+
tparts = []
|
|
1158
|
+
else:
|
|
1159
|
+
break
|
|
1160
|
+
title = " · ".join(tparts)
|
|
950
1161
|
name = f" {title} " if title else ""
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
avail = width - 4 - 3
|
|
954
|
-
ln, lr = ui.visual_len(name), ui.visual_len(right)
|
|
955
|
-
if right and ln + lr > avail:
|
|
956
|
-
fit = avail - min(ln, max(avail // 2, avail - lr))
|
|
957
|
-
if lr > fit:
|
|
958
|
-
# The help toggle at its end stays whole (it's clicked, and found
|
|
959
|
-
# there by place_help_toggle); what's before it shortens.
|
|
960
|
-
toggle = next((t for t, _w in _toggle_variants() if t and right.endswith(t)), "")
|
|
961
|
-
head, tw = right[:len(right) - len(toggle)], ui.visual_len(toggle)
|
|
962
|
-
room_h = fit - tw
|
|
963
|
-
head = (_clip_ansi(head, room_h - 2) + f"{C.DIM}… {C.RESET}") if room_h > 4 else ""
|
|
964
|
-
right = head + toggle if tw <= fit else ""
|
|
1162
|
+
sub = " · ".join(sparts)
|
|
1163
|
+
right = (f"{C.DIM}{sub}{C.RESET}" if sub else "") + (f"{C.DIM} ─ {C.RESET}" if sub and toggle else "") + toggle
|
|
965
1164
|
tail = f" {right} ─" if right else ""
|
|
966
|
-
room = width - 4 - ui.visual_len(tail)
|
|
967
|
-
if ui.visual_len(name) > room:
|
|
968
|
-
name = (_clip_ansi(name, max(0, room - 2)) + "… ") if room > 2 else ""
|
|
969
1165
|
rule = max(1, width - 3 - ui.visual_len(name) - ui.visual_len(tail))
|
|
970
1166
|
shade = C.PRIMARY if focused else C.DIM
|
|
971
1167
|
tail = f"{C.DIM} {C.RESET}{right}{C.DIM} ─{C.RESET}" if right else ""
|
|
@@ -979,6 +1175,18 @@ def box_lines(lines: list[str], width: int, height: int, title: str = "", right:
|
|
|
979
1175
|
return [top, *mid, f"{pad}{C.DIM}╰{h * (width - 2)}╯{C.RESET}"]
|
|
980
1176
|
|
|
981
1177
|
|
|
1178
|
+
def whole_parts(text: str, room: int) -> str:
|
|
1179
|
+
"""`text` as it is if it fits in `room` columns, else with whole " · "
|
|
1180
|
+
parts dropped from the end till it does, else nothing: never cut short."""
|
|
1181
|
+
parts = text.split(" · ")
|
|
1182
|
+
while parts:
|
|
1183
|
+
shown = " · ".join(parts)
|
|
1184
|
+
if ui.visual_len(shown) <= room:
|
|
1185
|
+
return shown
|
|
1186
|
+
parts.pop()
|
|
1187
|
+
return ""
|
|
1188
|
+
|
|
1189
|
+
|
|
982
1190
|
def border_right(subtitle: str | None, help_key: bool | None) -> str:
|
|
983
1191
|
"""The right end of a box's top border: a dim `subtitle`, then the hints
|
|
984
1192
|
toggle (`help_key` None: none; else as help_corner_text has it)."""
|
|
@@ -1254,6 +1462,10 @@ def place_help_toggle(out: list, first_row: int, cells: dict, help_key: bool = F
|
|
|
1254
1462
|
if help_key:
|
|
1255
1463
|
cells['__help_key__'] = True
|
|
1256
1464
|
return
|
|
1465
|
+
if out and ui.strip_ansi(out[0]).lstrip().startswith("╭"):
|
|
1466
|
+
if help_key: # a box's border without it: it had no room (`?` still works)
|
|
1467
|
+
cells['__help_key__'] = True
|
|
1468
|
+
return
|
|
1257
1469
|
if out:
|
|
1258
1470
|
out[0] = add_help_corner(out[0], first_row, cells, help_key)
|
|
1259
1471
|
|
|
@@ -1532,6 +1744,8 @@ def footer_click_action(row: int, col: int) -> str | None:
|
|
|
1532
1744
|
def render_status_bar():
|
|
1533
1745
|
"""Redraw the bottom status bar in place, saving/restoring the cursor so
|
|
1534
1746
|
the text input caret doesn't move."""
|
|
1747
|
+
if _chrome_held(): # the miniplayer has the window
|
|
1748
|
+
return
|
|
1535
1749
|
rows = ui.get_terminal_height()
|
|
1536
1750
|
float_tick()
|
|
1537
1751
|
if rows <= 0 or not ui.chrome_fits(): # too short a window: its rows are the screen's
|
|
@@ -2050,8 +2264,9 @@ def _read_key_raw(fd: int) -> str:
|
|
|
2050
2264
|
btn, col, row = int(parts[0]), int(parts[1]), int(parts[2])
|
|
2051
2265
|
if c == 'm':
|
|
2052
2266
|
return f'MOUSE_RELEASE:{btn}:{row}:{col}'
|
|
2053
|
-
if btn
|
|
2054
|
-
|
|
2267
|
+
if btn in (64, 65):
|
|
2268
|
+
_wheel_at[0] = (row, col)
|
|
2269
|
+
return 'SCROLL_UP' if btn == 64 else 'SCROLL_DOWN'
|
|
2055
2270
|
if btn in (0, 1, 2): return f'MOUSE_CLICK:{btn}:{row}:{col}'
|
|
2056
2271
|
except ValueError:
|
|
2057
2272
|
pass
|
|
@@ -2184,6 +2399,8 @@ class _Widget:
|
|
|
2184
2399
|
status bar) are left exactly as they are instead of being wiped and
|
|
2185
2400
|
restamped on every keystroke.
|
|
2186
2401
|
"""
|
|
2402
|
+
if _cramped(): # a frame without its boxes: the miniplayer shows instead
|
|
2403
|
+
return
|
|
2187
2404
|
mv = ui.MARGIN_V
|
|
2188
2405
|
top = ui.top_margin()
|
|
2189
2406
|
rows = ui.get_terminal_height()
|
|
@@ -9,7 +9,7 @@ from typing import Any, Callable, Literal, overload
|
|
|
9
9
|
from backbone.prompt.core import (
|
|
10
10
|
_COLUMNS_MAX_WIDTH, _EDGE_MARGIN, _get_term_attrs, _set_raw, _restore_term_attrs,
|
|
11
11
|
_wait_for_keypress, _table_widths, _render_table_row, _clip_ansi, _norm, block_cursor,
|
|
12
|
-
_read_key, _visible_rows, _cols, _Widget, _hint_pin_target, screen_takeover_next,
|
|
12
|
+
_read_key, _visible_rows, _cols, _Widget, _hint_pin_target, screen_takeover_next, screen_backdrop,
|
|
13
13
|
Choice, Column, JumpTo, PanelTitle, Trail, border_right, box_fits, box_lines, boxed_frame, column_widths,
|
|
14
14
|
COL_GAP, _COL_MAIN_MIN, _COL_PREVIEW, _LIST_ROWS_MIN, _SHAPE_SAMPLE, _STRIP_SPLIT, strip_rows,
|
|
15
15
|
columns_shown, help_corner_text, panel_header, rounded_header, set_columns_shown, trail_lines,
|
|
@@ -83,14 +83,14 @@ keys.define("list", "Lists", [
|
|
|
83
83
|
("top", ("HOME",), "first row"),
|
|
84
84
|
("bottom", ("END",), "last row"),
|
|
85
85
|
("choose", ("ENTER", "RIGHT"), "choose the row"),
|
|
86
|
-
("toggle", ("SPACE",), "tick the row
|
|
87
|
-
("toggle_all", ("a", "A"), "tick or clear every row
|
|
86
|
+
("toggle", ("SPACE",), "tick the row, in lists with ticks"),
|
|
87
|
+
("toggle_all", ("a", "A"), "tick or clear every row, in lists with ticks"),
|
|
88
88
|
("back", ("ESC", "b", "LEFT"), "back"),
|
|
89
89
|
("quit", ("q", "Q"), "quit the app"),
|
|
90
90
|
("sections", ("/",), "a long list's sections, or the whole list"),
|
|
91
91
|
("row_options", ("o",), "everything you can do with the row"),
|
|
92
92
|
("list_options", ("O",), "everything you can do with the whole list"),
|
|
93
|
-
("columns", ("v",), "side columns on or off
|
|
93
|
+
("columns", ("v",), "side columns on or off, in lists that have them"),
|
|
94
94
|
])
|
|
95
95
|
# The live search lists: typing goes into the query, so their keys are the
|
|
96
96
|
# ones that can't be typed.
|
|
@@ -108,7 +108,7 @@ keys.define("confirm", "Yes/no questions", [
|
|
|
108
108
|
("yes", ("y", "Y"), "yes"),
|
|
109
109
|
("no", ("n", "N"), "no"),
|
|
110
110
|
("default", ("ENTER",), "the default answer"),
|
|
111
|
-
("back", ("ESC",), "back
|
|
111
|
+
("back", ("ESC",), "back, answering no"),
|
|
112
112
|
])
|
|
113
113
|
L = keys.label
|
|
114
114
|
|
|
@@ -558,7 +558,8 @@ def _select_flat(message: str, choices: list, *,
|
|
|
558
558
|
"""The preview's width moved a few columns, if need be, so the edge
|
|
559
559
|
between the browser's box and the preview's isn't under the showing
|
|
560
560
|
tab (whose outline opens into the box under it: ui.tab_notch); the
|
|
561
|
-
shorter way, as far as both keep their least.
|
|
561
|
+
shorter way, as far as both keep their least. Where neither way
|
|
562
|
+
fits, no preview (0): a box cut by the tab is no box."""
|
|
562
563
|
notch = ui.tab_notch() if pw else None
|
|
563
564
|
if not notch:
|
|
564
565
|
return pw
|
|
@@ -573,7 +574,7 @@ def _select_flat(message: str, choices: list, *,
|
|
|
573
574
|
move = right - (a - mh - 1) # the preview's corner at the tab's left side
|
|
574
575
|
if _cols() - pw - move - mh - 4 - trails_w >= _COL_MAIN_MIN[0]:
|
|
575
576
|
fits.append(pw + move)
|
|
576
|
-
return min(fits, key=lambda w: abs(w - pw)) if fits else
|
|
577
|
+
return min(fits, key=lambda w: abs(w - pw)) if fits else 0
|
|
577
578
|
|
|
578
579
|
def _contents_box(pv, width: int, height: int, title: bool = True) -> list:
|
|
579
580
|
"""What the highlighted row holds, in a box `width` × `height`."""
|
|
@@ -620,7 +621,8 @@ def _select_flat(message: str, choices: list, *,
|
|
|
620
621
|
def _browser_rows(body, shown, main_in, height, first, lead) -> list:
|
|
621
622
|
"""The column browser's rows inside its box: the levels above that fit
|
|
622
623
|
(`shown`: (trail, width)), then the list (`body`, its rows' margin
|
|
623
|
-
dropped), the levels' rows
|
|
624
|
+
dropped), the levels' rows from the top of the box (`lead` lines down:
|
|
625
|
+
the message), not moved by the list's "N above".
|
|
624
626
|
Records where each column landed (_col_geo), for clicks."""
|
|
625
627
|
sep = f" {C.DIM}│{C.RESET} "
|
|
626
628
|
x = 1 + ui.MARGIN_H + 2 # the screen column inside "│ "
|
|
@@ -814,7 +816,7 @@ def _select_flat(message: str, choices: list, *,
|
|
|
814
816
|
for line, col, rows, key, esc, *size in getattr(h_lines, 'pictures', ()):
|
|
815
817
|
w.pictures.append((1 + ui.top_margin() + line, col + 1, rows, key, esc, *size))
|
|
816
818
|
if boxed and (shown or pw or strip_h):
|
|
817
|
-
browser = box_lines(_browser_rows(out, shown, main_in, box_h - 2, first, len(pre)
|
|
819
|
+
browser = box_lines(_browser_rows(out, shown, main_in, box_h - 2, first, len(pre)),
|
|
818
820
|
browser_w, box_h, box_title, box_right)
|
|
819
821
|
if pw:
|
|
820
822
|
browser = [b + p for b, p in zip(browser, _preview_column(pview, pw, box_h, browser_w, first))]
|
|
@@ -842,17 +844,22 @@ def _select_flat(message: str, choices: list, *,
|
|
|
842
844
|
|
|
843
845
|
result = None
|
|
844
846
|
_sel_last_click: int | None = None
|
|
847
|
+
_drop_backdrop = lambda: None # noqa: E731 (none until the loop's first frame)
|
|
845
848
|
try:
|
|
846
849
|
_set_raw(fd)
|
|
847
850
|
enable_mouse()
|
|
848
851
|
screen_takeover_next() # paint over the previous screen, no flash
|
|
849
852
|
w.render(_lines())
|
|
850
853
|
|
|
854
|
+
def _relaid() -> None:
|
|
855
|
+
ui.clear_screen()
|
|
856
|
+
w.anchor_reset()
|
|
857
|
+
w.render(_lines())
|
|
858
|
+
_drop_backdrop = screen_backdrop(_relaid)
|
|
859
|
+
|
|
851
860
|
while True:
|
|
852
861
|
if ui.consume_resize():
|
|
853
|
-
|
|
854
|
-
w.anchor_reset()
|
|
855
|
-
w.render(_lines())
|
|
862
|
+
_relaid()
|
|
856
863
|
continue
|
|
857
864
|
|
|
858
865
|
if not _wait_for_keypress(0.05):
|
|
@@ -1101,6 +1108,7 @@ def _select_flat(message: str, choices: list, *,
|
|
|
1101
1108
|
w.render(_lines())
|
|
1102
1109
|
|
|
1103
1110
|
finally:
|
|
1111
|
+
_drop_backdrop()
|
|
1104
1112
|
disable_mouse()
|
|
1105
1113
|
_restore_term_attrs(fd, old)
|
|
1106
1114
|
w.clear()
|
|
@@ -1351,11 +1359,15 @@ def live_select(message: str, provider: Callable[[str], list], *,
|
|
|
1351
1359
|
screen_takeover_next() # paint over the previous screen, no flash
|
|
1352
1360
|
w.render(_lines())
|
|
1353
1361
|
|
|
1362
|
+
def _relaid() -> None:
|
|
1363
|
+
ui.clear_screen()
|
|
1364
|
+
w.anchor_reset()
|
|
1365
|
+
w.render(_lines())
|
|
1366
|
+
_drop_backdrop = screen_backdrop(_relaid)
|
|
1367
|
+
|
|
1354
1368
|
while True:
|
|
1355
1369
|
if ui.consume_resize():
|
|
1356
|
-
|
|
1357
|
-
w.anchor_reset()
|
|
1358
|
-
w.render(_lines())
|
|
1370
|
+
_relaid()
|
|
1359
1371
|
continue
|
|
1360
1372
|
if not _wait_for_keypress(0.05):
|
|
1361
1373
|
continue
|