691 lines
22 KiB
Python
691 lines
22 KiB
Python
# Urwid curses output wrapper.. the horror..
|
|
# Copyright (C) 2004-2011 Ian Ward
|
|
#
|
|
# This library is free software; you can redistribute it and/or
|
|
# modify it under the terms of the GNU Lesser General Public
|
|
# License as published by the Free Software Foundation; either
|
|
# version 2.1 of the License, or (at your option) any later version.
|
|
#
|
|
# This library is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
# Lesser General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU Lesser General Public
|
|
# License along with this library; if not, write to the Free Software
|
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
#
|
|
# Urwid web site: https://urwid.org/
|
|
|
|
|
|
"""
|
|
Curses-based UI implementation
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import curses
|
|
import sys
|
|
import typing
|
|
from contextlib import suppress
|
|
|
|
from urwid import util
|
|
|
|
from . import escape
|
|
from .common import UNPRINTABLE_TRANS_TABLE, AttrSpec, BaseScreen, RealTerminal
|
|
|
|
if typing.TYPE_CHECKING:
|
|
from typing_extensions import Literal
|
|
|
|
from urwid import Canvas
|
|
|
|
IS_WINDOWS = sys.platform == "win32"
|
|
|
|
# curses.KEY_RESIZE (sometimes not defined)
|
|
if IS_WINDOWS:
|
|
KEY_MOUSE = 539 # under Windows key mouse is different
|
|
KEY_RESIZE = 546
|
|
|
|
COLOR_CORRECTION: dict[int, int] = dict(
|
|
enumerate(
|
|
(
|
|
curses.COLOR_BLACK,
|
|
curses.COLOR_RED,
|
|
curses.COLOR_GREEN,
|
|
curses.COLOR_YELLOW,
|
|
curses.COLOR_BLUE,
|
|
curses.COLOR_MAGENTA,
|
|
curses.COLOR_CYAN,
|
|
curses.COLOR_WHITE,
|
|
)
|
|
)
|
|
)
|
|
|
|
def initscr():
|
|
import curses # noqa: I001 # pylint: disable=redefined-outer-name,reimported # special case for monkeypatch
|
|
|
|
import _curses
|
|
|
|
stdscr = _curses.initscr()
|
|
for key, value in _curses.__dict__.items():
|
|
if key[:4] == "ACS_" or key in {"LINES", "COLS"}:
|
|
setattr(curses, key, value)
|
|
|
|
return stdscr
|
|
|
|
curses.initscr = initscr
|
|
|
|
else:
|
|
KEY_MOUSE = 409 # curses.KEY_MOUSE
|
|
KEY_RESIZE = 410
|
|
|
|
COLOR_CORRECTION = {}
|
|
|
|
_curses_colours = { # pylint: disable=consider-using-namedtuple-or-dataclass # historic test/debug data
|
|
"default": (-1, 0),
|
|
"black": (curses.COLOR_BLACK, 0),
|
|
"dark red": (curses.COLOR_RED, 0),
|
|
"dark green": (curses.COLOR_GREEN, 0),
|
|
"brown": (curses.COLOR_YELLOW, 0),
|
|
"dark blue": (curses.COLOR_BLUE, 0),
|
|
"dark magenta": (curses.COLOR_MAGENTA, 0),
|
|
"dark cyan": (curses.COLOR_CYAN, 0),
|
|
"light gray": (curses.COLOR_WHITE, 0),
|
|
"dark gray": (curses.COLOR_BLACK, 1),
|
|
"light red": (curses.COLOR_RED, 1),
|
|
"light green": (curses.COLOR_GREEN, 1),
|
|
"yellow": (curses.COLOR_YELLOW, 1),
|
|
"light blue": (curses.COLOR_BLUE, 1),
|
|
"light magenta": (curses.COLOR_MAGENTA, 1),
|
|
"light cyan": (curses.COLOR_CYAN, 1),
|
|
"white": (curses.COLOR_WHITE, 1),
|
|
}
|
|
|
|
|
|
class Screen(BaseScreen, RealTerminal):
|
|
def __init__(self) -> None:
|
|
super().__init__()
|
|
self.curses_pairs = [(None, None)] # Can't be sure what pair 0 will default to
|
|
self.palette = {}
|
|
self.has_color = False
|
|
self.s = None
|
|
self.cursor_state = None
|
|
self.prev_input_resize = 0
|
|
self.set_input_timeouts()
|
|
self.last_bstate = 0
|
|
self._mouse_tracking_enabled = False
|
|
|
|
self.register_palette_entry(None, "default", "default")
|
|
|
|
def set_mouse_tracking(self, enable: bool = True) -> None:
|
|
"""
|
|
Enable mouse tracking.
|
|
|
|
After calling this function get_input will include mouse
|
|
click events along with keystrokes.
|
|
"""
|
|
enable = bool(enable) # noqa: FURB123,RUF100
|
|
if enable == self._mouse_tracking_enabled:
|
|
return
|
|
|
|
if enable:
|
|
curses.mousemask(
|
|
0
|
|
| curses.BUTTON1_PRESSED
|
|
| curses.BUTTON1_RELEASED
|
|
| curses.BUTTON2_PRESSED
|
|
| curses.BUTTON2_RELEASED
|
|
| curses.BUTTON3_PRESSED
|
|
| curses.BUTTON3_RELEASED
|
|
| curses.BUTTON4_PRESSED
|
|
| curses.BUTTON4_RELEASED
|
|
| curses.BUTTON1_DOUBLE_CLICKED
|
|
| curses.BUTTON1_TRIPLE_CLICKED
|
|
| curses.BUTTON2_DOUBLE_CLICKED
|
|
| curses.BUTTON2_TRIPLE_CLICKED
|
|
| curses.BUTTON3_DOUBLE_CLICKED
|
|
| curses.BUTTON3_TRIPLE_CLICKED
|
|
| curses.BUTTON4_DOUBLE_CLICKED
|
|
| curses.BUTTON4_TRIPLE_CLICKED
|
|
| curses.BUTTON_SHIFT
|
|
| curses.BUTTON_ALT
|
|
| curses.BUTTON_CTRL
|
|
)
|
|
else:
|
|
raise NotImplementedError()
|
|
|
|
self._mouse_tracking_enabled = enable
|
|
|
|
def _start(self) -> None:
|
|
"""
|
|
Initialize the screen and input mode.
|
|
"""
|
|
self.s = curses.initscr()
|
|
self.has_color = curses.has_colors()
|
|
if self.has_color:
|
|
curses.start_color()
|
|
if curses.COLORS < 8:
|
|
# not colourful enough
|
|
self.has_color = False
|
|
if self.has_color:
|
|
try:
|
|
curses.use_default_colors()
|
|
self.has_default_colors = True
|
|
except curses.error:
|
|
self.has_default_colors = False
|
|
self._setup_colour_pairs()
|
|
curses.noecho()
|
|
curses.meta(True)
|
|
curses.halfdelay(10) # use set_input_timeouts to adjust
|
|
self.s.keypad(False)
|
|
|
|
if not self._signal_keys_set:
|
|
self._old_signal_keys = self.tty_signal_keys()
|
|
|
|
super()._start()
|
|
|
|
if IS_WINDOWS:
|
|
# halfdelay() seems unnecessary and causes everything to slow down a lot.
|
|
curses.nocbreak() # exits halfdelay mode
|
|
# keypad(1) is needed, or we get no special keys (cursor keys, etc.)
|
|
self.s.keypad(True)
|
|
|
|
def _stop(self) -> None:
|
|
"""
|
|
Restore the screen.
|
|
"""
|
|
curses.echo()
|
|
self._curs_set(1)
|
|
with suppress(curses.error):
|
|
curses.endwin()
|
|
# don't block original error with curses error
|
|
|
|
if self._old_signal_keys:
|
|
self.tty_signal_keys(*self._old_signal_keys)
|
|
|
|
super()._stop()
|
|
|
|
def _setup_colour_pairs(self) -> None:
|
|
"""
|
|
Initialize all 63 color pairs based on the term:
|
|
bg * 8 + 7 - fg
|
|
So to get a color, we just need to use that term and get the right color
|
|
pair number.
|
|
"""
|
|
if not self.has_color:
|
|
return
|
|
|
|
if IS_WINDOWS:
|
|
self.has_default_colors = False
|
|
|
|
for fg in range(8):
|
|
for bg in range(8):
|
|
# leave out white on black
|
|
if fg == curses.COLOR_WHITE and bg == curses.COLOR_BLACK:
|
|
continue
|
|
|
|
curses.init_pair(bg * 8 + 7 - fg, COLOR_CORRECTION.get(fg, fg), COLOR_CORRECTION.get(bg, bg))
|
|
|
|
def _curs_set(self, x: int):
|
|
if self.cursor_state in {"fixed", x}:
|
|
return
|
|
try:
|
|
curses.curs_set(x)
|
|
self.cursor_state = x
|
|
except curses.error:
|
|
self.cursor_state = "fixed"
|
|
|
|
def _clear(self) -> None:
|
|
self.s.clear()
|
|
self.s.refresh()
|
|
|
|
def _getch(self, wait_tenths: int | None) -> int:
|
|
if wait_tenths == 0:
|
|
return self._getch_nodelay()
|
|
|
|
if not IS_WINDOWS:
|
|
if wait_tenths is None:
|
|
curses.cbreak()
|
|
else:
|
|
curses.halfdelay(wait_tenths)
|
|
|
|
self.s.nodelay(False)
|
|
return self.s.getch()
|
|
|
|
def _getch_nodelay(self) -> int:
|
|
self.s.nodelay(True)
|
|
|
|
if not IS_WINDOWS:
|
|
while True:
|
|
# this call fails sometimes, but seems to work when I try again
|
|
with suppress(curses.error):
|
|
curses.cbreak()
|
|
break
|
|
|
|
return self.s.getch()
|
|
|
|
def set_input_timeouts(
|
|
self,
|
|
max_wait: float | None = None,
|
|
complete_wait: float = 0.1,
|
|
resize_wait: float = 0.1,
|
|
):
|
|
"""
|
|
Set the get_input timeout values. All values have a granularity
|
|
of 0.1s, ie. any value between 0.15 and 0.05 will be treated as
|
|
0.1 and any value less than 0.05 will be treated as 0. The
|
|
maximum timeout value for this module is 25.5 seconds.
|
|
|
|
max_wait -- amount of time in seconds to wait for input when
|
|
there is no input pending, wait forever if None
|
|
complete_wait -- amount of time in seconds to wait when
|
|
get_input detects an incomplete escape sequence at the
|
|
end of the available input
|
|
resize_wait -- amount of time in seconds to wait for more input
|
|
after receiving two screen resize requests in a row to
|
|
stop urwid from consuming 100% cpu during a gradual
|
|
window resize operation
|
|
"""
|
|
|
|
def convert_to_tenths(s):
|
|
if s is None:
|
|
return None
|
|
return int((s + 0.05) * 10)
|
|
|
|
self.max_tenths = convert_to_tenths(max_wait)
|
|
self.complete_tenths = convert_to_tenths(complete_wait)
|
|
self.resize_tenths = convert_to_tenths(resize_wait)
|
|
|
|
@typing.overload
|
|
def get_input(self, raw_keys: Literal[False]) -> list[str]: ...
|
|
|
|
@typing.overload
|
|
def get_input(self, raw_keys: Literal[True]) -> tuple[list[str], list[int]]: ...
|
|
|
|
def get_input(self, raw_keys: bool = False) -> list[str] | tuple[list[str], list[int]]:
|
|
"""Return pending input as a list.
|
|
|
|
raw_keys -- return raw keycodes as well as translated versions
|
|
|
|
This function will immediately return all the input since the
|
|
last time it was called. If there is no input pending it will
|
|
wait before returning an empty list. The wait time may be
|
|
configured with the set_input_timeouts function.
|
|
|
|
If raw_keys is False (default) this function will return a list
|
|
of keys pressed. If raw_keys is True this function will return
|
|
a ( keys pressed, raw keycodes ) tuple instead.
|
|
|
|
Examples of keys returned:
|
|
|
|
* ASCII printable characters: " ", "a", "0", "A", "-", "/"
|
|
* ASCII control characters: "tab", "enter"
|
|
* Escape sequences: "up", "page up", "home", "insert", "f1"
|
|
* Key combinations: "shift f1", "meta a", "ctrl b"
|
|
* Window events: "window resize"
|
|
|
|
When a narrow encoding is not enabled:
|
|
|
|
* "Extended ASCII" characters: "\\xa1", "\\xb2", "\\xfe"
|
|
|
|
When a wide encoding is enabled:
|
|
|
|
* Double-byte characters: "\\xa1\\xea", "\\xb2\\xd4"
|
|
|
|
When utf8 encoding is enabled:
|
|
|
|
* Unicode characters: u"\\u00a5", u'\\u253c"
|
|
|
|
Examples of mouse events returned:
|
|
|
|
* Mouse button press: ('mouse press', 1, 15, 13),
|
|
('meta mouse press', 2, 17, 23)
|
|
* Mouse button release: ('mouse release', 0, 18, 13),
|
|
('ctrl mouse release', 0, 17, 23)
|
|
"""
|
|
if not self._started:
|
|
raise RuntimeError
|
|
|
|
keys, raw = self._get_input(self.max_tenths)
|
|
|
|
# Avoid pegging CPU at 100% when slowly resizing, and work
|
|
# around a bug with some braindead curses implementations that
|
|
# return "no key" between "window resize" commands
|
|
if keys == ["window resize"] and self.prev_input_resize:
|
|
for _ in range(2):
|
|
new_keys, new_raw = self._get_input(self.resize_tenths)
|
|
raw += new_raw
|
|
if new_keys and new_keys != ["window resize"]:
|
|
if "window resize" in new_keys:
|
|
keys = new_keys
|
|
else:
|
|
keys.extend(new_keys)
|
|
break
|
|
|
|
if keys == ["window resize"]:
|
|
self.prev_input_resize = 2
|
|
elif self.prev_input_resize == 2 and not keys:
|
|
self.prev_input_resize = 1
|
|
else:
|
|
self.prev_input_resize = 0
|
|
|
|
if raw_keys:
|
|
return keys, raw
|
|
return keys
|
|
|
|
def _get_input(self, wait_tenths: int | None) -> tuple[list[str], list[int]]:
|
|
# this works around a strange curses bug with window resizing
|
|
# not being reported correctly with repeated calls to this
|
|
# function without a doupdate call in between
|
|
curses.doupdate()
|
|
|
|
key = self._getch(wait_tenths)
|
|
resize = False
|
|
raw = []
|
|
keys = []
|
|
|
|
while key >= 0:
|
|
raw.append(key)
|
|
if key == KEY_RESIZE:
|
|
resize = True
|
|
elif key == KEY_MOUSE:
|
|
keys += self._encode_mouse_event()
|
|
else:
|
|
keys.append(key)
|
|
key = self._getch_nodelay()
|
|
|
|
processed = []
|
|
|
|
try:
|
|
while keys:
|
|
run, keys = escape.process_keyqueue(keys, True)
|
|
processed += run
|
|
except escape.MoreInputRequired:
|
|
key = self._getch(self.complete_tenths)
|
|
while key >= 0:
|
|
raw.append(key)
|
|
if key == KEY_RESIZE:
|
|
resize = True
|
|
elif key == KEY_MOUSE:
|
|
keys += self._encode_mouse_event()
|
|
else:
|
|
keys.append(key)
|
|
key = self._getch_nodelay()
|
|
while keys:
|
|
run, keys = escape.process_keyqueue(keys, False)
|
|
processed += run
|
|
|
|
if resize:
|
|
processed.append("window resize")
|
|
|
|
return processed, raw
|
|
|
|
def _encode_mouse_event(self) -> list[int]:
|
|
# convert to escape sequence
|
|
last_state = next_state = self.last_bstate
|
|
(_id, x, y, _z, bstate) = curses.getmouse()
|
|
|
|
mod = 0
|
|
if bstate & curses.BUTTON_SHIFT:
|
|
mod |= 4
|
|
if bstate & curses.BUTTON_ALT:
|
|
mod |= 8
|
|
if bstate & curses.BUTTON_CTRL:
|
|
mod |= 16
|
|
|
|
result = []
|
|
|
|
def append_button(b: int) -> None:
|
|
b |= mod
|
|
result.extend([27, ord("["), ord("M"), b + 32, x + 33, y + 33])
|
|
|
|
if bstate & curses.BUTTON1_PRESSED and last_state & 1 == 0:
|
|
append_button(0)
|
|
next_state |= 1
|
|
if bstate & curses.BUTTON2_PRESSED and last_state & 2 == 0:
|
|
append_button(1)
|
|
next_state |= 2
|
|
if bstate & curses.BUTTON3_PRESSED and last_state & 4 == 0:
|
|
append_button(2)
|
|
next_state |= 4
|
|
if bstate & curses.BUTTON4_PRESSED and last_state & 8 == 0:
|
|
append_button(64)
|
|
next_state |= 8
|
|
if bstate & curses.BUTTON1_RELEASED and last_state & 1:
|
|
append_button(0 + escape.MOUSE_RELEASE_FLAG)
|
|
next_state &= ~1
|
|
if bstate & curses.BUTTON2_RELEASED and last_state & 2:
|
|
append_button(1 + escape.MOUSE_RELEASE_FLAG)
|
|
next_state &= ~2
|
|
if bstate & curses.BUTTON3_RELEASED and last_state & 4:
|
|
append_button(2 + escape.MOUSE_RELEASE_FLAG)
|
|
next_state &= ~4
|
|
if bstate & curses.BUTTON4_RELEASED and last_state & 8:
|
|
append_button(64 + escape.MOUSE_RELEASE_FLAG)
|
|
next_state &= ~8
|
|
|
|
if bstate & curses.BUTTON1_DOUBLE_CLICKED:
|
|
append_button(0 + escape.MOUSE_MULTIPLE_CLICK_FLAG)
|
|
if bstate & curses.BUTTON2_DOUBLE_CLICKED:
|
|
append_button(1 + escape.MOUSE_MULTIPLE_CLICK_FLAG)
|
|
if bstate & curses.BUTTON3_DOUBLE_CLICKED:
|
|
append_button(2 + escape.MOUSE_MULTIPLE_CLICK_FLAG)
|
|
if bstate & curses.BUTTON4_DOUBLE_CLICKED:
|
|
append_button(64 + escape.MOUSE_MULTIPLE_CLICK_FLAG)
|
|
|
|
if bstate & curses.BUTTON1_TRIPLE_CLICKED:
|
|
append_button(0 + escape.MOUSE_MULTIPLE_CLICK_FLAG * 2)
|
|
if bstate & curses.BUTTON2_TRIPLE_CLICKED:
|
|
append_button(1 + escape.MOUSE_MULTIPLE_CLICK_FLAG * 2)
|
|
if bstate & curses.BUTTON3_TRIPLE_CLICKED:
|
|
append_button(2 + escape.MOUSE_MULTIPLE_CLICK_FLAG * 2)
|
|
if bstate & curses.BUTTON4_TRIPLE_CLICKED:
|
|
append_button(64 + escape.MOUSE_MULTIPLE_CLICK_FLAG * 2)
|
|
|
|
self.last_bstate = next_state
|
|
return result
|
|
|
|
def _dbg_instr(self): # messy input string (intended for debugging)
|
|
curses.echo()
|
|
self.s.nodelay(0)
|
|
curses.halfdelay(100)
|
|
string = self.s.getstr()
|
|
curses.noecho()
|
|
return string
|
|
|
|
def _dbg_out(self, string) -> None: # messy output function (intended for debugging)
|
|
self.s.clrtoeol()
|
|
self.s.addstr(string)
|
|
self.s.refresh()
|
|
self._curs_set(1)
|
|
|
|
def _dbg_query(self, question): # messy query (intended for debugging)
|
|
self._dbg_out(question)
|
|
return self._dbg_instr()
|
|
|
|
def _dbg_refresh(self) -> None:
|
|
self.s.refresh()
|
|
|
|
def get_cols_rows(self) -> tuple[int, int]:
|
|
"""Return the terminal dimensions (num columns, num rows)."""
|
|
rows, cols = self.s.getmaxyx()
|
|
return cols, rows
|
|
|
|
def _setattr(self, a):
|
|
if a is None:
|
|
self.s.attrset(0)
|
|
return
|
|
if not isinstance(a, AttrSpec):
|
|
p = self._palette.get(a, (AttrSpec("default", "default"),))
|
|
a = p[0]
|
|
|
|
if self.has_color:
|
|
if a.foreground_basic:
|
|
if a.foreground_number >= 8:
|
|
fg = a.foreground_number - 8
|
|
else:
|
|
fg = a.foreground_number
|
|
else:
|
|
fg = 7
|
|
|
|
if a.background_basic:
|
|
bg = a.background_number
|
|
else:
|
|
bg = 0
|
|
|
|
attr = curses.color_pair(bg * 8 + 7 - fg)
|
|
else:
|
|
attr = 0
|
|
|
|
if a.bold:
|
|
attr |= curses.A_BOLD
|
|
if a.standout:
|
|
attr |= curses.A_STANDOUT
|
|
if a.underline:
|
|
attr |= curses.A_UNDERLINE
|
|
if a.blink:
|
|
attr |= curses.A_BLINK
|
|
|
|
self.s.attrset(attr)
|
|
|
|
def draw_screen(self, size: tuple[int, int], canvas: Canvas) -> None:
|
|
"""Paint screen with rendered canvas."""
|
|
|
|
logger = self.logger.getChild("draw_screen")
|
|
|
|
if not self._started:
|
|
raise RuntimeError
|
|
|
|
_cols, rows = size
|
|
|
|
if canvas.rows() != rows:
|
|
raise ValueError("canvas size and passed size don't match")
|
|
|
|
logger.debug(f"Drawing screen with size {size!r}")
|
|
|
|
y = -1
|
|
for row in canvas.content():
|
|
y += 1
|
|
try:
|
|
self.s.move(y, 0)
|
|
except curses.error:
|
|
# terminal shrunk?
|
|
# move failed so stop rendering.
|
|
return
|
|
|
|
first = True
|
|
lasta = None
|
|
|
|
for nr, (a, cs, seg) in enumerate(row):
|
|
if cs != "U":
|
|
seg = seg.translate(UNPRINTABLE_TRANS_TABLE) # noqa: PLW2901
|
|
if not isinstance(seg, bytes):
|
|
raise TypeError(seg)
|
|
|
|
if first or lasta != a:
|
|
self._setattr(a)
|
|
lasta = a
|
|
try:
|
|
if cs in {"0", "U"}:
|
|
for segment in seg:
|
|
self.s.addch(0x400000 + segment)
|
|
else:
|
|
if cs is not None:
|
|
raise ValueError(f"cs not in ('0', 'U' ,'None'): {cs!r}")
|
|
if not isinstance(seg, bytes):
|
|
raise TypeError(seg)
|
|
self.s.addstr(seg.decode(util.get_encoding()))
|
|
except curses.error:
|
|
# it's ok to get out of the
|
|
# screen on the lower right
|
|
if y != rows - 1 or nr != len(row) - 1:
|
|
# perhaps screen size changed
|
|
# quietly abort.
|
|
return
|
|
|
|
if canvas.cursor is not None:
|
|
x, y = canvas.cursor
|
|
self._curs_set(1)
|
|
with suppress(curses.error):
|
|
self.s.move(y, x)
|
|
else:
|
|
self._curs_set(0)
|
|
self.s.move(0, 0)
|
|
|
|
self.s.refresh()
|
|
self.keep_cache_alive_link = canvas
|
|
|
|
def clear(self) -> None:
|
|
"""
|
|
Force the screen to be completely repainted on the next call to draw_screen().
|
|
"""
|
|
self.s.clear()
|
|
|
|
|
|
class _test:
|
|
def __init__(self):
|
|
self.ui = Screen()
|
|
self.l = sorted(_curses_colours)
|
|
|
|
for c in self.l:
|
|
self.ui.register_palette(
|
|
[
|
|
(f"{c} on black", c, "black", "underline"),
|
|
(f"{c} on dark blue", c, "dark blue", "bold"),
|
|
(f"{c} on light gray", c, "light gray", "standout"),
|
|
]
|
|
)
|
|
|
|
with self.ui.start():
|
|
self.run()
|
|
|
|
def run(self) -> None:
|
|
class FakeRender:
|
|
pass
|
|
|
|
r = FakeRender()
|
|
text = [f" has_color = {self.ui.has_color!r}", ""]
|
|
attr = [[], []]
|
|
r.coords = {}
|
|
r.cursor = None
|
|
|
|
for c in self.l:
|
|
t = ""
|
|
a = []
|
|
for p in f"{c} on black", f"{c} on dark blue", f"{c} on light gray":
|
|
a.append((p, 27))
|
|
t += (p + 27 * " ")[:27]
|
|
text.append(t)
|
|
attr.append(a)
|
|
|
|
text += ["", "return values from get_input(): (q exits)", ""]
|
|
attr += [[], [], []]
|
|
cols, rows = self.ui.get_cols_rows()
|
|
keys = None
|
|
while keys != ["q"]:
|
|
r.text = ([t.ljust(cols) for t in text] + [""] * rows)[:rows]
|
|
r.attr = (attr + [[] for _ in range(rows)])[:rows]
|
|
self.ui.draw_screen((cols, rows), r)
|
|
keys, raw = self.ui.get_input(raw_keys=True)
|
|
if "window resize" in keys:
|
|
cols, rows = self.ui.get_cols_rows()
|
|
if not keys:
|
|
continue
|
|
t = ""
|
|
a = []
|
|
for k in keys:
|
|
if isinstance(k, str):
|
|
k = k.encode(util.get_encoding()) # noqa: PLW2901
|
|
|
|
t += f"'{k}' "
|
|
a += [(None, 1), ("yellow on dark blue", len(k)), (None, 2)]
|
|
|
|
text.append(f"{t}: {raw!r}")
|
|
attr.append(a)
|
|
text = text[-rows:]
|
|
attr = attr[-rows:]
|
|
|
|
|
|
if __name__ == "__main__":
|
|
_test()
|