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,1219 @@
1
+ # Urwid common display code
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
+ from __future__ import annotations
22
+
23
+ import abc
24
+ import logging
25
+ import os
26
+ import sys
27
+ import typing
28
+ import warnings
29
+
30
+ from urwid import signals
31
+ from urwid.util import StoppingContext, int_scale
32
+
33
+ if typing.TYPE_CHECKING:
34
+ from collections.abc import Iterable, Sequence
35
+
36
+ from typing_extensions import Literal, Self
37
+
38
+ from urwid import Canvas
39
+
40
+ IS_WINDOWS = sys.platform == "win32"
41
+
42
+ if not IS_WINDOWS:
43
+ import termios
44
+
45
+ # for replacing unprintable bytes with '?'
46
+ UNPRINTABLE_TRANS_TABLE = b"?" * 32 + bytes(range(32, 256))
47
+
48
+
49
+ # signals sent by BaseScreen
50
+ UPDATE_PALETTE_ENTRY = "update palette entry"
51
+ INPUT_DESCRIPTORS_CHANGED = "input descriptors changed"
52
+
53
+
54
+ # AttrSpec internal values
55
+ _BASIC_START = 0 # first index of basic color aliases
56
+ _CUBE_START = 16 # first index of color cube
57
+ _CUBE_SIZE_256 = 6 # one side of the color cube
58
+ _GRAY_SIZE_256 = 24
59
+ _GRAY_START_256 = _CUBE_SIZE_256**3 + _CUBE_START
60
+ _CUBE_WHITE_256 = _GRAY_START_256 - 1
61
+ _CUBE_SIZE_88 = 4
62
+ _GRAY_SIZE_88 = 8
63
+ _GRAY_START_88 = _CUBE_SIZE_88**3 + _CUBE_START
64
+ _CUBE_WHITE_88 = _GRAY_START_88 - 1
65
+ _CUBE_BLACK = _CUBE_START
66
+
67
+ # values copied from xterm 256colres.h:
68
+ _CUBE_STEPS_256 = [0x00, 0x5F, 0x87, 0xAF, 0xD7, 0xFF]
69
+ _GRAY_STEPS_256 = [
70
+ 0x08,
71
+ 0x12,
72
+ 0x1C,
73
+ 0x26,
74
+ 0x30,
75
+ 0x3A,
76
+ 0x44,
77
+ 0x4E,
78
+ 0x58,
79
+ 0x62,
80
+ 0x6C,
81
+ 0x76,
82
+ 0x80,
83
+ 0x84,
84
+ 0x94,
85
+ 0x9E,
86
+ 0xA8,
87
+ 0xB2,
88
+ 0xBC,
89
+ 0xC6,
90
+ 0xD0,
91
+ 0xDA,
92
+ 0xE4,
93
+ 0xEE,
94
+ ]
95
+ # values copied from xterm 88colres.h:
96
+ _CUBE_STEPS_88 = [0x00, 0x8B, 0xCD, 0xFF]
97
+ _GRAY_STEPS_88 = [0x2E, 0x5C, 0x73, 0x8B, 0xA2, 0xB9, 0xD0, 0xE7]
98
+ # values copied from X11/rgb.txt and XTerm-col.ad:
99
+ _BASIC_COLOR_VALUES = [
100
+ (0, 0, 0),
101
+ (205, 0, 0),
102
+ (0, 205, 0),
103
+ (205, 205, 0),
104
+ (0, 0, 238),
105
+ (205, 0, 205),
106
+ (0, 205, 205),
107
+ (229, 229, 229),
108
+ (127, 127, 127),
109
+ (255, 0, 0),
110
+ (0, 255, 0),
111
+ (255, 255, 0),
112
+ (0x5C, 0x5C, 0xFF),
113
+ (255, 0, 255),
114
+ (0, 255, 255),
115
+ (255, 255, 255),
116
+ ]
117
+
118
+ _COLOR_VALUES_256 = (
119
+ _BASIC_COLOR_VALUES
120
+ + [(r, g, b) for r in _CUBE_STEPS_256 for g in _CUBE_STEPS_256 for b in _CUBE_STEPS_256]
121
+ + [(gr, gr, gr) for gr in _GRAY_STEPS_256]
122
+ )
123
+ _COLOR_VALUES_88 = (
124
+ _BASIC_COLOR_VALUES
125
+ + [(r, g, b) for r in _CUBE_STEPS_88 for g in _CUBE_STEPS_88 for b in _CUBE_STEPS_88]
126
+ + [(gr, gr, gr) for gr in _GRAY_STEPS_88]
127
+ )
128
+
129
+ if len(_COLOR_VALUES_256) != 256:
130
+ raise RuntimeError(_COLOR_VALUES_256)
131
+ if len(_COLOR_VALUES_88) != 88:
132
+ raise RuntimeError(_COLOR_VALUES_88)
133
+
134
+ # fmt: off
135
+ _FG_COLOR_MASK = 0x000000ffffff
136
+ _BG_COLOR_MASK = 0xffffff000000
137
+ _FG_BASIC_COLOR = 0x1000000000000
138
+ _FG_HIGH_COLOR = 0x2000000000000
139
+ _FG_TRUE_COLOR = 0x4000000000000
140
+ _BG_BASIC_COLOR = 0x8000000000000
141
+ _BG_HIGH_COLOR = 0x10000000000000
142
+ _BG_TRUE_COLOR = 0x20000000000000
143
+ _BG_SHIFT = 24
144
+ _HIGH_88_COLOR = 0x40000000000000
145
+ _HIGH_TRUE_COLOR = 0x80000000000000
146
+ _STANDOUT = 0x100000000000000
147
+ _UNDERLINE = 0x200000000000000
148
+ _BOLD = 0x400000000000000
149
+ _BLINK = 0x800000000000000
150
+ _ITALICS = 0x1000000000000000
151
+ _STRIKETHROUGH = 0x2000000000000000
152
+ # fmt: on
153
+
154
+ _FG_MASK = (
155
+ _FG_COLOR_MASK
156
+ | _FG_BASIC_COLOR
157
+ | _FG_HIGH_COLOR
158
+ | _STANDOUT
159
+ | _UNDERLINE
160
+ | _BLINK
161
+ | _BOLD
162
+ | _ITALICS
163
+ | _STRIKETHROUGH
164
+ )
165
+ _BG_MASK = _BG_COLOR_MASK | _BG_BASIC_COLOR | _BG_HIGH_COLOR
166
+
167
+ DEFAULT = "default"
168
+ BLACK = "black"
169
+ DARK_RED = "dark red"
170
+ DARK_GREEN = "dark green"
171
+ BROWN = "brown"
172
+ DARK_BLUE = "dark blue"
173
+ DARK_MAGENTA = "dark magenta"
174
+ DARK_CYAN = "dark cyan"
175
+ LIGHT_GRAY = "light gray"
176
+ DARK_GRAY = "dark gray"
177
+ LIGHT_RED = "light red"
178
+ LIGHT_GREEN = "light green"
179
+ YELLOW = "yellow"
180
+ LIGHT_BLUE = "light blue"
181
+ LIGHT_MAGENTA = "light magenta"
182
+ LIGHT_CYAN = "light cyan"
183
+ WHITE = "white"
184
+
185
+ _BASIC_COLORS = [
186
+ BLACK,
187
+ DARK_RED,
188
+ DARK_GREEN,
189
+ BROWN,
190
+ DARK_BLUE,
191
+ DARK_MAGENTA,
192
+ DARK_CYAN,
193
+ LIGHT_GRAY,
194
+ DARK_GRAY,
195
+ LIGHT_RED,
196
+ LIGHT_GREEN,
197
+ YELLOW,
198
+ LIGHT_BLUE,
199
+ LIGHT_MAGENTA,
200
+ LIGHT_CYAN,
201
+ WHITE,
202
+ ]
203
+
204
+ _ATTRIBUTES = {
205
+ "bold": _BOLD,
206
+ "italics": _ITALICS,
207
+ "underline": _UNDERLINE,
208
+ "blink": _BLINK,
209
+ "standout": _STANDOUT,
210
+ "strikethrough": _STRIKETHROUGH,
211
+ }
212
+
213
+
214
+ def _value_lookup_table(values: Sequence[int], size: int) -> list[int]:
215
+ """
216
+ Generate a lookup table for finding the closest item in values.
217
+ Lookup returns (index into values)+1
218
+
219
+ values -- list of values in ascending order, all < size
220
+ size -- size of lookup table and maximum value
221
+
222
+ >>> _value_lookup_table([0, 7, 9], 10)
223
+ [0, 0, 0, 0, 1, 1, 1, 1, 2, 2]
224
+ """
225
+
226
+ middle_values = [0] + [(values[i] + values[i + 1] + 1) // 2 for i in range(len(values) - 1)] + [size]
227
+ lookup_table = []
228
+ for i in range(len(middle_values) - 1):
229
+ count = middle_values[i + 1] - middle_values[i]
230
+ lookup_table.extend([i] * count)
231
+ return lookup_table
232
+
233
+
234
+ _CUBE_256_LOOKUP = _value_lookup_table(_CUBE_STEPS_256, 256)
235
+ _GRAY_256_LOOKUP = _value_lookup_table([0, *_GRAY_STEPS_256, 255], 256)
236
+ _CUBE_88_LOOKUP = _value_lookup_table(_CUBE_STEPS_88, 256)
237
+ _GRAY_88_LOOKUP = _value_lookup_table([0, *_GRAY_STEPS_88, 255], 256)
238
+
239
+ # convert steps to values that will be used by string versions of the colors
240
+ # 1 hex digit for rgb and 0..100 for grayscale
241
+ _CUBE_STEPS_256_16 = [int_scale(n, 0x100, 0x10) for n in _CUBE_STEPS_256]
242
+ _GRAY_STEPS_256_101 = [int_scale(n, 0x100, 101) for n in _GRAY_STEPS_256]
243
+ _CUBE_STEPS_88_16 = [int_scale(n, 0x100, 0x10) for n in _CUBE_STEPS_88]
244
+ _GRAY_STEPS_88_101 = [int_scale(n, 0x100, 101) for n in _GRAY_STEPS_88]
245
+
246
+ # create lookup tables for 1 hex digit rgb and 0..100 for grayscale values
247
+ _CUBE_256_LOOKUP_16 = [_CUBE_256_LOOKUP[int_scale(n, 16, 0x100)] for n in range(16)]
248
+ _GRAY_256_LOOKUP_101 = [_GRAY_256_LOOKUP[int_scale(n, 101, 0x100)] for n in range(101)]
249
+ _CUBE_88_LOOKUP_16 = [_CUBE_88_LOOKUP[int_scale(n, 16, 0x100)] for n in range(16)]
250
+ _GRAY_88_LOOKUP_101 = [_GRAY_88_LOOKUP[int_scale(n, 101, 0x100)] for n in range(101)]
251
+
252
+
253
+ # The functions _gray_num_256() and _gray_num_88() do not include the gray
254
+ # values from the color cube so that the gray steps are an even width.
255
+ # The color cube grays are available by using the rgb functions. Pure
256
+ # white and black are taken from the color cube, since the gray range does
257
+ # not include them, and the basic colors are more likely to have been
258
+ # customized by an end-user.
259
+
260
+
261
+ def _gray_num_256(gnum: int) -> int:
262
+ """Return ths color number for gray number gnum.
263
+
264
+ Color cube black and white are returned for 0 and 25 respectively
265
+ since those values aren't included in the gray scale.
266
+
267
+ """
268
+ # grays start from index 1
269
+ gnum -= 1
270
+
271
+ if gnum < 0:
272
+ return _CUBE_BLACK
273
+ if gnum >= _GRAY_SIZE_256:
274
+ return _CUBE_WHITE_256
275
+ return _GRAY_START_256 + gnum
276
+
277
+
278
+ def _gray_num_88(gnum: int) -> int:
279
+ """Return ths color number for gray number gnum.
280
+
281
+ Color cube black and white are returned for 0 and 9 respectively
282
+ since those values aren't included in the gray scale.
283
+
284
+ """
285
+ # gnums start from index 1
286
+ gnum -= 1
287
+
288
+ if gnum < 0:
289
+ return _CUBE_BLACK
290
+ if gnum >= _GRAY_SIZE_88:
291
+ return _CUBE_WHITE_88
292
+ return _GRAY_START_88 + gnum
293
+
294
+
295
+ def _color_desc_true(num: int) -> str:
296
+ return f"#{num:06x}"
297
+
298
+
299
+ def _color_desc_256(num: int) -> str:
300
+ """
301
+ Return a string description of color number num.
302
+ 0..15 -> 'h0'..'h15' basic colors (as high-colors)
303
+ 16..231 -> '#000'..'#fff' color cube colors
304
+ 232..255 -> 'g3'..'g93' grays
305
+
306
+ >>> _color_desc_256(15)
307
+ 'h15'
308
+ >>> _color_desc_256(16)
309
+ '#000'
310
+ >>> _color_desc_256(17)
311
+ '#006'
312
+ >>> _color_desc_256(230)
313
+ '#ffd'
314
+ >>> _color_desc_256(233)
315
+ 'g7'
316
+ >>> _color_desc_256(234)
317
+ 'g11'
318
+
319
+ """
320
+ if not 0 <= num < 256:
321
+ raise ValueError(num)
322
+ if num < _CUBE_START:
323
+ return f"h{num:d}"
324
+ if num < _GRAY_START_256:
325
+ num -= _CUBE_START
326
+ b, num = num % _CUBE_SIZE_256, num // _CUBE_SIZE_256
327
+ g, num = num % _CUBE_SIZE_256, num // _CUBE_SIZE_256
328
+ r = num % _CUBE_SIZE_256
329
+ return f"#{_CUBE_STEPS_256_16[r]:x}{_CUBE_STEPS_256_16[g]:x}{_CUBE_STEPS_256_16[b]:x}"
330
+ return f"g{_GRAY_STEPS_256_101[num - _GRAY_START_256]:d}"
331
+
332
+
333
+ def _color_desc_88(num: int) -> str:
334
+ """
335
+ Return a string description of color number num.
336
+ 0..15 -> 'h0'..'h15' basic colors (as high-colors)
337
+ 16..79 -> '#000'..'#fff' color cube colors
338
+ 80..87 -> 'g18'..'g90' grays
339
+
340
+ >>> _color_desc_88(15)
341
+ 'h15'
342
+ >>> _color_desc_88(16)
343
+ '#000'
344
+ >>> _color_desc_88(17)
345
+ '#008'
346
+ >>> _color_desc_88(78)
347
+ '#ffc'
348
+ >>> _color_desc_88(81)
349
+ 'g36'
350
+ >>> _color_desc_88(82)
351
+ 'g45'
352
+
353
+ """
354
+ if not 0 < num < 88:
355
+ raise ValueError(num)
356
+ if num < _CUBE_START:
357
+ return f"h{num:d}"
358
+ if num < _GRAY_START_88:
359
+ num -= _CUBE_START
360
+ b, num = num % _CUBE_SIZE_88, num // _CUBE_SIZE_88
361
+ g, r = num % _CUBE_SIZE_88, num // _CUBE_SIZE_88
362
+ return f"#{_CUBE_STEPS_88_16[r]:x}{_CUBE_STEPS_88_16[g]:x}{_CUBE_STEPS_88_16[b]:x}"
363
+ return f"g{_GRAY_STEPS_88_101[num - _GRAY_START_88]:d}"
364
+
365
+
366
+ def _parse_color_true(desc: str) -> int | None:
367
+ c = _parse_color_256(desc)
368
+ if c is not None:
369
+ (r, g, b) = _COLOR_VALUES_256[c]
370
+ return (r << 16) + (g << 8) + b
371
+
372
+ if not desc.startswith("#"):
373
+ return None
374
+ if len(desc) == 7:
375
+ h = desc[1:]
376
+ return int(h, 16)
377
+ if len(desc) == 4:
378
+ h = f"0x{desc[1]}0{desc[2]}0{desc[3]}"
379
+ return int(h, 16)
380
+ return None
381
+
382
+
383
+ def _parse_color_256(desc: str) -> int | None:
384
+ """
385
+ Return a color number for the description desc.
386
+ 'h0'..'h255' -> 0..255 actual color number
387
+ '#000'..'#fff' -> 16..231 color cube colors
388
+ 'g0'..'g100' -> 16, 232..255, 231 grays and color cube black/white
389
+ 'g#00'..'g#ff' -> 16, 232...255, 231 gray and color cube black/white
390
+
391
+ Returns None if desc is invalid.
392
+
393
+ >>> _parse_color_256('h142')
394
+ 142
395
+ >>> _parse_color_256('#f00')
396
+ 196
397
+ >>> _parse_color_256('g100')
398
+ 231
399
+ >>> _parse_color_256('g#80')
400
+ 244
401
+ """
402
+ if len(desc) > 4:
403
+ # keep the length within reason before parsing
404
+ return None
405
+ try:
406
+ if desc.startswith("h"):
407
+ # high-color number
408
+ num = int(desc[1:], 10)
409
+ if num < 0 or num > 255:
410
+ return None
411
+ return num
412
+
413
+ if desc.startswith("#") and len(desc) == 4:
414
+ # color-cube coordinates
415
+ rgb = int(desc[1:], 16)
416
+ if rgb < 0:
417
+ return None
418
+ b, rgb = rgb % 16, rgb // 16
419
+ g, r = rgb % 16, rgb // 16
420
+ # find the closest rgb values
421
+ r = _CUBE_256_LOOKUP_16[r]
422
+ g = _CUBE_256_LOOKUP_16[g]
423
+ b = _CUBE_256_LOOKUP_16[b]
424
+ return _CUBE_START + (r * _CUBE_SIZE_256 + g) * _CUBE_SIZE_256 + b
425
+
426
+ # Only remaining possibility is gray value
427
+ if desc.startswith("g#"):
428
+ # hex value 00..ff
429
+ gray = int(desc[2:], 16)
430
+ if gray < 0 or gray > 255:
431
+ return None
432
+ gray = _GRAY_256_LOOKUP[gray]
433
+ elif desc.startswith("g"):
434
+ # decimal value 0..100
435
+ gray = int(desc[1:], 10)
436
+ if gray < 0 or gray > 100:
437
+ return None
438
+ gray = _GRAY_256_LOOKUP_101[gray]
439
+ else:
440
+ return None
441
+ if gray == 0:
442
+ return _CUBE_BLACK
443
+ gray -= 1
444
+ if gray == _GRAY_SIZE_256:
445
+ return _CUBE_WHITE_256
446
+ return _GRAY_START_256 + gray
447
+
448
+ except ValueError:
449
+ return None
450
+
451
+
452
+ def _true_to_256(desc: str) -> str | None:
453
+ if not (desc.startswith("#") and len(desc) == 7):
454
+ return None
455
+
456
+ c256 = _parse_color_256("#" + "".join(format(int(x, 16) // 16, "x") for x in (desc[1:3], desc[3:5], desc[5:7])))
457
+ return _color_desc_256(c256)
458
+
459
+
460
+ def _parse_color_88(desc: str) -> int | None:
461
+ """
462
+ Return a color number for the description desc.
463
+ 'h0'..'h87' -> 0..87 actual color number
464
+ '#000'..'#fff' -> 16..79 color cube colors
465
+ 'g0'..'g100' -> 16, 80..87, 79 grays and color cube black/white
466
+ 'g#00'..'g#ff' -> 16, 80...87, 79 gray and color cube black/white
467
+
468
+ Returns None if desc is invalid.
469
+
470
+ >>> _parse_color_88('h142')
471
+ >>> _parse_color_88('h42')
472
+ 42
473
+ >>> _parse_color_88('#f00')
474
+ 64
475
+ >>> _parse_color_88('g100')
476
+ 79
477
+ >>> _parse_color_88('g#80')
478
+ 83
479
+ """
480
+ if len(desc) == 7:
481
+ desc = desc[0:2] + desc[3] + desc[5]
482
+ if len(desc) > 4:
483
+ # keep the length within reason before parsing
484
+ return None
485
+ try:
486
+ if desc.startswith("h"):
487
+ # high-color number
488
+ num = int(desc[1:], 10)
489
+ if num < 0 or num > 87:
490
+ return None
491
+ return num
492
+
493
+ if desc.startswith("#") and len(desc) == 4:
494
+ # color-cube coordinates
495
+ rgb = int(desc[1:], 16)
496
+ if rgb < 0:
497
+ return None
498
+ b, rgb = rgb % 16, rgb // 16
499
+ g, r = rgb % 16, rgb // 16
500
+ # find the closest rgb values
501
+ r = _CUBE_88_LOOKUP_16[r]
502
+ g = _CUBE_88_LOOKUP_16[g]
503
+ b = _CUBE_88_LOOKUP_16[b]
504
+ return _CUBE_START + (r * _CUBE_SIZE_88 + g) * _CUBE_SIZE_88 + b
505
+
506
+ # Only remaining possibility is gray value
507
+ if desc.startswith("g#"):
508
+ # hex value 00..ff
509
+ gray = int(desc[2:], 16)
510
+ if gray < 0 or gray > 255:
511
+ return None
512
+ gray = _GRAY_88_LOOKUP[gray]
513
+ elif desc.startswith("g"):
514
+ # decimal value 0..100
515
+ gray = int(desc[1:], 10)
516
+ if gray < 0 or gray > 100:
517
+ return None
518
+ gray = _GRAY_88_LOOKUP_101[gray]
519
+ else:
520
+ return None
521
+ if gray == 0:
522
+ return _CUBE_BLACK
523
+ gray -= 1
524
+ if gray == _GRAY_SIZE_88:
525
+ return _CUBE_WHITE_88
526
+ return _GRAY_START_88 + gray
527
+
528
+ except ValueError:
529
+ return None
530
+
531
+
532
+ class AttrSpecError(Exception):
533
+ pass
534
+
535
+
536
+ class AttrSpec:
537
+ __slots__ = ("__value",)
538
+
539
+ def __init__(self, fg: str, bg: str, colors: Literal[1, 16, 88, 256, 16777216] = 256) -> None:
540
+ """
541
+ fg -- a string containing a comma-separated foreground color
542
+ and settings
543
+
544
+ Color values:
545
+ 'default' (use the terminal's default foreground),
546
+ 'black', 'dark red', 'dark green', 'brown', 'dark blue',
547
+ 'dark magenta', 'dark cyan', 'light gray', 'dark gray',
548
+ 'light red', 'light green', 'yellow', 'light blue',
549
+ 'light magenta', 'light cyan', 'white'
550
+
551
+ High-color example values:
552
+ '#009' (0% red, 0% green, 60% red, like HTML colors)
553
+ '#23facc' (RRGGBB hex color code)
554
+ '#fcc' (100% red, 80% green, 80% blue)
555
+ 'g40' (40% gray, decimal), 'g#cc' (80% gray, hex),
556
+ '#000', 'g0', 'g#00' (black),
557
+ '#fff', 'g100', 'g#ff' (white)
558
+ 'h8' (color number 8), 'h255' (color number 255)
559
+
560
+ Setting:
561
+ 'bold', 'italics', 'underline', 'blink', 'standout',
562
+ 'strikethrough'
563
+
564
+ Some terminals use 'bold' for bright colors. Most terminals
565
+ ignore the 'blink' setting. If the color is not given then
566
+ 'default' will be assumed.
567
+
568
+ bg -- a string containing the background color
569
+
570
+ Color values:
571
+ 'default' (use the terminal's default background),
572
+ 'black', 'dark red', 'dark green', 'brown', 'dark blue',
573
+ 'dark magenta', 'dark cyan', 'light gray'
574
+
575
+ High-color exaples:
576
+ see fg examples above
577
+
578
+ An empty string will be treated the same as 'default'.
579
+
580
+ colors -- the maximum colors available for the specification
581
+
582
+ Valid values include: 1, 16, 88, 256, and 2**24. High-color
583
+ values are only usable with 88, 256, or 2**24 colors. With
584
+ 1 color only the foreground settings may be used.
585
+
586
+ >>> AttrSpec('dark red', 'light gray', 16)
587
+ AttrSpec('dark red', 'light gray')
588
+ >>> AttrSpec('yellow, underline, bold', 'dark blue')
589
+ AttrSpec('yellow,bold,underline', 'dark blue')
590
+ >>> AttrSpec('#ddb', '#004', 256) # closest colors will be found
591
+ AttrSpec('#dda', '#006')
592
+ >>> AttrSpec('#ddb', '#004', 88)
593
+ AttrSpec('#ccc', '#000', colors=88)
594
+ """
595
+ if colors not in {1, 16, 88, 256, 2**24}:
596
+ raise AttrSpecError(f"invalid number of colors ({colors:d}).")
597
+ self.__value = 0 | _HIGH_88_COLOR * (colors == 88) | _HIGH_TRUE_COLOR * (colors == 2**24)
598
+ self.__set_foreground(fg)
599
+ self.__set_background(bg)
600
+ if self.colors > colors:
601
+ raise AttrSpecError(
602
+ f"foreground/background ({fg!r}/{bg!r}) require more colors than have been specified ({colors:d})."
603
+ )
604
+
605
+ def copy_modified(
606
+ self,
607
+ fg: str | None = None,
608
+ bg: str | None = None,
609
+ colors: Literal[1, 16, 88, 256, 16777216] | None = None,
610
+ ) -> Self:
611
+ if fg is None:
612
+ foreground = self.foreground
613
+ else:
614
+ foreground = fg
615
+
616
+ if bg is None:
617
+ background = self.background
618
+ else:
619
+ background = bg
620
+
621
+ if colors is None:
622
+ new_colors = self.colors
623
+ else:
624
+ new_colors = colors
625
+
626
+ return self.__class__(foreground, background, new_colors)
627
+
628
+ def __hash__(self) -> int:
629
+ """Instance is immutable and hashable."""
630
+ return hash((self.__class__, self.__value))
631
+
632
+ @property
633
+ def _value(self) -> int:
634
+ """Read-only value access."""
635
+ return self.__value
636
+
637
+ @property
638
+ def foreground_basic(self) -> bool:
639
+ return self.__value & _FG_BASIC_COLOR != 0
640
+
641
+ @property
642
+ def foreground_high(self) -> bool:
643
+ return self.__value & _FG_HIGH_COLOR != 0
644
+
645
+ @property
646
+ def foreground_true(self) -> bool:
647
+ return self.__value & _FG_TRUE_COLOR != 0
648
+
649
+ @property
650
+ def foreground_number(self) -> int:
651
+ return self.__value & _FG_COLOR_MASK
652
+
653
+ @property
654
+ def background_basic(self) -> bool:
655
+ return self.__value & _BG_BASIC_COLOR != 0
656
+
657
+ @property
658
+ def background_high(self) -> bool:
659
+ return self.__value & _BG_HIGH_COLOR != 0
660
+
661
+ @property
662
+ def background_true(self) -> bool:
663
+ return self.__value & _BG_TRUE_COLOR != 0
664
+
665
+ @property
666
+ def background_number(self) -> int:
667
+ return (self.__value & _BG_COLOR_MASK) >> _BG_SHIFT
668
+
669
+ @property
670
+ def italics(self) -> bool:
671
+ return self.__value & _ITALICS != 0
672
+
673
+ @property
674
+ def bold(self) -> bool:
675
+ return self.__value & _BOLD != 0
676
+
677
+ @property
678
+ def underline(self) -> bool:
679
+ return self.__value & _UNDERLINE != 0
680
+
681
+ @property
682
+ def blink(self) -> bool:
683
+ return self.__value & _BLINK != 0
684
+
685
+ @property
686
+ def standout(self) -> bool:
687
+ return self.__value & _STANDOUT != 0
688
+
689
+ @property
690
+ def strikethrough(self) -> bool:
691
+ return self.__value & _STRIKETHROUGH != 0
692
+
693
+ @property
694
+ def colors(self) -> int:
695
+ """
696
+ Return the maximum colors required for this object.
697
+
698
+ Returns 256, 88, 16 or 1.
699
+ """
700
+ if self.__value & _HIGH_88_COLOR:
701
+ return 88
702
+ if self.__value & (_BG_HIGH_COLOR | _FG_HIGH_COLOR):
703
+ return 256
704
+ if self.__value & (_BG_TRUE_COLOR | _FG_TRUE_COLOR):
705
+ return 2**24
706
+ if self.__value & (_BG_BASIC_COLOR | _FG_BASIC_COLOR):
707
+ return 16
708
+ return 1
709
+
710
+ def _colors(self) -> int:
711
+ warnings.warn(
712
+ f"Method `{self.__class__.__name__}._colors` is deprecated, "
713
+ f"please use property `{self.__class__.__name__}.colors`",
714
+ DeprecationWarning,
715
+ stacklevel=2,
716
+ )
717
+ return self.colors
718
+
719
+ def __repr__(self) -> str:
720
+ """
721
+ Return an executable python representation of the AttrSpec
722
+ object.
723
+ """
724
+ args = f"{self.foreground!r}, {self.background!r}"
725
+ if self.colors == 88:
726
+ # 88-color mode is the only one that is handled differently
727
+ args = f"{args}, colors=88"
728
+ return f"{self.__class__.__name__}({args})"
729
+
730
+ def _foreground_color(self) -> str:
731
+ """Return only the color component of the foreground."""
732
+ if not (self.foreground_basic or self.foreground_high or self.foreground_true):
733
+ return "default"
734
+ if self.foreground_basic:
735
+ return _BASIC_COLORS[self.foreground_number]
736
+ if self.colors == 88:
737
+ return _color_desc_88(self.foreground_number)
738
+ if self.colors == 2**24:
739
+ return _color_desc_true(self.foreground_number)
740
+ return _color_desc_256(self.foreground_number)
741
+
742
+ @property
743
+ def foreground(self) -> str:
744
+ return (
745
+ self._foreground_color()
746
+ + ",bold" * self.bold
747
+ + ",italics" * self.italics
748
+ + ",standout" * self.standout
749
+ + ",blink" * self.blink
750
+ + ",underline" * self.underline
751
+ + ",strikethrough" * self.strikethrough
752
+ )
753
+
754
+ def __set_foreground(self, foreground: str) -> None:
755
+ color = None
756
+ flags = 0
757
+ # handle comma-separated foreground
758
+ for part in foreground.split(","):
759
+ part = part.strip() # noqa: PLW2901
760
+ if part in _ATTRIBUTES:
761
+ # parse and store "settings"/attributes in flags
762
+ if flags & _ATTRIBUTES[part]:
763
+ raise AttrSpecError(f"Setting {part!r} specified more than once in foreground ({foreground!r})")
764
+ flags |= _ATTRIBUTES[part]
765
+ continue
766
+ # past this point we must be specifying a color
767
+ if part in {"", "default"}:
768
+ scolor = 0
769
+ elif part in _BASIC_COLORS:
770
+ scolor = _BASIC_COLORS.index(part)
771
+ flags |= _FG_BASIC_COLOR
772
+ elif self.__value & _HIGH_88_COLOR:
773
+ scolor = _parse_color_88(part)
774
+ flags |= _FG_HIGH_COLOR
775
+ elif self.__value & _HIGH_TRUE_COLOR:
776
+ scolor = _parse_color_true(part)
777
+ flags |= _FG_TRUE_COLOR
778
+ else:
779
+ scolor = _parse_color_256(_true_to_256(part) or part)
780
+ flags |= _FG_HIGH_COLOR
781
+ # _parse_color_*() return None for unrecognised colors
782
+ if scolor is None:
783
+ raise AttrSpecError(f"Unrecognised color specification {part!r} in foreground ({foreground!r})")
784
+ if color is not None:
785
+ raise AttrSpecError(f"More than one color given for foreground ({foreground!r})")
786
+ color = scolor
787
+ if color is None:
788
+ color = 0
789
+ self.__value = (self.__value & ~_FG_MASK) | color | flags
790
+
791
+ def _foreground(self) -> str:
792
+ warnings.warn(
793
+ f"Method `{self.__class__.__name__}._foreground` is deprecated, "
794
+ f"please use property `{self.__class__.__name__}.foreground`",
795
+ DeprecationWarning,
796
+ stacklevel=2,
797
+ )
798
+ return self.foreground
799
+
800
+ @property
801
+ def background(self) -> str:
802
+ """Return the background color."""
803
+ if not (self.background_basic or self.background_high or self.background_true):
804
+ return "default"
805
+ if self.background_basic:
806
+ return _BASIC_COLORS[self.background_number]
807
+ if self.__value & _HIGH_88_COLOR:
808
+ return _color_desc_88(self.background_number)
809
+ if self.colors == 2**24:
810
+ return _color_desc_true(self.background_number)
811
+ return _color_desc_256(self.background_number)
812
+
813
+ def __set_background(self, background: str) -> None:
814
+ flags = 0
815
+ if background in {"", "default"}:
816
+ color = 0
817
+ elif background in _BASIC_COLORS:
818
+ color = _BASIC_COLORS.index(background)
819
+ flags |= _BG_BASIC_COLOR
820
+ elif self.__value & _HIGH_88_COLOR:
821
+ color = _parse_color_88(background)
822
+ flags |= _BG_HIGH_COLOR
823
+ elif self.__value & _HIGH_TRUE_COLOR:
824
+ color = _parse_color_true(background)
825
+ flags |= _BG_TRUE_COLOR
826
+ else:
827
+ color = _parse_color_256(_true_to_256(background) or background)
828
+ flags |= _BG_HIGH_COLOR
829
+ if color is None:
830
+ raise AttrSpecError(f"Unrecognised color specification in background ({background!r})")
831
+ self.__value = (self.__value & ~_BG_MASK) | (color << _BG_SHIFT) | flags
832
+
833
+ def _background(self) -> str:
834
+ warnings.warn(
835
+ f"Method `{self.__class__.__name__}._background` is deprecated, "
836
+ f"please use property `{self.__class__.__name__}.background`",
837
+ DeprecationWarning,
838
+ stacklevel=2,
839
+ )
840
+ return self.background
841
+
842
+ def get_rgb_values(self) -> tuple[int | None, int | None, int | None, int | None, int | None, int | None]:
843
+ """
844
+ Return (fg_red, fg_green, fg_blue, bg_red, bg_green, bg_blue) color
845
+ components. Each component is in the range 0-255. Values are taken
846
+ from the XTerm defaults and may not exactly match the user's terminal.
847
+
848
+ If the foreground or background is 'default' then all their compenents
849
+ will be returned as None.
850
+
851
+ >>> AttrSpec('yellow', '#ccf', colors=88).get_rgb_values()
852
+ (255, 255, 0, 205, 205, 255)
853
+ >>> AttrSpec('default', 'g92').get_rgb_values()
854
+ (None, None, None, 238, 238, 238)
855
+ """
856
+ if not (self.foreground_basic or self.foreground_high or self.foreground_true):
857
+ vals = (None, None, None)
858
+ elif self.colors == 88:
859
+ if self.foreground_number >= 88:
860
+ raise ValueError(f"Invalid AttrSpec _value: {self.foreground_number!r}")
861
+ vals = _COLOR_VALUES_88[self.foreground_number]
862
+ elif self.colors == 2**24:
863
+ h = f"{self.foreground_number:06x}"
864
+ vals = tuple(int(x, 16) for x in (h[0:2], h[2:4], h[4:6]))
865
+ else:
866
+ vals = _COLOR_VALUES_256[self.foreground_number]
867
+
868
+ if not (self.background_basic or self.background_high or self.background_true):
869
+ return (*vals, None, None, None)
870
+ if self.colors == 88:
871
+ if self.background_number >= 88:
872
+ raise ValueError(f"Invalid AttrSpec _value: {self.background_number!r}")
873
+ return vals + _COLOR_VALUES_88[self.background_number]
874
+ if self.colors == 2**24:
875
+ h = f"{self.background_number:06x}"
876
+ return vals + tuple(int(x, 16) for x in (h[0:2], h[2:4], h[4:6]))
877
+
878
+ return vals + _COLOR_VALUES_256[self.background_number]
879
+
880
+ def __eq__(self, other: object) -> bool:
881
+ return isinstance(other, AttrSpec) and self.__value == other._value
882
+
883
+ def __ne__(self, other: object) -> bool:
884
+ return not self == other
885
+
886
+
887
+ class RealTerminal:
888
+ def __init__(self) -> None:
889
+ super().__init__()
890
+ self._signal_keys_set = False
891
+ self._old_signal_keys = None
892
+
893
+ if IS_WINDOWS:
894
+
895
+ def tty_signal_keys(
896
+ self,
897
+ intr: Literal["undefined"] | int | None = None,
898
+ quit: Literal["undefined"] | int | None = None, # noqa: A002 # pylint: disable=redefined-builtin
899
+ start: Literal["undefined"] | int | None = None,
900
+ stop: Literal["undefined"] | int | None = None,
901
+ susp: Literal["undefined"] | int | None = None,
902
+ fileno: int | None = None,
903
+ ):
904
+ """
905
+ Read and/or set the tty's signal character settings.
906
+ This function returns the current settings as a tuple.
907
+
908
+ Use the string 'undefined' to unmap keys from their signals.
909
+ The value None is used when no change is being made.
910
+ Setting signal keys is done using the integer ascii
911
+ code for the key, eg. 3 for CTRL+C.
912
+
913
+ If this function is called after start() has been called
914
+ then the original settings will be restored when stop()
915
+ is called.
916
+ """
917
+ return ()
918
+
919
+ else:
920
+
921
+ def tty_signal_keys(
922
+ self,
923
+ intr: Literal["undefined"] | int | None = None,
924
+ quit: Literal["undefined"] | int | None = None, # noqa: A002 # pylint: disable=redefined-builtin
925
+ start: Literal["undefined"] | int | None = None,
926
+ stop: Literal["undefined"] | int | None = None,
927
+ susp: Literal["undefined"] | int | None = None,
928
+ fileno: int | None = None,
929
+ ):
930
+ """
931
+ Read and/or set the tty's signal character settings.
932
+ This function returns the current settings as a tuple.
933
+
934
+ Use the string 'undefined' to unmap keys from their signals.
935
+ The value None is used when no change is being made.
936
+ Setting signal keys is done using the integer ascii
937
+ code for the key, eg. 3 for CTRL+C.
938
+
939
+ If this function is called after start() has been called
940
+ then the original settings will be restored when stop()
941
+ is called.
942
+ """
943
+
944
+ if fileno is None:
945
+ fileno = sys.stdin.fileno()
946
+ if not os.isatty(fileno):
947
+ return None
948
+
949
+ tattr = termios.tcgetattr(fileno)
950
+ sattr = tattr[6]
951
+ skeys = (
952
+ sattr[termios.VINTR],
953
+ sattr[termios.VQUIT],
954
+ sattr[termios.VSTART],
955
+ sattr[termios.VSTOP],
956
+ sattr[termios.VSUSP],
957
+ )
958
+
959
+ if intr == "undefined":
960
+ intr = 0
961
+ if quit == "undefined":
962
+ quit = 0 # noqa: A001
963
+ if start == "undefined":
964
+ start = 0
965
+ if stop == "undefined":
966
+ stop = 0
967
+ if susp == "undefined":
968
+ susp = 0
969
+
970
+ if intr is not None:
971
+ tattr[6][termios.VINTR] = intr
972
+ if quit is not None:
973
+ tattr[6][termios.VQUIT] = quit
974
+ if start is not None:
975
+ tattr[6][termios.VSTART] = start
976
+ if stop is not None:
977
+ tattr[6][termios.VSTOP] = stop
978
+ if susp is not None:
979
+ tattr[6][termios.VSUSP] = susp
980
+
981
+ if any(item is not None for item in (intr, quit, start, stop, susp)):
982
+ termios.tcsetattr(fileno, termios.TCSADRAIN, tattr)
983
+ self._signal_keys_set = True
984
+
985
+ return skeys
986
+
987
+
988
+ class ScreenError(Exception):
989
+ pass
990
+
991
+
992
+ class BaseMeta(signals.MetaSignals, abc.ABCMeta):
993
+ """Base metaclass for abstra"""
994
+
995
+
996
+ class BaseScreen(metaclass=BaseMeta):
997
+ """
998
+ Base class for Screen classes (raw_display.Screen, .. etc)
999
+ """
1000
+
1001
+ signals: typing.ClassVar[list[str]] = [UPDATE_PALETTE_ENTRY, INPUT_DESCRIPTORS_CHANGED]
1002
+
1003
+ def __init__(self) -> None:
1004
+ super().__init__()
1005
+
1006
+ self.logger = logging.getLogger(f"{self.__class__.__module__}.{self.__class__.__name__}")
1007
+
1008
+ self._palette: dict[str | None, tuple[AttrSpec, AttrSpec, AttrSpec, AttrSpec, AttrSpec]] = {}
1009
+ self._started: bool = False
1010
+
1011
+ @property
1012
+ def started(self) -> bool:
1013
+ return self._started
1014
+
1015
+ def start(self, *args, **kwargs) -> StoppingContext:
1016
+ """Set up the screen. If the screen has already been started, does
1017
+ nothing.
1018
+
1019
+ May be used as a context manager, in which case :meth:`stop` will
1020
+ automatically be called at the end of the block:
1021
+
1022
+ with screen.start():
1023
+ ...
1024
+
1025
+ You shouldn't override this method in a subclass; instead, override
1026
+ :meth:`_start`.
1027
+ """
1028
+ if not self._started:
1029
+ self._started = True
1030
+ self._start(*args, **kwargs)
1031
+ return StoppingContext(self)
1032
+
1033
+ def _start(self) -> None:
1034
+ pass
1035
+
1036
+ def stop(self) -> None:
1037
+ if self._started:
1038
+ self._stop()
1039
+ self._started = False
1040
+
1041
+ def _stop(self) -> None:
1042
+ pass
1043
+
1044
+ def run_wrapper(self, fn, *args, **kwargs):
1045
+ """Start the screen, call a function, then stop the screen. Extra
1046
+ arguments are passed to `start`.
1047
+
1048
+ Deprecated in favor of calling `start` as a context manager.
1049
+ """
1050
+ warnings.warn(
1051
+ "run_wrapper is deprecated in favor of calling `start` as a context manager.",
1052
+ DeprecationWarning,
1053
+ stacklevel=3,
1054
+ )
1055
+ with self.start(*args, **kwargs):
1056
+ return fn()
1057
+
1058
+ def set_mouse_tracking(self, enable: bool = True) -> None:
1059
+ pass
1060
+
1061
+ @abc.abstractmethod
1062
+ def draw_screen(self, size: tuple[int, int], canvas: Canvas) -> None:
1063
+ pass
1064
+
1065
+ def clear(self) -> None:
1066
+ """Clear the screen if possible.
1067
+
1068
+ Force the screen to be completely repainted on the next call to draw_screen().
1069
+ """
1070
+
1071
+ def get_cols_rows(self) -> tuple[int, int]:
1072
+ """Return the terminal dimensions (num columns, num rows).
1073
+
1074
+ Default (fallback) is 80x24.
1075
+ """
1076
+ return 80, 24
1077
+
1078
+ def register_palette(
1079
+ self,
1080
+ palette: Iterable[
1081
+ tuple[str, str] | tuple[str, str, str] | tuple[str, str, str, str] | tuple[str, str, str, str, str, str]
1082
+ ],
1083
+ ) -> None:
1084
+ """Register a set of palette entries.
1085
+
1086
+ palette -- a list of (name, like_other_name) or
1087
+ (name, foreground, background, mono, foreground_high, background_high) tuples
1088
+
1089
+ The (name, like_other_name) format will copy the settings
1090
+ from the palette entry like_other_name, which must appear
1091
+ before this tuple in the list.
1092
+
1093
+ The mono and foreground/background_high values are
1094
+ optional ie. the second tuple format may have 3, 4 or 6
1095
+ values. See register_palette_entry() for a description
1096
+ of the tuple values.
1097
+ """
1098
+
1099
+ for item in palette:
1100
+ if len(item) in {3, 4, 6}:
1101
+ self.register_palette_entry(*item)
1102
+ continue
1103
+ if len(item) != 2:
1104
+ raise ScreenError(f"Invalid register_palette entry: {item!r}")
1105
+ name, like_name = item
1106
+ if like_name not in self._palette:
1107
+ raise ScreenError(f"palette entry '{like_name}' doesn't exist")
1108
+ self._palette[name] = self._palette[like_name]
1109
+
1110
+ def register_palette_entry(
1111
+ self,
1112
+ name: str | None,
1113
+ foreground: str,
1114
+ background: str,
1115
+ mono: str | None = None,
1116
+ foreground_high: str | None = None,
1117
+ background_high: str | None = None,
1118
+ ) -> None:
1119
+ """Register a single palette entry.
1120
+
1121
+ name -- new entry/attribute name
1122
+
1123
+ foreground -- a string containing a comma-separated foreground
1124
+ color and settings
1125
+
1126
+ Color values:
1127
+ 'default' (use the terminal's default foreground),
1128
+ 'black', 'dark red', 'dark green', 'brown', 'dark blue',
1129
+ 'dark magenta', 'dark cyan', 'light gray', 'dark gray',
1130
+ 'light red', 'light green', 'yellow', 'light blue',
1131
+ 'light magenta', 'light cyan', 'white'
1132
+
1133
+ Settings:
1134
+ 'bold', 'underline', 'blink', 'standout', 'strikethrough'
1135
+
1136
+ Some terminals use 'bold' for bright colors. Most terminals
1137
+ ignore the 'blink' setting. If the color is not given then
1138
+ 'default' will be assumed.
1139
+
1140
+ background -- a string containing the background color
1141
+
1142
+ Background color values:
1143
+ 'default' (use the terminal's default background),
1144
+ 'black', 'dark red', 'dark green', 'brown', 'dark blue',
1145
+ 'dark magenta', 'dark cyan', 'light gray'
1146
+
1147
+ mono -- a comma-separated string containing monochrome terminal
1148
+ settings (see "Settings" above.)
1149
+
1150
+ None = no terminal settings (same as 'default')
1151
+
1152
+ foreground_high -- a string containing a comma-separated
1153
+ foreground color and settings, standard foreground
1154
+ colors (see "Color values" above) or high-colors may
1155
+ be used
1156
+
1157
+ High-color example values:
1158
+ '#009' (0% red, 0% green, 60% red, like HTML colors)
1159
+ '#fcc' (100% red, 80% green, 80% blue)
1160
+ 'g40' (40% gray, decimal), 'g#cc' (80% gray, hex),
1161
+ '#000', 'g0', 'g#00' (black),
1162
+ '#fff', 'g100', 'g#ff' (white)
1163
+ 'h8' (color number 8), 'h255' (color number 255)
1164
+
1165
+ None = use foreground parameter value
1166
+
1167
+ background_high -- a string containing the background color,
1168
+ standard background colors (see "Background colors" above)
1169
+ or high-colors (see "High-color example values" above)
1170
+ may be used
1171
+
1172
+ None = use background parameter value
1173
+ """
1174
+ basic = AttrSpec(foreground, background, 16)
1175
+
1176
+ if isinstance(mono, tuple):
1177
+ # old style of specifying mono attributes was to put them
1178
+ # in a tuple. convert to comma-separated string
1179
+ mono = ",".join(mono)
1180
+ if mono is None:
1181
+ mono = DEFAULT
1182
+ mono_spec = AttrSpec(mono, DEFAULT, 1)
1183
+
1184
+ if foreground_high is None:
1185
+ foreground_high = foreground
1186
+ if background_high is None:
1187
+ background_high = background
1188
+
1189
+ high_256 = AttrSpec(foreground_high, background_high, 256)
1190
+ high_true = AttrSpec(foreground_high, background_high, 2**24)
1191
+
1192
+ # 'hX' where X > 15 are different in 88/256 color, use
1193
+ # basic colors for 88-color mode if high colors are specified
1194
+ # in this way (also avoids crash when X > 87)
1195
+ def large_h(desc: str) -> bool:
1196
+ if not desc.startswith("h"):
1197
+ return False
1198
+ if "," in desc:
1199
+ desc = desc.split(",", 1)[0]
1200
+ num = int(desc[1:], 10)
1201
+ return num > 15
1202
+
1203
+ if large_h(foreground_high) or large_h(background_high):
1204
+ high_88 = basic
1205
+ else:
1206
+ high_88 = AttrSpec(foreground_high, background_high, 88)
1207
+
1208
+ signals.emit_signal(self, UPDATE_PALETTE_ENTRY, name, basic, mono_spec, high_88, high_256, high_true)
1209
+ self._palette[name] = (basic, mono_spec, high_88, high_256, high_true)
1210
+
1211
+
1212
+ def _test():
1213
+ import doctest
1214
+
1215
+ doctest.testmod()
1216
+
1217
+
1218
+ if __name__ == "__main__":
1219
+ _test()