urwid 2.6.0.post0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of urwid might be problematic. Click here for more details.

Files changed (75) hide show
  1. urwid/__init__.py +333 -0
  2. urwid/canvas.py +1413 -0
  3. urwid/command_map.py +137 -0
  4. urwid/container.py +59 -0
  5. urwid/decoration.py +65 -0
  6. urwid/display/__init__.py +97 -0
  7. urwid/display/_posix_raw_display.py +413 -0
  8. urwid/display/_raw_display_base.py +914 -0
  9. urwid/display/_web.css +12 -0
  10. urwid/display/_web.js +462 -0
  11. urwid/display/_win32.py +171 -0
  12. urwid/display/_win32_raw_display.py +269 -0
  13. urwid/display/common.py +1219 -0
  14. urwid/display/curses.py +690 -0
  15. urwid/display/escape.py +624 -0
  16. urwid/display/html_fragment.py +251 -0
  17. urwid/display/lcd.py +518 -0
  18. urwid/display/raw.py +37 -0
  19. urwid/display/web.py +636 -0
  20. urwid/event_loop/__init__.py +55 -0
  21. urwid/event_loop/abstract_loop.py +175 -0
  22. urwid/event_loop/asyncio_loop.py +231 -0
  23. urwid/event_loop/glib_loop.py +294 -0
  24. urwid/event_loop/main_loop.py +721 -0
  25. urwid/event_loop/select_loop.py +230 -0
  26. urwid/event_loop/tornado_loop.py +206 -0
  27. urwid/event_loop/trio_loop.py +302 -0
  28. urwid/event_loop/twisted_loop.py +269 -0
  29. urwid/event_loop/zmq_loop.py +275 -0
  30. urwid/font.py +695 -0
  31. urwid/graphics.py +96 -0
  32. urwid/highlight.css +19 -0
  33. urwid/listbox.py +1899 -0
  34. urwid/monitored_list.py +522 -0
  35. urwid/numedit.py +376 -0
  36. urwid/signals.py +330 -0
  37. urwid/split_repr.py +130 -0
  38. urwid/str_util.py +358 -0
  39. urwid/text_layout.py +632 -0
  40. urwid/treetools.py +515 -0
  41. urwid/util.py +557 -0
  42. urwid/version.py +16 -0
  43. urwid/vterm.py +1806 -0
  44. urwid/widget/__init__.py +181 -0
  45. urwid/widget/attr_map.py +161 -0
  46. urwid/widget/attr_wrap.py +140 -0
  47. urwid/widget/bar_graph.py +649 -0
  48. urwid/widget/big_text.py +77 -0
  49. urwid/widget/box_adapter.py +126 -0
  50. urwid/widget/columns.py +1145 -0
  51. urwid/widget/constants.py +574 -0
  52. urwid/widget/container.py +227 -0
  53. urwid/widget/divider.py +110 -0
  54. urwid/widget/edit.py +718 -0
  55. urwid/widget/filler.py +403 -0
  56. urwid/widget/frame.py +539 -0
  57. urwid/widget/grid_flow.py +539 -0
  58. urwid/widget/line_box.py +194 -0
  59. urwid/widget/overlay.py +829 -0
  60. urwid/widget/padding.py +597 -0
  61. urwid/widget/pile.py +971 -0
  62. urwid/widget/popup.py +170 -0
  63. urwid/widget/progress_bar.py +141 -0
  64. urwid/widget/scrollable.py +597 -0
  65. urwid/widget/solid_fill.py +44 -0
  66. urwid/widget/text.py +354 -0
  67. urwid/widget/widget.py +852 -0
  68. urwid/widget/widget_decoration.py +166 -0
  69. urwid/widget/wimp.py +792 -0
  70. urwid/wimp.py +23 -0
  71. urwid-2.6.0.post0.dist-info/COPYING +504 -0
  72. urwid-2.6.0.post0.dist-info/METADATA +332 -0
  73. urwid-2.6.0.post0.dist-info/RECORD +75 -0
  74. urwid-2.6.0.post0.dist-info/WHEEL +5 -0
  75. urwid-2.6.0.post0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,690 @@
1
+ # Urwid curses output wrapper.. the horror..
2
+ # Copyright (C) 2004-2011 Ian Ward
3
+ #
4
+ # This library is free software; you can redistribute it and/or
5
+ # modify it under the terms of the GNU Lesser General Public
6
+ # License as published by the Free Software Foundation; either
7
+ # version 2.1 of the License, or (at your option) any later version.
8
+ #
9
+ # This library is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12
+ # Lesser General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU Lesser General Public
15
+ # License along with this library; if not, write to the Free Software
16
+ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17
+ #
18
+ # Urwid web site: https://urwid.org/
19
+
20
+
21
+ """
22
+ Curses-based UI implementation
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import curses
28
+ import sys
29
+ import typing
30
+ from contextlib import suppress
31
+
32
+ from urwid import util
33
+
34
+ from . import escape
35
+ from .common import UNPRINTABLE_TRANS_TABLE, AttrSpec, BaseScreen, RealTerminal
36
+
37
+ if typing.TYPE_CHECKING:
38
+ from typing_extensions import Literal
39
+
40
+ from urwid import Canvas
41
+
42
+ IS_WINDOWS = sys.platform == "win32"
43
+
44
+ # curses.KEY_RESIZE (sometimes not defined)
45
+ if IS_WINDOWS:
46
+ KEY_MOUSE = 539 # under Windows key mouse is different
47
+ KEY_RESIZE = 546
48
+
49
+ COLOR_CORRECTION: dict[int, int] = dict(
50
+ enumerate(
51
+ (
52
+ curses.COLOR_BLACK,
53
+ curses.COLOR_RED,
54
+ curses.COLOR_GREEN,
55
+ curses.COLOR_YELLOW,
56
+ curses.COLOR_BLUE,
57
+ curses.COLOR_MAGENTA,
58
+ curses.COLOR_CYAN,
59
+ curses.COLOR_WHITE,
60
+ )
61
+ )
62
+ )
63
+
64
+ def initscr():
65
+ import curses # pylint: disable=redefined-outer-name,reimported # special case for monkeypatch
66
+
67
+ import _curses
68
+
69
+ stdscr = _curses.initscr()
70
+ for key, value in _curses.__dict__.items():
71
+ if key[:4] == "ACS_" or key in {"LINES", "COLS"}:
72
+ setattr(curses, key, value)
73
+
74
+ return stdscr
75
+
76
+ curses.initscr = initscr
77
+
78
+ else:
79
+ KEY_MOUSE = 409 # curses.KEY_MOUSE
80
+ KEY_RESIZE = 410
81
+
82
+ COLOR_CORRECTION = {}
83
+
84
+ _curses_colours = { # pylint: disable=consider-using-namedtuple-or-dataclass # historic test/debug data
85
+ "default": (-1, 0),
86
+ "black": (curses.COLOR_BLACK, 0),
87
+ "dark red": (curses.COLOR_RED, 0),
88
+ "dark green": (curses.COLOR_GREEN, 0),
89
+ "brown": (curses.COLOR_YELLOW, 0),
90
+ "dark blue": (curses.COLOR_BLUE, 0),
91
+ "dark magenta": (curses.COLOR_MAGENTA, 0),
92
+ "dark cyan": (curses.COLOR_CYAN, 0),
93
+ "light gray": (curses.COLOR_WHITE, 0),
94
+ "dark gray": (curses.COLOR_BLACK, 1),
95
+ "light red": (curses.COLOR_RED, 1),
96
+ "light green": (curses.COLOR_GREEN, 1),
97
+ "yellow": (curses.COLOR_YELLOW, 1),
98
+ "light blue": (curses.COLOR_BLUE, 1),
99
+ "light magenta": (curses.COLOR_MAGENTA, 1),
100
+ "light cyan": (curses.COLOR_CYAN, 1),
101
+ "white": (curses.COLOR_WHITE, 1),
102
+ }
103
+
104
+
105
+ class Screen(BaseScreen, RealTerminal):
106
+ def __init__(self):
107
+ super().__init__()
108
+ self.curses_pairs = [(None, None)] # Can't be sure what pair 0 will default to
109
+ self.palette = {}
110
+ self.has_color = False
111
+ self.s = None
112
+ self.cursor_state = None
113
+ self.prev_input_resize = 0
114
+ self.set_input_timeouts()
115
+ self.last_bstate = 0
116
+ self._mouse_tracking_enabled = False
117
+
118
+ self.register_palette_entry(None, "default", "default")
119
+
120
+ def set_mouse_tracking(self, enable: bool = True) -> None:
121
+ """
122
+ Enable mouse tracking.
123
+
124
+ After calling this function get_input will include mouse
125
+ click events along with keystrokes.
126
+ """
127
+ enable = bool(enable) # noqa: FURB123,RUF100
128
+ if enable == self._mouse_tracking_enabled:
129
+ return
130
+
131
+ if enable:
132
+ curses.mousemask(
133
+ 0
134
+ | curses.BUTTON1_PRESSED
135
+ | curses.BUTTON1_RELEASED
136
+ | curses.BUTTON2_PRESSED
137
+ | curses.BUTTON2_RELEASED
138
+ | curses.BUTTON3_PRESSED
139
+ | curses.BUTTON3_RELEASED
140
+ | curses.BUTTON4_PRESSED
141
+ | curses.BUTTON4_RELEASED
142
+ | curses.BUTTON1_DOUBLE_CLICKED
143
+ | curses.BUTTON1_TRIPLE_CLICKED
144
+ | curses.BUTTON2_DOUBLE_CLICKED
145
+ | curses.BUTTON2_TRIPLE_CLICKED
146
+ | curses.BUTTON3_DOUBLE_CLICKED
147
+ | curses.BUTTON3_TRIPLE_CLICKED
148
+ | curses.BUTTON4_DOUBLE_CLICKED
149
+ | curses.BUTTON4_TRIPLE_CLICKED
150
+ | curses.BUTTON_SHIFT
151
+ | curses.BUTTON_ALT
152
+ | curses.BUTTON_CTRL
153
+ )
154
+ else:
155
+ raise NotImplementedError()
156
+
157
+ self._mouse_tracking_enabled = enable
158
+
159
+ def _start(self) -> None:
160
+ """
161
+ Initialize the screen and input mode.
162
+ """
163
+ self.s = curses.initscr()
164
+ self.has_color = curses.has_colors()
165
+ if self.has_color:
166
+ curses.start_color()
167
+ if curses.COLORS < 8:
168
+ # not colourful enough
169
+ self.has_color = False
170
+ if self.has_color:
171
+ try:
172
+ curses.use_default_colors()
173
+ self.has_default_colors = True
174
+ except curses.error:
175
+ self.has_default_colors = False
176
+ self._setup_colour_pairs()
177
+ curses.noecho()
178
+ curses.meta(True)
179
+ curses.halfdelay(10) # use set_input_timeouts to adjust
180
+ self.s.keypad(False)
181
+
182
+ if not self._signal_keys_set:
183
+ self._old_signal_keys = self.tty_signal_keys()
184
+
185
+ super()._start()
186
+
187
+ if IS_WINDOWS:
188
+ # halfdelay() seems unnecessary and causes everything to slow down a lot.
189
+ curses.nocbreak() # exits halfdelay mode
190
+ # keypad(1) is needed, or we get no special keys (cursor keys, etc.)
191
+ self.s.keypad(True)
192
+
193
+ def _stop(self) -> None:
194
+ """
195
+ Restore the screen.
196
+ """
197
+ curses.echo()
198
+ self._curs_set(1)
199
+ with suppress(curses.error):
200
+ curses.endwin()
201
+ # don't block original error with curses error
202
+
203
+ if self._old_signal_keys:
204
+ self.tty_signal_keys(*self._old_signal_keys)
205
+
206
+ super()._stop()
207
+
208
+ def _setup_colour_pairs(self) -> None:
209
+ """
210
+ Initialize all 63 color pairs based on the term:
211
+ bg * 8 + 7 - fg
212
+ So to get a color, we just need to use that term and get the right color
213
+ pair number.
214
+ """
215
+ if not self.has_color:
216
+ return
217
+
218
+ if IS_WINDOWS:
219
+ self.has_default_colors = False
220
+
221
+ for fg in range(8):
222
+ for bg in range(8):
223
+ # leave out white on black
224
+ if fg == curses.COLOR_WHITE and bg == curses.COLOR_BLACK:
225
+ continue
226
+
227
+ curses.init_pair(bg * 8 + 7 - fg, COLOR_CORRECTION.get(fg, fg), COLOR_CORRECTION.get(bg, bg))
228
+
229
+ def _curs_set(self, x: int):
230
+ if self.cursor_state in {"fixed", x}:
231
+ return
232
+ try:
233
+ curses.curs_set(x)
234
+ self.cursor_state = x
235
+ except curses.error:
236
+ self.cursor_state = "fixed"
237
+
238
+ def _clear(self) -> None:
239
+ self.s.clear()
240
+ self.s.refresh()
241
+
242
+ def _getch(self, wait_tenths: int | None) -> int:
243
+ if wait_tenths == 0:
244
+ return self._getch_nodelay()
245
+
246
+ if not IS_WINDOWS:
247
+ if wait_tenths is None:
248
+ curses.cbreak()
249
+ else:
250
+ curses.halfdelay(wait_tenths)
251
+
252
+ self.s.nodelay(False)
253
+ return self.s.getch()
254
+
255
+ def _getch_nodelay(self) -> int:
256
+ self.s.nodelay(True)
257
+
258
+ if not IS_WINDOWS:
259
+ while True:
260
+ # this call fails sometimes, but seems to work when I try again
261
+ with suppress(curses.error):
262
+ curses.cbreak()
263
+ break
264
+
265
+ return self.s.getch()
266
+
267
+ def set_input_timeouts(
268
+ self,
269
+ max_wait: float | None = None,
270
+ complete_wait: float = 0.1,
271
+ resize_wait: float = 0.1,
272
+ ):
273
+ """
274
+ Set the get_input timeout values. All values have a granularity
275
+ of 0.1s, ie. any value between 0.15 and 0.05 will be treated as
276
+ 0.1 and any value less than 0.05 will be treated as 0. The
277
+ maximum timeout value for this module is 25.5 seconds.
278
+
279
+ max_wait -- amount of time in seconds to wait for input when
280
+ there is no input pending, wait forever if None
281
+ complete_wait -- amount of time in seconds to wait when
282
+ get_input detects an incomplete escape sequence at the
283
+ end of the available input
284
+ resize_wait -- amount of time in seconds to wait for more input
285
+ after receiving two screen resize requests in a row to
286
+ stop urwid from consuming 100% cpu during a gradual
287
+ window resize operation
288
+ """
289
+
290
+ def convert_to_tenths(s):
291
+ if s is None:
292
+ return None
293
+ return int((s + 0.05) * 10)
294
+
295
+ self.max_tenths = convert_to_tenths(max_wait)
296
+ self.complete_tenths = convert_to_tenths(complete_wait)
297
+ self.resize_tenths = convert_to_tenths(resize_wait)
298
+
299
+ @typing.overload
300
+ def get_input(self, raw_keys: Literal[False]) -> list[str]: ...
301
+
302
+ @typing.overload
303
+ def get_input(self, raw_keys: Literal[True]) -> tuple[list[str], list[int]]: ...
304
+
305
+ def get_input(self, raw_keys: bool = False) -> list[str] | tuple[list[str], list[int]]:
306
+ """Return pending input as a list.
307
+
308
+ raw_keys -- return raw keycodes as well as translated versions
309
+
310
+ This function will immediately return all the input since the
311
+ last time it was called. If there is no input pending it will
312
+ wait before returning an empty list. The wait time may be
313
+ configured with the set_input_timeouts function.
314
+
315
+ If raw_keys is False (default) this function will return a list
316
+ of keys pressed. If raw_keys is True this function will return
317
+ a ( keys pressed, raw keycodes ) tuple instead.
318
+
319
+ Examples of keys returned:
320
+
321
+ * ASCII printable characters: " ", "a", "0", "A", "-", "/"
322
+ * ASCII control characters: "tab", "enter"
323
+ * Escape sequences: "up", "page up", "home", "insert", "f1"
324
+ * Key combinations: "shift f1", "meta a", "ctrl b"
325
+ * Window events: "window resize"
326
+
327
+ When a narrow encoding is not enabled:
328
+
329
+ * "Extended ASCII" characters: "\\xa1", "\\xb2", "\\xfe"
330
+
331
+ When a wide encoding is enabled:
332
+
333
+ * Double-byte characters: "\\xa1\\xea", "\\xb2\\xd4"
334
+
335
+ When utf8 encoding is enabled:
336
+
337
+ * Unicode characters: u"\\u00a5", u'\\u253c"
338
+
339
+ Examples of mouse events returned:
340
+
341
+ * Mouse button press: ('mouse press', 1, 15, 13),
342
+ ('meta mouse press', 2, 17, 23)
343
+ * Mouse button release: ('mouse release', 0, 18, 13),
344
+ ('ctrl mouse release', 0, 17, 23)
345
+ """
346
+ if not self._started:
347
+ raise RuntimeError
348
+
349
+ keys, raw = self._get_input(self.max_tenths)
350
+
351
+ # Avoid pegging CPU at 100% when slowly resizing, and work
352
+ # around a bug with some braindead curses implementations that
353
+ # return "no key" between "window resize" commands
354
+ if keys == ["window resize"] and self.prev_input_resize:
355
+ for _ in range(2):
356
+ new_keys, new_raw = self._get_input(self.resize_tenths)
357
+ raw += new_raw
358
+ if new_keys and new_keys != ["window resize"]:
359
+ if "window resize" in new_keys:
360
+ keys = new_keys
361
+ else:
362
+ keys.extend(new_keys)
363
+ break
364
+
365
+ if keys == ["window resize"]:
366
+ self.prev_input_resize = 2
367
+ elif self.prev_input_resize == 2 and not keys:
368
+ self.prev_input_resize = 1
369
+ else:
370
+ self.prev_input_resize = 0
371
+
372
+ if raw_keys:
373
+ return keys, raw
374
+ return keys
375
+
376
+ def _get_input(self, wait_tenths: int | None) -> tuple[list[str], list[int]]:
377
+ # this works around a strange curses bug with window resizing
378
+ # not being reported correctly with repeated calls to this
379
+ # function without a doupdate call in between
380
+ curses.doupdate()
381
+
382
+ key = self._getch(wait_tenths)
383
+ resize = False
384
+ raw = []
385
+ keys = []
386
+
387
+ while key >= 0:
388
+ raw.append(key)
389
+ if key == KEY_RESIZE:
390
+ resize = True
391
+ elif key == KEY_MOUSE:
392
+ keys += self._encode_mouse_event()
393
+ else:
394
+ keys.append(key)
395
+ key = self._getch_nodelay()
396
+
397
+ processed = []
398
+
399
+ try:
400
+ while keys:
401
+ run, keys = escape.process_keyqueue(keys, True)
402
+ processed += run
403
+ except escape.MoreInputRequired:
404
+ key = self._getch(self.complete_tenths)
405
+ while key >= 0:
406
+ raw.append(key)
407
+ if key == KEY_RESIZE:
408
+ resize = True
409
+ elif key == KEY_MOUSE:
410
+ keys += self._encode_mouse_event()
411
+ else:
412
+ keys.append(key)
413
+ key = self._getch_nodelay()
414
+ while keys:
415
+ run, keys = escape.process_keyqueue(keys, False)
416
+ processed += run
417
+
418
+ if resize:
419
+ processed.append("window resize")
420
+
421
+ return processed, raw
422
+
423
+ def _encode_mouse_event(self) -> list[int]:
424
+ # convert to escape sequence
425
+ last_state = next_state = self.last_bstate
426
+ (_id, x, y, _z, bstate) = curses.getmouse()
427
+
428
+ mod = 0
429
+ if bstate & curses.BUTTON_SHIFT:
430
+ mod |= 4
431
+ if bstate & curses.BUTTON_ALT:
432
+ mod |= 8
433
+ if bstate & curses.BUTTON_CTRL:
434
+ mod |= 16
435
+
436
+ result = []
437
+
438
+ def append_button(b: int) -> None:
439
+ b |= mod
440
+ result.extend([27, ord("["), ord("M"), b + 32, x + 33, y + 33])
441
+
442
+ if bstate & curses.BUTTON1_PRESSED and last_state & 1 == 0:
443
+ append_button(0)
444
+ next_state |= 1
445
+ if bstate & curses.BUTTON2_PRESSED and last_state & 2 == 0:
446
+ append_button(1)
447
+ next_state |= 2
448
+ if bstate & curses.BUTTON3_PRESSED and last_state & 4 == 0:
449
+ append_button(2)
450
+ next_state |= 4
451
+ if bstate & curses.BUTTON4_PRESSED and last_state & 8 == 0:
452
+ append_button(64)
453
+ next_state |= 8
454
+ if bstate & curses.BUTTON1_RELEASED and last_state & 1:
455
+ append_button(0 + escape.MOUSE_RELEASE_FLAG)
456
+ next_state &= ~1
457
+ if bstate & curses.BUTTON2_RELEASED and last_state & 2:
458
+ append_button(1 + escape.MOUSE_RELEASE_FLAG)
459
+ next_state &= ~2
460
+ if bstate & curses.BUTTON3_RELEASED and last_state & 4:
461
+ append_button(2 + escape.MOUSE_RELEASE_FLAG)
462
+ next_state &= ~4
463
+ if bstate & curses.BUTTON4_RELEASED and last_state & 8:
464
+ append_button(64 + escape.MOUSE_RELEASE_FLAG)
465
+ next_state &= ~8
466
+
467
+ if bstate & curses.BUTTON1_DOUBLE_CLICKED:
468
+ append_button(0 + escape.MOUSE_MULTIPLE_CLICK_FLAG)
469
+ if bstate & curses.BUTTON2_DOUBLE_CLICKED:
470
+ append_button(1 + escape.MOUSE_MULTIPLE_CLICK_FLAG)
471
+ if bstate & curses.BUTTON3_DOUBLE_CLICKED:
472
+ append_button(2 + escape.MOUSE_MULTIPLE_CLICK_FLAG)
473
+ if bstate & curses.BUTTON4_DOUBLE_CLICKED:
474
+ append_button(64 + escape.MOUSE_MULTIPLE_CLICK_FLAG)
475
+
476
+ if bstate & curses.BUTTON1_TRIPLE_CLICKED:
477
+ append_button(0 + escape.MOUSE_MULTIPLE_CLICK_FLAG * 2)
478
+ if bstate & curses.BUTTON2_TRIPLE_CLICKED:
479
+ append_button(1 + escape.MOUSE_MULTIPLE_CLICK_FLAG * 2)
480
+ if bstate & curses.BUTTON3_TRIPLE_CLICKED:
481
+ append_button(2 + escape.MOUSE_MULTIPLE_CLICK_FLAG * 2)
482
+ if bstate & curses.BUTTON4_TRIPLE_CLICKED:
483
+ append_button(64 + escape.MOUSE_MULTIPLE_CLICK_FLAG * 2)
484
+
485
+ self.last_bstate = next_state
486
+ return result
487
+
488
+ def _dbg_instr(self): # messy input string (intended for debugging)
489
+ curses.echo()
490
+ self.s.nodelay(0)
491
+ curses.halfdelay(100)
492
+ string = self.s.getstr()
493
+ curses.noecho()
494
+ return string
495
+
496
+ def _dbg_out(self, string) -> None: # messy output function (intended for debugging)
497
+ self.s.clrtoeol()
498
+ self.s.addstr(string)
499
+ self.s.refresh()
500
+ self._curs_set(1)
501
+
502
+ def _dbg_query(self, question): # messy query (intended for debugging)
503
+ self._dbg_out(question)
504
+ return self._dbg_instr()
505
+
506
+ def _dbg_refresh(self) -> None:
507
+ self.s.refresh()
508
+
509
+ def get_cols_rows(self) -> tuple[int, int]:
510
+ """Return the terminal dimensions (num columns, num rows)."""
511
+ rows, cols = self.s.getmaxyx()
512
+ return cols, rows
513
+
514
+ def _setattr(self, a):
515
+ if a is None:
516
+ self.s.attrset(0)
517
+ return
518
+ if not isinstance(a, AttrSpec):
519
+ p = self._palette.get(a, (AttrSpec("default", "default"),))
520
+ a = p[0]
521
+
522
+ if self.has_color:
523
+ if a.foreground_basic:
524
+ if a.foreground_number >= 8:
525
+ fg = a.foreground_number - 8
526
+ else:
527
+ fg = a.foreground_number
528
+ else:
529
+ fg = 7
530
+
531
+ if a.background_basic:
532
+ bg = a.background_number
533
+ else:
534
+ bg = 0
535
+
536
+ attr = curses.color_pair(bg * 8 + 7 - fg)
537
+ else:
538
+ attr = 0
539
+
540
+ if a.bold:
541
+ attr |= curses.A_BOLD
542
+ if a.standout:
543
+ attr |= curses.A_STANDOUT
544
+ if a.underline:
545
+ attr |= curses.A_UNDERLINE
546
+ if a.blink:
547
+ attr |= curses.A_BLINK
548
+
549
+ self.s.attrset(attr)
550
+
551
+ def draw_screen(self, size: tuple[int, int], canvas: Canvas):
552
+ """Paint screen with rendered canvas."""
553
+
554
+ logger = self.logger.getChild("draw_screen")
555
+
556
+ if not self._started:
557
+ raise RuntimeError
558
+
559
+ _cols, rows = size
560
+
561
+ if canvas.rows() != rows:
562
+ raise ValueError("canvas size and passed size don't match")
563
+
564
+ logger.debug(f"Drawing screen with size {size!r}")
565
+
566
+ y = -1
567
+ for row in canvas.content():
568
+ y += 1
569
+ try:
570
+ self.s.move(y, 0)
571
+ except curses.error:
572
+ # terminal shrunk?
573
+ # move failed so stop rendering.
574
+ return
575
+
576
+ first = True
577
+ lasta = None
578
+
579
+ for nr, (a, cs, seg) in enumerate(row):
580
+ if cs != "U":
581
+ seg = seg.translate(UNPRINTABLE_TRANS_TABLE) # noqa: PLW2901
582
+ if not isinstance(seg, bytes):
583
+ raise TypeError(seg)
584
+
585
+ if first or lasta != a:
586
+ self._setattr(a)
587
+ lasta = a
588
+ try:
589
+ if cs in {"0", "U"}:
590
+ for segment in seg:
591
+ self.s.addch(0x400000 + segment)
592
+ else:
593
+ if cs is not None:
594
+ raise ValueError(f"cs not in ('0', 'U' ,'None'): {cs!r}")
595
+ if not isinstance(seg, bytes):
596
+ raise TypeError(seg)
597
+ self.s.addstr(seg.decode(util.get_encoding()))
598
+ except curses.error:
599
+ # it's ok to get out of the
600
+ # screen on the lower right
601
+ if y != rows - 1 or nr != len(row) - 1:
602
+ # perhaps screen size changed
603
+ # quietly abort.
604
+ return
605
+
606
+ if canvas.cursor is not None:
607
+ x, y = canvas.cursor
608
+ self._curs_set(1)
609
+ with suppress(curses.error):
610
+ self.s.move(y, x)
611
+ else:
612
+ self._curs_set(0)
613
+ self.s.move(0, 0)
614
+
615
+ self.s.refresh()
616
+ self.keep_cache_alive_link = canvas
617
+
618
+ def clear(self) -> None:
619
+ """
620
+ Force the screen to be completely repainted on the next call to draw_screen().
621
+ """
622
+ self.s.clear()
623
+
624
+
625
+ class _test:
626
+ def __init__(self):
627
+ self.ui = Screen()
628
+ self.l = sorted(_curses_colours)
629
+
630
+ for c in self.l:
631
+ self.ui.register_palette(
632
+ [
633
+ (f"{c} on black", c, "black", "underline"),
634
+ (f"{c} on dark blue", c, "dark blue", "bold"),
635
+ (f"{c} on light gray", c, "light gray", "standout"),
636
+ ]
637
+ )
638
+
639
+ with self.ui.start():
640
+ self.run()
641
+
642
+ def run(self) -> None:
643
+ class FakeRender:
644
+ pass
645
+
646
+ r = FakeRender()
647
+ text = [f" has_color = {self.ui.has_color!r}", ""]
648
+ attr = [[], []]
649
+ r.coords = {}
650
+ r.cursor = None
651
+
652
+ for c in self.l:
653
+ t = ""
654
+ a = []
655
+ for p in f"{c} on black", f"{c} on dark blue", f"{c} on light gray":
656
+ a.append((p, 27))
657
+ t += (p + 27 * " ")[:27]
658
+ text.append(t)
659
+ attr.append(a)
660
+
661
+ text += ["", "return values from get_input(): (q exits)", ""]
662
+ attr += [[], [], []]
663
+ cols, rows = self.ui.get_cols_rows()
664
+ keys = None
665
+ while keys != ["q"]:
666
+ r.text = ([t.ljust(cols) for t in text] + [""] * rows)[:rows]
667
+ r.attr = (attr + [[] for _ in range(rows)])[:rows]
668
+ self.ui.draw_screen((cols, rows), r)
669
+ keys, raw = self.ui.get_input(raw_keys=True) # pylint: disable=unpacking-non-sequence
670
+ if "window resize" in keys:
671
+ cols, rows = self.ui.get_cols_rows()
672
+ if not keys:
673
+ continue
674
+ t = ""
675
+ a = []
676
+ for k in keys:
677
+ if isinstance(k, str):
678
+ k = k.encode(util.get_encoding()) # noqa: PLW2901
679
+
680
+ t += f"'{k}' "
681
+ a += [(None, 1), ("yellow on dark blue", len(k)), (None, 2)]
682
+
683
+ text.append(f"{t}: {raw!r}")
684
+ attr.append(a)
685
+ text = text[-rows:]
686
+ attr = attr[-rows:]
687
+
688
+
689
+ if __name__ == "__main__":
690
+ _test()