Coverage for yuio / theme.py: 98%

357 statements  

« prev     ^ index     » next       coverage.py v7.13.3, created at 2026-02-03 15:42 +0000

1# Yuio project, MIT license. 

2# 

3# https://github.com/taminomara/yuio/ 

4# 

5# You're free to copy this file to your project and edit it for your needs, 

6# just keep this copyright line please :3 

7 

8""" 

9Controlling visual aspects of Yuio with themes. 

10 

11Theme base class 

12---------------- 

13 

14The overall look and feel of a Yuio application is declared 

15in a :class:`Theme` object: 

16 

17.. autoclass:: Theme 

18 

19 .. autoattribute:: progress_bar_width 

20 

21 .. autoattribute:: spinner_update_rate_ms 

22 

23 .. autoattribute:: separate_headings 

24 

25 .. autoattribute:: fallback_width 

26 

27 .. autoattribute:: msg_decorations_unicode 

28 

29 .. automethod:: set_msg_decoration_unicode 

30 

31 .. automethod:: _set_msg_decoration_unicode_if_not_overridden 

32 

33 .. autoattribute:: msg_decorations_ascii 

34 

35 .. automethod:: set_msg_decoration_ascii 

36 

37 .. automethod:: _set_msg_decoration_ascii_if_not_overridden 

38 

39 .. autoattribute:: colors 

40 

41 .. automethod:: set_color 

42 

43 .. automethod:: _set_color_if_not_overridden 

44 

45 .. automethod:: get_color 

46 

47 .. automethod:: to_color 

48 

49 .. automethod:: check 

50 

51 

52Default theme 

53------------- 

54 

55Use the following loader to create an instance of the default theme: 

56 

57.. autofunction:: load 

58 

59.. autoclass:: BaseTheme 

60 

61.. autoclass:: DefaultTheme 

62 

63 

64.. _all-color-paths: 

65 

66Color paths 

67----------- 

68 

69.. _common-tags: 

70 

71.. color-path:: common tags 

72 

73 :class:`BaseTheme` sets up commonly used colors that you can use 

74 in formatted messages: 

75 

76 - ``code``: inline code, 

77 - ``note``: inline highlighting, 

78 - ``path``: file paths, 

79 - ``flag``: CLI flags, 

80 - ``bold``, ``b``: font style, 

81 - ``dim``, ``d``: font style, 

82 - ``italic``, ``i``: font style, 

83 - ``underline``, ``u``: font style, 

84 - ``inverse``: swap foreground and background colors, 

85 - ``normal``: normal foreground, 

86 - ``muted``: muted foreground (see :attr:`~yuio.color.Color.FORE_NORMAL_DIM`), 

87 - ``red``: foreground, 

88 - ``green``: foreground, 

89 - ``yellow``: foreground, 

90 - ``blue``: foreground, 

91 - ``magenta``: foreground, 

92 - ``cyan``: foreground, 

93 

94 .. note:: 

95 

96 We don't define ``black`` and ``white`` because they can be invisible 

97 with some terminal themes. Prefer ``muted`` when you need a muted color. 

98 

99 We also don't define tags for backgrounds because there's no way to tell 

100 which foreground/background combination will be readable and which will not. 

101 Prefer ``inverse`` when you need to add a background. 

102 

103.. _main-colors: 

104 

105.. color-path:: main colors 

106 

107 :class:`DefaultTheme` defines *main colors*, which you can override by subclassing. 

108 

109 - ``heading_color``: for headings, 

110 - ``primary_color``: for main text, 

111 - ``accent_color``, ``accent_color_2``: for visually highlighted elements, 

112 - ``secondary_color``: for visually dimmed elements, 

113 - ``error_color``: for everything that indicates an error, 

114 - ``warning_color``: for everything that indicates a warning, 

115 - ``success_color``: for everything that indicates a success, 

116 - ``critical_color``: for critical or internal errors, 

117 - ``low_priority_color_a``: for auxiliary elements such as help widget, 

118 - ``low_priority_color_b``: for auxiliary elements such as help widget, 

119 even lower priority. 

120 

121.. _term-colors: 

122 

123.. color-path:: `term/{color}` 

124 

125 :class:`DefaultTheme` will export default colors for the attached terminal 

126 as :samp:`term/{color}`. This is useful when defining gradients for progress bars, 

127 as they require exact color values for interpolation. 

128 

129 ``color`` can be one ``background``, ``foreground``, ``black``, ``bright_black``, 

130 ``red``, ``bright_red``, ``green``, ``bright_green``, ``yellow``, ``bright_yellow``, 

131 ``blue``, ``bright_blue``, ``magenta``, ``bright_magenta``, 

132 ``cyan``, ``bright_cyan``, ``white``, or ``bright_white``. 

133 

134.. color-path:: `msg/decoration:{tag}` 

135 

136 Color for decorations in front of messages: 

137 

138 - ``msg/decoration:info``: messages from :mod:`yuio.io.info`, 

139 - ``msg/decoration:warning``: messages from :mod:`yuio.io.warning`, 

140 - ``msg/decoration:error``: messages from :mod:`yuio.io.error`, 

141 - ``msg/decoration:success``: messages from :mod:`yuio.io.success`, 

142 - ``msg/decoration:failure``: messages from :mod:`yuio.io.failure`, 

143 - :samp:`msg/decoration:heading/{level}`: messages from :mod:`yuio.io.heading` 

144 and headings in markdown, 

145 - ``msg/decoration:heading/section``: first-level headings in CLI help, 

146 - ``msg/decoration:question``: messages from :func:`yuio.io.ask`, 

147 - ``msg/decoration:list``: bullets in markdown, 

148 - ``msg/decoration:quote``: quote decorations in markdown, 

149 - ``msg/decoration:code``: code decorations in markdown, 

150 - ``msg/decoration:thematic_break``: thematic breaks 

151 (i.e. horizontal rulers) in markdown, 

152 - :samp:`msg/decoration:hr/{weight}`: horizontal rulers (see :func:`yuio.io.hr` 

153 and :func:`yuio.string.Hr`). 

154 

155.. color-path:: `msg/text:{tag}` 

156 

157 Color for the text part of messages: 

158 

159 - ``msg/text:info`` and all other tags from ``msg/decoration``, 

160 - ``msg/text:paragraph``: plain text in markdown, 

161 - :samp:`msg/text:code/{syntax}`: plain text in highlighted code blocks. 

162 

163.. color-path:: `task/...:{status}` 

164 

165 Running and finished tasks: 

166 

167 - :samp:`task/decoration`: decoration before the task, 

168 - ``task/progressbar/done``: filled portion of the progress bar, 

169 - ``task/progressbar/done/start``: gradient start for the filled 

170 portion of the progress bar, 

171 - ``task/progressbar/done/end``: gradient end for the filled 

172 portion of the progress bar, 

173 - ``task/progressbar/pending``: unfilled portion of the progress bar, 

174 - ``task/progressbar/pending/start``: gradient start for the unfilled 

175 portion of the progress bar, 

176 - ``task/progressbar/pending/end``: gradient end for the unfilled 

177 portion of the progress bar, 

178 - ``task/heading``: task title, 

179 - ``task/progress``: number that indicates task progress, 

180 - ``task/comment``: task comment. 

181 

182 ``status`` can be ``running``, ``done``, or ``error``. 

183 

184.. color-path:: `hl/{token}:{syntax}` 

185 

186 Color for highlighted part of code: 

187 

188 - ``hl/comment``: code comments, 

189 - ``hl/kwd``: keyword, 

190 - ``hl/lit``: non-string literals, 

191 - ``hl/lit/builtin``: built-in literals, i.e. ``None``, ``True``, ``False``, 

192 - ``hl/lit/num``: numeric literals, 

193 - ``hl/lit/num/bin``: binary numeric literals, 

194 - ``hl/lit/num/oct``: octal numeric literals, 

195 - ``hl/lit/num/dec``: decimal numeric literals, 

196 - ``hl/lit/num/hex``: hexadecimal numeric literals, 

197 - ``hl/punct``: punctuation, 

198 - ``hl/str``: string literals, 

199 - ``hl/str/esc``: escape sequences in strings, 

200 - ``hl/str/prefix``: string prefix, i.e. ``f`` in ``f"str"``, 

201 - ``hl/type``: type names, 

202 - ``hl/type/builtin``: type names for builtin types, 

203 - ``hl/type/user``: type names for user-defined types, 

204 - ``hl/meta``: diff meta info for diff highlighting, 

205 - ``hl/added``: added lines in diff highlighting, 

206 - ``hl/removed``: removed lines in diff highlighting, 

207 - ``hl/prog``: program name in CLI usage and shell highlighting, 

208 - ``hl/flag``: CLI flags, 

209 - ``hl/metavar``: meta variables in CLI usage. 

210 

211.. color-path:: `tb/heading` 

212 `tb/message` 

213 `tb/frame/{location}/...` 

214 

215 For highlighted tracebacks: 

216 

217 - ``tb/heading``: traceback heading, 

218 - ``tb/message``: error message, 

219 - :samp:`tb/frame/{location}/file/module`: module name, 

220 - :samp:`tb/frame/{location}/file/line`: line number, 

221 - :samp:`tb/frame/{location}/file/path`: file path, 

222 - :samp:`tb/frame/{location}/code`: code sample at the error line, 

223 - :samp:`tb/frame/{location}/highlight`: highlighting under the code sample. 

224 

225 ``location`` is either ``lib`` or ``usr`` depending on whether the code 

226 is located in site-packages or in user code. 

227 

228.. color-path:: `log/{part}:{level}` 

229 

230 Colors for log records. ``part`` is name of a `log record attribute`__, 

231 level is lowercase name of logging level. 

232 

233 __ https://docs.python.org/3/library/logging.html#logrecord-attributes 

234 

235 .. seealso:: 

236 

237 :class:`yuio.io.Formatter`. 

238 

239.. color-path:: input widget 

240 

241 Colors for :class:`yuio.widget.Input`: 

242 

243 - ``menu/decoration:input``: decoration before an input box, 

244 - ``menu/text:input``: entered text in an input box, 

245 - ``menu/text/esc:input``: highlights for invisible characters in an input box, 

246 - ``menu/text/error:input``: highlights for error region reported by a parser, 

247 - ``menu/text/placeholder:input``: placeholder text in an input box, 

248 

249.. color-path:: grid widgets 

250 

251 Colors for :class:`yuio.widget.Grid`, :class:`yuio.widget.Choice`, and other 

252 similar widgets: 

253 

254 - :samp:`menu/decoration:choice/{status}/{color_tag}`: 

255 decoration before a grid item, 

256 - :samp:`menu/decoration/comment:choice/{status}/{color_tag}`: 

257 decoration around comments for a grid item, 

258 - :samp:`menu/text:choice/{status}/{color_tag}`: 

259 text of a grid item, 

260 - :samp:`menu/text/comment:choice/{status}/{color_tag}`: 

261 comment for a grid item, 

262 - :samp:`menu/text/prefix:choice/{status}/{color_tag}`: 

263 prefix before the main text of a grid item 

264 (see :attr:`yuio.widget.Option.display_text_prefix` and 

265 :attr:`yuio.complete.Completion.dprefix`), 

266 - :samp:`menu/text/suffix:choice/{status}/{color_tag}`: 

267 suffix after the main text of a grid item 

268 (see :attr:`yuio.widget.Option.display_text_suffix` and 

269 :attr:`yuio.complete.Completion.dsuffix`), 

270 - ``menu/text:choice/status_line``: status line (i.e. "Page x of y"). 

271 - ``menu/text:choice/status_line/number``: page numbers in a status line. 

272 

273 ``status`` is either ``normal`` or ``active``: 

274 

275 - ``normal`` for regular grid items, 

276 - ``active`` for the currently selected item. 

277 

278 ``color_tag`` is whatever tag specified by :attr:`yuio.widget.Option.color_tag` 

279 and :attr:`yuio.complete.Completion.group_color_tag`. Currently supported tags: 

280 

281 - ``none``: color tag is not given, 

282 - ``selected``: items selected in :class:`yuio.widget.Multiselect`, 

283 - ``dir``: directory (in file completion), 

284 - ``exec``: executable file (in file completion), 

285 - ``symlink``: symbolic link (in file completion), 

286 - ``socket``: socket (in file completion), 

287 - ``pipe``: FIFO pipe (in file completion), 

288 - ``block_device``: block device (in file completion), 

289 - ``char_device``: character device (in file completion), 

290 - ``original``: original completion item before spelling correction, 

291 - ``corrected``: completion item that was found after spelling correction. 

292 

293.. color-path:: full screen help menu 

294 

295 Colors for help menu that appears when pressing :kbd:`F1`: 

296 

297 - ``menu/text/heading:help_menu``: section heading, 

298 - ``menu/text/help_key:help_menu``: key names, 

299 - ``menu/text/help_sep:help_menu``: separators between key names, 

300 - ``menu/decoration:help_menu``: decorations. 

301 

302.. color-path:: inline help menu 

303 

304 Colors for help items rendered under a widget: 

305 

306 - ``menu/text/help_info:help``: help items that aren't associated with any key, 

307 - ``menu/text/help_msg:help``: regular help items, 

308 - ``menu/text/help_key:help``: keybinding names, 

309 - ``menu/text/help_sep:help``: separator between items. 

310 

311 

312.. _all-decorations: 

313 

314Decorations 

315----------- 

316 

317.. decoration-path:: `info` 

318 

319 Messages from :mod:`yuio.io.info`. 

320 

321.. decoration-path:: `warning` 

322 

323 Messages from :mod:`yuio.io.warning`. 

324 

325.. decoration-path:: `error` 

326 

327 Messages from :mod:`yuio.io.error`. 

328 

329.. decoration-path:: `success` 

330 

331 Messages from :mod:`yuio.io.success`. 

332 

333.. decoration-path:: `failure` 

334 

335 Messages from :mod:`yuio.io.failure`. 

336 

337.. decoration-path:: `heading/{level}` 

338 

339 Messages from :mod:`yuio.io.heading` and headings in markdown. 

340 

341.. decoration-path:: `heading/section` 

342 

343 First-level headings in CLI help. 

344 

345.. decoration-path:: `question` 

346 

347 Messages from :func:`yuio.io.ask`. 

348 

349.. decoration-path:: `list` 

350 

351 Bullets in markdown. 

352 

353.. decoration-path:: `quote` 

354 

355 Quote decorations in markdown. 

356 

357.. decoration-path:: `code` 

358 

359 Code decorations in markdown. 

360 

361.. decoration-path:: `thematic_break` 

362 

363 Thematic breaks (i.e. horizontal rulers) in markdown. 

364 

365.. decoration-path:: `overflow` 

366 

367 Ellipsis symbol for lines that don't fit terminal width. Must be one character wide. 

368 

369.. decoration-path:: `progress_bar/{position}` 

370 

371 Decorations for progress bars. 

372 

373 Available positions are: 

374 

375 :``start_symbol``: 

376 Start of the progress bar. 

377 :``done_symbol``: 

378 Tiles finished portion of the progress bar, must be one character wide. 

379 :``pending_symbol``: 

380 Tiles unfinished portion of the progress bar, must be one character wide. 

381 :``end_symbol``: 

382 End of the progress bar. 

383 :``transition_pattern``: 

384 If this decoration is empty, there's no symbol between finished and unfinished 

385 parts of the progress bar. 

386 

387 Otherwise, this decoration defines a left-to-right gradient of transition 

388 characters, ordered from most filled to least filled. Each character 

389 must be one character wide. 

390 

391 .. raw:: html 

392 

393 <div class="highlight-text notranslate"> 

394 <div class="highlight"> 

395 <pre class="ascii-graphics"> 

396 <span class="k">[------> ]</span> 

397 │└┬───┘│└┬───────┘│ 

398 │ │ │ │ end_symbol 

399 │ │ │ └ pending_symbol 

400 │ │ └ transition_pattern 

401 │ └ done_symbol 

402 └ start_symbol 

403 </pre> 

404 </div> 

405 </div> 

406 

407 **Example:** 

408 

409 To get the classic blocky look, you can do the following: 

410 

411 .. code-block:: python 

412 

413 class BlockProgressTheme(yuio.theme.DefaultTheme): 

414 msg_decorations = { 

415 "progress_bar/start_symbol": "|", 

416 "progress_bar/end_symbol": "|", 

417 "progress_bar/done_symbol": "█", 

418 "progress_bar/pending_symbol": " ", 

419 "progress_bar/transition_pattern": "█▉▊▋▌▍▎▏ ", 

420 } 

421 

422.. decoration-path:: `spinner/pattern` 

423 

424 Defines a sequence of symbols that will be used to show spinners for tasks 

425 without known progress. Next element of the sequence will be shown 

426 every :attr:`~Theme.spinner_update_rate_ms`. 

427 

428 You can find some pre-made patterns in py-spinners__ package. 

429 

430 __ https://github.com/ManrajGrover/py-spinners?tab=readme-ov-file 

431 

432.. decoration-path:: `spinner/static_symbol` 

433 

434 Static spinner symbol, for sub-tasks that've finished running but'. 

435 

436.. decoration-path:: `hr/{weight}/{position}` 

437 

438 Decorations for horizontal rulers (see :func:`yuio.io.hr` 

439 and :func:`yuio.string.Hr`). 

440 

441 Default theme defines three weights: 

442 

443 - ``0`` prints no ruler (but still prints centered text), 

444 - ``1`` prints normal ruler, 

445 - ``2`` prints bold ruler. 

446 

447 Available positions are: 

448 

449 :``left_start``: 

450 Start of the ruler to the left of the message. 

451 :``left_middle``: 

452 Filler of the ruler to the left of the message. 

453 :``left_end``: 

454 End of the ruler to the left of the message. 

455 :``middle``: 

456 Filler of the ruler that's used if `msg` is empty. 

457 :``right_start``: 

458 Start of the ruler to the right of the message. 

459 :``right_middle``: 

460 Filler of the ruler to the right of the message. 

461 :``right_end``: 

462 End of the ruler to the right of the message. 

463 

464 .. raw:: html 

465 

466 <div class="highlight-text notranslate"> 

467 <div class="highlight"> 

468 <pre class="ascii-graphics"> 

469 <span class="k"><------>message<------></span> 

470 │└┬───┘│ │└┬───┘│ 

471 │ │ │ │ │ right_end 

472 │ │ │ │ └ right_middle 

473 │ │ │ └ right_start 

474 │ │ └ left_end 

475 │ └ left_middle 

476 └ left_start 

477 

478 <span class="k"><---------------------></span> 

479 │└┬──────────────────┘│ 

480 │ middle right_end 

481 └ left_start 

482 </pre> 

483 </div> 

484 </div> 

485 

486.. decoration-path:: input widget 

487 

488 Decorations for :class:`yuio.widget.Input`: 

489 

490 - ``menu/input/decoration``: decoration before an input box, 

491 - ``menu/input/decoration_search``: decoration before a search input box. 

492 

493.. decoration-path:: choice and multiselect widget 

494 

495 Decorations for :class:`yuio.widget.Choice` and :class:`yuio.widget.Multiselect`: 

496 

497 - ``menu/choice/decoration/active_item``: current item, 

498 - ``menu/choice/decoration/selected_item``: selected item in multiselect widget, 

499 - ``menu/choice/decoration/deselected_item``: deselected item in multiselect widget. 

500 

501.. decoration-path:: inline help and help menu 

502 

503 Decorations for widget help: 

504 

505 - ``menu/help/decoration``: decoration at the bottom of the help menu, 

506 - :samp:`menu/help/key/{key}`: text for functional keys. 

507 

508 ``key`` can be one of ``ctrl``, ``shift``, ``enter``, ``escape``, ``insert``, 

509 ``delete``, ``backspace``, ``tab``, ``home``, ``end``, ``page_up``, 

510 ``page_down``, ``arrow_up``, ``arrow_down``, ``arrow_left``, ``arrow_right``, 

511 ``space``, ``f1``...\\ ``f12``. 

512 

513""" 

514 

515from __future__ import annotations 

516 

517import dataclasses 

518import functools 

519import os 

520import pathlib 

521import warnings 

522from dataclasses import dataclass 

523from enum import IntFlag 

524 

525import yuio.color 

526import yuio.term 

527 

528from typing import TYPE_CHECKING 

529 

530if TYPE_CHECKING: 

531 import typing_extensions as _t 

532else: 

533 from yuio import _typing as _t 

534 

535__all__ = [ 

536 "BaseTheme", 

537 "DefaultTheme", 

538 "RecursiveThemeWarning", 

539 "TableJunction", 

540 "Theme", 

541 "ThemeWarning", 

542 "load", 

543] 

544 

545K = _t.TypeVar("K") 

546V = _t.TypeVar("V") 

547 

548 

549class ThemeWarning(yuio.YuioWarning): 

550 pass 

551 

552 

553class RecursiveThemeWarning(ThemeWarning): 

554 pass 

555 

556 

557_COLOR_NAMES = [ 

558 "background", 

559 "foreground", 

560 "black", 

561 "bright_black", 

562 "red", 

563 "bright_red", 

564 "green", 

565 "bright_green", 

566 "yellow", 

567 "bright_yellow", 

568 "blue", 

569 "bright_blue", 

570 "magenta", 

571 "bright_magenta", 

572 "cyan", 

573 "bright_cyan", 

574 "white", 

575 "bright_white", 

576] 

577 

578 

579@_t.final 

580class _ImmutableDict(_t.Mapping[K, V], _t.Generic[K, V]): 

581 def __init__( 

582 self, data: dict[K, V], sources: dict[K, type[Theme] | None], attr: str 

583 ): 

584 self.__data = data 

585 self.__sources = sources 

586 self.__attr = attr 

587 

588 def items(self) -> _t.ItemsView[K, V]: 

589 return self.__data.items() 

590 

591 def keys(self) -> _t.KeysView[K]: 

592 return self.__data.keys() 

593 

594 def values(self) -> _t.ValuesView[V]: 

595 return self.__data.values() 

596 

597 def __len__(self): 

598 return len(self.__data) 

599 

600 def __getitem__(self, key): 

601 return self.__data[key] 

602 

603 def __iter__(self): 

604 return iter(self.__data) 

605 

606 def __contains__(self, key): 

607 return key in self.__data 

608 

609 def __repr__(self): 

610 return repr(self.__data) 

611 

612 def __setitem__(self, key, item): 

613 raise TypeError(f"Theme.{self.__attr} is immutable") 

614 

615 def __delitem__(self, key): 

616 raise TypeError(f"Theme.{self.__attr} is immutable") 

617 

618 def copy(self) -> _t.Self: 

619 return self.__class__( 

620 self.__data.copy(), 

621 self.__sources.copy(), 

622 self.__attr, 

623 ) 

624 

625 def _set(self, key: K, value: V, source: type[Theme]): 

626 self.__data[key] = value 

627 self.__sources[key] = source 

628 

629 def _set_if_not_overridden(self, key: K, value: V, source: type[Theme] | None): 

630 if source is None: 

631 raise TypeError( 

632 f"Theme._set_{self.__attr}_if_not_overridden can't be called " 

633 "outside of __init__" 

634 ) 

635 prev_source = self.__sources.get(key) 

636 if prev_source is None or issubclass(source, prev_source): 

637 self._set(key, value, source) 

638 

639 

640@_t.final 

641class _ReadOnlyDescriptor: 

642 def __set_name__(self, owner: object, attr: str): 

643 self.__attr = attr 

644 self.__private_name = f"_Theme__{attr}" 

645 

646 def __get__(self, instance: object | None, owner: type[object] | None = None): 

647 if instance is None: # pragma: no cover 

648 return self 

649 elif (data := instance.__dict__.get(self.__private_name)) is not None: 

650 return data 

651 else: 

652 data = owner.__dict__[self.__private_name].copy() 

653 instance.__dict__[self.__private_name] = data 

654 return data 

655 

656 def __set__(self, instance: object, value: _t.Any): 

657 raise TypeError(f"Theme.{self.__attr} is immutable") 

658 

659 def __delete__(self, instance: object): 

660 raise TypeError(f"Theme.{self.__attr} is immutable") 

661 

662 

663class _ThemeMeta(type): 

664 # BEWARE OF MAGIC! 

665 # 

666 # 

667 # Descriptors 

668 # ----------- 

669 # 

670 # _ThemeMeta.__dict__["colors"] 

671 # this is a `_ReadOnlyDescriptor` that handles access to `Theme.colors`, 

672 # proxying it to `Theme.__dict__["_Theme__colors"]`. 

673 # 

674 # Accessing `Theme.colors` is equivalent to calling 

675 # `_ThemeMeta.__dict__["colors"].__get__(Theme)`, 

676 # which in turn will return `Theme.__dict__["_Theme__colors"]`. 

677 # 

678 # Value for `Theme.__dict__["_Theme__colors"]` is assigned by this metaclass. 

679 # 

680 # Theme.__dict__["colors"] 

681 # this is a `_ReadOnlyDescriptor` that handles access to `theme.colors`, 

682 # proxying it to `theme.__dict__["_Theme__colors"]`. 

683 # 

684 # Accessing `theme.colors` is equivalent to calling 

685 # `Theme.__dict__["colors"].__get__(theme)`, 

686 # which in turn will return `theme.__dict__["_Theme__colors"]`. 

687 # 

688 # If `theme.__dict__` does not contain `"_Theme__colors"`, then it will assign 

689 # `theme.__dict__["_Theme__colors"] = Theme.__dict__["_Theme__colors"].copy()`. 

690 # 

691 # theme.__dict__["colors"] 

692 # this attribute does not exist. Accessing `theme.colors` is handled 

693 # by its descriptor. 

694 # 

695 # 

696 # Data 

697 # ---- 

698 # 

699 # Theme.__dict__["_Theme__colors"] 

700 # this is the data returned when accessing `Theme.colors`. It contains 

701 # an `_ImmutableDict` with combination of all colors from all bases. 

702 # 

703 # Theme.__dict__["_Theme__colors__orig"] 

704 # this is original data assigned to `colors` variable in `Theme`'s namespace. 

705 # 

706 # For example: 

707 # 

708 # class MyTheme(Theme): 

709 # colors = {"foo": "#000000"} 

710 # 

711 # In this class: 

712 # 

713 # - `MyTheme.__dict__["_Theme__colors"]` will contain combination of 

714 # colors defined in `Theme` and in `MyTheme`. 

715 # - `MyTheme.__dict__["_Theme__colors__orig"]` will contain initial dict 

716 # `{"foo": "#000000"}`. 

717 # 

718 # theme.__dict__["_Theme__colors"] 

719 # this is lazily initialized copy of `Theme.__dict__["_Theme__colors"]`; 

720 # `Theme.set_color` will mutate this value. 

721 

722 _managed_attrs = ["msg_decorations_ascii", "msg_decorations_unicode", "colors"] 

723 for _attr in _managed_attrs: 

724 locals()[_attr] = _ReadOnlyDescriptor() 

725 del _attr # type: ignore 

726 

727 def __new__(mcs, name, bases, ns, **kwargs): 

728 # Pop any overrides from class' namespace and save them in `_Theme__attr__orig`. 

729 # Set up read-only descriptors for managed attributes. 

730 for attr in mcs._managed_attrs: 

731 ns[f"_Theme__{attr}__orig"] = ns.pop(attr, {}) 

732 ns[attr] = _ReadOnlyDescriptor() 

733 

734 # Create metaclass instance. 

735 cls = super().__new__(mcs, name, bases, ns, **kwargs) 

736 

737 # Set up class-level data for managed attributes. 

738 for attr in mcs._managed_attrs: 

739 setattr(cls, f"_Theme__{attr}", mcs._collect_data(cls, attr)) 

740 

741 # Patch `__init__` so that it handles `__expected_source`. 

742 if init := cls.__dict__.get("__init__", None): 

743 

744 @functools.wraps(init) 

745 def _wrapped_init(self, *args, **kwargs): 

746 prev_expected_source = self._Theme__expected_source 

747 self._Theme__expected_source = cls 

748 try: 

749 return init(self, *args, **kwargs) 

750 finally: 

751 self._Theme__expected_source = prev_expected_source 

752 

753 setattr(cls, "__init__", _wrapped_init) 

754 

755 return cls 

756 

757 def _collect_data(cls, attr): 

758 attr_orig = f"_Theme__{attr}__orig" 

759 data = {} 

760 sources = {} 

761 for base in reversed(cls.__mro__): 

762 if base_data := base.__dict__.get(attr_orig): 

763 data.update(base_data) 

764 sources.update(dict.fromkeys(base_data, base)) 

765 return _ImmutableDict(data, sources, attr) 

766 

767 

768class Theme(metaclass=_ThemeMeta): 

769 """ 

770 Base class for Yuio themes. 

771 

772 .. warning:: 

773 

774 Do not change theme contents after it was passed to :func:`yuio.io.setup`. 

775 Otherwise there's a risc of race conditions. 

776 

777 """ 

778 

779 msg_decorations_unicode: _t.Mapping[str, str] = {} 

780 """ 

781 Decorative symbols for certain text elements, such as headings, 

782 list items, etc. 

783 

784 This mapping becomes immutable once a theme class is created. The only possible 

785 way to modify it is by using :meth:`~Theme.set_msg_decoration_ascii` 

786 or :meth:`~Theme._set_msg_decoration_ascii_if_not_overridden`. 

787 

788 """ 

789 

790 msg_decorations_ascii: _t.Mapping[str, str] = {} 

791 """ 

792 Like :attr:`~Theme.msg_decorations_unicode`, but suitable for non-unicode terminals. 

793 

794 """ 

795 

796 progress_bar_width: int = 15 

797 """ 

798 Width of a progress bar for :class:`yuio.io.Task`. 

799 

800 """ 

801 

802 spinner_update_rate_ms: int = 200 

803 """ 

804 How often the spinner pattern changes. 

805 

806 """ 

807 

808 separate_headings: bool = True 

809 """ 

810 Whether to print newlines before and after :func:`yuio.io.heading`. 

811 

812 """ 

813 

814 fallback_width: int = 80 

815 """ 

816 Preferred width that will be used if printing to a stream that's redirected 

817 to a file. 

818 

819 """ 

820 

821 colors: _t.Mapping[str, str | yuio.color.Color] = {} 

822 """ 

823 Mapping of color paths to actual colors. 

824 

825 Themes use color paths to describe styles and colors for different 

826 parts of an application. Color paths are similar to file paths, 

827 they use snake case identifiers separated by slashes, and consist of 

828 two parts separated by a colon. 

829 

830 The first part represents an object, i.e. what we're coloring. 

831 

832 The second part represents a context, i.e. what is the state or location 

833 of an object that we're coloring. 

834 

835 For example, a color for the filled part of the task's progress bar 

836 has path ``"task/progressbar/done"``, a color for a text of an error 

837 log record has path ``"log/message:error"``, and a color for a string escape 

838 sequence in a highlighted python code has path ``"hl/str/esc:python"``. 

839 

840 A color at a certain path is propagated to all sub-paths. For example, 

841 if ``"task/progressbar"`` is bold, and ``"task/progressbar/done"`` is green, 

842 the final color will be bold green. 

843 

844 Each color path can be associated with an instance of :class:`~yuio.color.Color` 

845 or with another path. 

846 

847 If path is mapped to a :class:`~yuio.color.Color`, then the path is associated 

848 with that particular color. 

849 

850 If path is mapped to another path, then the path is associated with 

851 the color value for that other path (please don't create recursions here). 

852 

853 You can combine multiple paths within the same mapping by separating them with 

854 whitespaces. In this case colors for those paths are combined. 

855 

856 For example: 

857 

858 .. code-block:: python 

859 

860 colors = { 

861 "heading_color": "bold", 

862 "error_color": "red", 

863 "tb/heading": "heading_color error_color", 

864 } 

865 

866 Here, color of traceback's heading ``"tb/heading"`` will be bold and red. 

867 

868 When deriving from a theme, you can override this mapping. When looking up 

869 colors via :meth:`~Theme.get_color`, base classes will be tried for color, 

870 in order of method resolution. 

871 

872 This mapping becomes immutable once a theme class is created. The only possible 

873 way to modify it is by using :meth:`~Theme.set_color` 

874 or :meth:`~Theme._set_color_if_not_overridden`. 

875 

876 """ 

877 

878 __expected_source: type[Theme] | None = None 

879 """ 

880 When running an ``__init__`` function, this variable will be set to the class 

881 that implemented it, regardless of type of `self`. 

882 

883 That is, inside ``DefaultTheme.__init__``, ``__expected_source`` is set 

884 to ``DefaultTheme``, in ``MyTheme.__init__`` it is ``MyTheme``, etc. 

885 

886 This is possible because ``_ThemeMeta`` wraps any implementation 

887 of ``__init__`` into a wrapper that sets this variable. 

888 

889 """ 

890 

891 def __init__(self): 

892 self.__color_cache: dict[str, yuio.color.Color | None] = {} 

893 

894 def _set_msg_decoration_unicode_if_not_overridden( 

895 self, 

896 name: str, 

897 msg_decoration: str, 

898 /, 

899 ): 

900 """ 

901 Set Unicode message decoration by name, but only if it wasn't overridden 

902 in a subclass. 

903 

904 This method should be called from ``__init__`` implementations 

905 to dynamically set message decorations. It will only set the decoration 

906 if it was not overridden by any child class. 

907 

908 """ 

909 

910 proxy = _t.cast(_ImmutableDict[str, str], self.msg_decorations_unicode) 

911 proxy._set_if_not_overridden( 

912 name, 

913 msg_decoration, 

914 self.__expected_source, 

915 ) 

916 

917 def set_msg_decoration_unicode( 

918 self, 

919 name: str, 

920 msg_decoration: str, 

921 /, 

922 ): 

923 """ 

924 Set Unicode message decoration by name. 

925 

926 """ 

927 

928 proxy = _t.cast(_ImmutableDict[str, str], self.msg_decorations_unicode) 

929 proxy._set( 

930 name, 

931 msg_decoration, 

932 self.__expected_source or type(self), 

933 ) 

934 

935 def _set_msg_decoration_ascii_if_not_overridden( 

936 self, 

937 name: str, 

938 msg_decoration: str, 

939 /, 

940 ): 

941 """ 

942 Set ASCII message decoration by name, but only if it wasn't overridden 

943 in a subclass. 

944 

945 This method should be called from ``__init__`` implementations 

946 to dynamically set message decorations. It will only set the decoration 

947 if it was not overridden by any child class. 

948 

949 """ 

950 

951 proxy = _t.cast(_ImmutableDict[str, str], self.msg_decorations_ascii) 

952 proxy._set_if_not_overridden( 

953 name, 

954 msg_decoration, 

955 self.__expected_source, 

956 ) 

957 

958 def set_msg_decoration_ascii( 

959 self, 

960 name: str, 

961 msg_decoration: str, 

962 /, 

963 ): 

964 """ 

965 Set ASCII message decoration by name. 

966 

967 """ 

968 

969 proxy = _t.cast(_ImmutableDict[str, str], self.msg_decorations_ascii) 

970 proxy._set( 

971 name, 

972 msg_decoration, 

973 self.__expected_source or type(self), 

974 ) 

975 

976 def get_msg_decoration(self, key: str, /, *, is_unicode: bool) -> str: 

977 """ 

978 Get message decoration by name. 

979 

980 """ 

981 

982 msg_decorations = ( 

983 self.msg_decorations_unicode if is_unicode else self.msg_decorations_ascii 

984 ) 

985 return msg_decorations.get(key, "") 

986 

987 def _set_color_if_not_overridden( 

988 self, 

989 path: str, 

990 color: str | yuio.color.Color, 

991 /, 

992 ): 

993 """ 

994 Set color by path, but only if the color was not overridden in a subclass. 

995 

996 This method should be called from ``__init__`` implementations 

997 to dynamically set colors. It will only set the color if it was not overridden 

998 by any child class. 

999 

1000 """ 

1001 

1002 proxy = _t.cast(_ImmutableDict[str, str | yuio.color.Color], self.colors) 

1003 proxy._set_if_not_overridden( 

1004 path, 

1005 color, 

1006 self.__expected_source, 

1007 ) 

1008 self.__color_cache.clear() 

1009 self.__dict__.pop("_Theme__color_tree", None) # type: ignore 

1010 

1011 def set_color( 

1012 self, 

1013 path: str, 

1014 color: str | yuio.color.Color, 

1015 /, 

1016 ): 

1017 """ 

1018 Set color by path. 

1019 

1020 """ 

1021 

1022 proxy = _t.cast(_ImmutableDict[str, str | yuio.color.Color], self.colors) 

1023 proxy._set( 

1024 path, 

1025 color, 

1026 self.__expected_source or type(self), 

1027 ) 

1028 self.__color_cache.clear() 

1029 self.__dict__.pop("_Theme__color_tree", None) # type: ignore 

1030 

1031 @dataclass(kw_only=True, slots=True) 

1032 class __ColorTree: 

1033 """ 

1034 Prefix-like tree that contains all of the theme's colors. 

1035 

1036 """ 

1037 

1038 colors: str | yuio.color.Color = yuio.color.Color.NONE 

1039 """ 

1040 Colors in this node. 

1041 

1042 """ 

1043 

1044 loc: dict[str, Theme.__ColorTree] = dataclasses.field(default_factory=dict) 

1045 """ 

1046 Location part of the tree. 

1047 

1048 """ 

1049 

1050 ctx: dict[str, Theme.__ColorTree] = dataclasses.field(default_factory=dict) 

1051 """ 

1052 Context part of the tree. 

1053 

1054 """ 

1055 

1056 @functools.cached_property 

1057 def __color_tree(self) -> Theme.__ColorTree: 

1058 root = self.__ColorTree() 

1059 

1060 for path, colors in self.colors.items(): 

1061 loc, ctx = self.__parse_path(path) 

1062 

1063 node = root 

1064 

1065 for part in loc: 

1066 if part not in node.loc: 

1067 node.loc[part] = self.__ColorTree() 

1068 node = node.loc[part] 

1069 

1070 for part in ctx: 

1071 if part not in node.ctx: 

1072 node.ctx[part] = self.__ColorTree() 

1073 node = node.ctx[part] 

1074 

1075 node.colors = colors 

1076 

1077 return root 

1078 

1079 @staticmethod 

1080 def __parse_path(path: str, /) -> tuple[list[str], list[str]]: 

1081 path_parts = path.split(":", maxsplit=1) 

1082 if len(path_parts) == 1: 

1083 loc, ctx = path_parts[0], "" 

1084 else: 

1085 loc, ctx = path_parts 

1086 return loc.split("/") if loc else [], ctx.split("/") if ctx else [] 

1087 

1088 def get_color(self, paths: str, /) -> yuio.color.Color: 

1089 """ 

1090 Lookup a color by path. 

1091 

1092 """ 

1093 

1094 color = yuio.color.Color.NONE 

1095 for path in paths.split(): 

1096 color |= self.__get_color(path) 

1097 return color 

1098 

1099 def __get_color(self, path: str, /) -> yuio.color.Color: 

1100 res: yuio.color.Color | None | yuio.Missing = self.__color_cache.get( 

1101 path, yuio.MISSING 

1102 ) 

1103 if res is None: 

1104 warnings.warn(f"recursive color path {path!r}", RecursiveThemeWarning) 

1105 return yuio.color.Color.NONE 

1106 elif res is not yuio.MISSING: 

1107 return res 

1108 

1109 self.__color_cache[path] = None 

1110 if path.startswith("#") and len(path) == 7: 

1111 try: 

1112 res = yuio.color.Color.fore_from_hex(path) 

1113 except ValueError as e: 

1114 warnings.warn(f"invalid color code {path!r}: {e}", ThemeWarning) 

1115 res = yuio.color.Color.NONE 

1116 elif path[:3].lower() == "bg#" and len(path) == 9: 

1117 try: 

1118 res = yuio.color.Color.back_from_hex(path[2:]) 

1119 except ValueError as e: 

1120 warnings.warn(f"invalid color code {path!r}: {e}", ThemeWarning) 

1121 res = yuio.color.Color.NONE 

1122 else: 

1123 loc, ctx = self.__parse_path(path) 

1124 res = self.__get_color_in_loc(self.__color_tree, loc, ctx) 

1125 self.__color_cache[path] = res 

1126 return res 

1127 

1128 def __get_color_in_loc( 

1129 self, node: Theme.__ColorTree, loc: list[str], ctx: list[str] 

1130 ): 

1131 color = yuio.color.Color.NONE 

1132 

1133 for part in loc: 

1134 if part not in node.loc: 

1135 break 

1136 color |= self.__get_color_in_ctx(node, ctx) 

1137 node = node.loc[part] 

1138 

1139 return color | self.__get_color_in_ctx(node, ctx) 

1140 

1141 def __get_color_in_ctx(self, node: Theme.__ColorTree, ctx: list[str]): 

1142 color = yuio.color.Color.NONE 

1143 

1144 for part in ctx: 

1145 if part not in node.ctx: 

1146 break 

1147 color |= self.__get_color_in_node(node) 

1148 node = node.ctx[part] 

1149 

1150 return color | self.__get_color_in_node(node) 

1151 

1152 def __get_color_in_node(self, node: Theme.__ColorTree) -> yuio.color.Color: 

1153 color = yuio.color.Color.NONE 

1154 

1155 if isinstance(node.colors, str): 

1156 color |= self.get_color(node.colors) 

1157 else: 

1158 color |= node.colors 

1159 

1160 return color 

1161 

1162 def to_color( 

1163 self, color_or_path: yuio.color.Color | str | None, / 

1164 ) -> yuio.color.Color: 

1165 """ 

1166 Convert color or color path to color. 

1167 

1168 """ 

1169 

1170 if color_or_path is None: 

1171 return yuio.color.Color.NONE 

1172 elif isinstance(color_or_path, yuio.color.Color): 

1173 return color_or_path 

1174 else: 

1175 return self.get_color(color_or_path) 

1176 

1177 def check(self): 

1178 """ 

1179 Check theme for recursion. 

1180 

1181 This method is slow, and should be called from unit tests of your application. 

1182 

1183 """ 

1184 

1185 if "" in self.colors: 

1186 warnings.warn("colors map contains an empty key", ThemeWarning) 

1187 

1188 for k, v in self.colors.items(): 

1189 if not v: 

1190 warnings.warn(f"color value for path {k!r} is empty", ThemeWarning) 

1191 

1192 err_path = None 

1193 with warnings.catch_warnings(): 

1194 warnings.simplefilter("error", category=RecursiveThemeWarning) 

1195 for k in self.colors: 

1196 try: 

1197 self.get_color(k) 

1198 except RecursiveThemeWarning: 

1199 err_path = k 

1200 if err_path is None: 

1201 return 

1202 

1203 self.__color_cache.clear() 

1204 recursive_path = [] 

1205 get_color_inner = self.__get_color 

1206 

1207 def get_color(path: str): 

1208 recursive_path.append(path) 

1209 return get_color_inner(path) 

1210 

1211 self.__get_color = get_color 

1212 

1213 try: 

1214 with warnings.catch_warnings(): 

1215 warnings.simplefilter("error", category=RecursiveThemeWarning) 

1216 self.get_color(err_path) 

1217 except RecursiveThemeWarning: 

1218 self.__get_color = get_color_inner 

1219 else: 

1220 assert False, ( 

1221 "unreachable, please report hitting this assert " 

1222 "to https://github.com/taminomara/yuio/issues" 

1223 ) 

1224 

1225 raise RecursiveThemeWarning( 

1226 f"infinite recursion in color path {err_path!r}:\n " 

1227 + "\n ".join( 

1228 f"{path!r} -> {self.colors.get(path)!r}" for path in recursive_path[:-1] 

1229 ) 

1230 ) 

1231 

1232 

1233class BaseTheme(Theme): 

1234 """ 

1235 This theme defines :ref:`common colors <common-tags>` that are commonly used 

1236 in :ref:`inline color tags <color-tags>`. 

1237 

1238 """ 

1239 

1240 colors = { 

1241 # 

1242 # Common tags 

1243 # ----------- 

1244 "code": "bold", 

1245 "note": "cyan", 

1246 "strong": "note", 

1247 "em": "italic", 

1248 "path": "underline", 

1249 "flag": "note", 

1250 "kbd": "note", 

1251 "gui": "kbd", 

1252 # 

1253 # Styles 

1254 # ------ 

1255 "bold": yuio.color.Color.STYLE_BOLD, 

1256 "b": "bold", 

1257 "dim": yuio.color.Color.STYLE_DIM, 

1258 "d": "dim", 

1259 "italic": yuio.color.Color.STYLE_ITALIC, 

1260 "i": "italic", 

1261 "underline": yuio.color.Color.STYLE_UNDERLINE, 

1262 "u": "underline", 

1263 "inverse": yuio.color.Color.STYLE_INVERSE, 

1264 # 

1265 # Foreground 

1266 # ---------- 

1267 # Note: we don't have tags for background because it's impossible to guarantee 

1268 # that they'll work nicely with whatever foreground you choose. Prefer using 

1269 # `inverse` instead. 

1270 "normal": yuio.color.Color.FORE_NORMAL, 

1271 "muted": yuio.color.Color.FORE_NORMAL_DIM, 

1272 "black": yuio.color.Color.FORE_BLACK, 

1273 "red": yuio.color.Color.FORE_RED, 

1274 "green": yuio.color.Color.FORE_GREEN, 

1275 "yellow": yuio.color.Color.FORE_YELLOW, 

1276 "blue": yuio.color.Color.FORE_BLUE, 

1277 "magenta": yuio.color.Color.FORE_MAGENTA, 

1278 "cyan": yuio.color.Color.FORE_CYAN, 

1279 "white": yuio.color.Color.FORE_WHITE, 

1280 } 

1281 

1282 

1283class DefaultTheme(BaseTheme): 

1284 """ 

1285 Default Yuio theme. Adapts for terminal background color, 

1286 if one can be detected. 

1287 

1288 This theme defines :ref:`main colors <main-colors>`, which you can override 

1289 by subclassing. All other colors are expressed in terms of main colors, 

1290 so changing a main color will have an effect on the entire theme. 

1291 

1292 """ 

1293 

1294 msg_decorations_ascii = { 

1295 "heading/section": "", 

1296 "heading/1": "# ", 

1297 "heading/2": "", 

1298 "heading/3": "", 

1299 "heading/4": "", 

1300 "heading/5": "", 

1301 "heading/6": "", 

1302 "question": "> ", 

1303 "task": "> ", 

1304 "thematic_break": "-" * 8, 

1305 "list": "* ", 

1306 "quote": "> ", 

1307 "code": " ", 

1308 "admonition/title": "", 

1309 "admonition/body": " ", 

1310 "overflow": "~", 

1311 "progress_bar/start_symbol": "[", 

1312 "progress_bar/end_symbol": "]", 

1313 "progress_bar/done_symbol": "-", 

1314 "progress_bar/pending_symbol": " ", 

1315 "progress_bar/transition_pattern": ">", 

1316 "spinner/pattern": "|||/-\\", 

1317 "spinner/static_symbol": ">", 

1318 "menuselection_separator": "->", 

1319 "hr/1/left_start": "-", 

1320 "hr/1/left_middle": "-", 

1321 "hr/1/left_end": " ", 

1322 "hr/1/middle": "-", 

1323 "hr/1/right_start": " ", 

1324 "hr/1/right_middle": "-", 

1325 "hr/1/right_end": "-", 

1326 "hr/2/left_start": "=", 

1327 "hr/2/left_middle": "=", 

1328 "hr/2/left_end": " ", 

1329 "hr/2/middle": "=", 

1330 "hr/2/right_start": " ", 

1331 "hr/2/right_middle": "=", 

1332 "hr/2/right_end": "=", 

1333 "menu/choice/decoration/active_item": "> ", 

1334 "menu/choice/decoration/deselected_item": "- ", 

1335 "menu/choice/decoration/selected_item": "* ", 

1336 "menu/input/decoration_search": "/ ", 

1337 "menu/input/decoration": "> ", 

1338 "menu/help/key/alt": "M-", 

1339 "menu/help/key/ctrl": "C-", 

1340 "menu/help/key/shift": "S-", 

1341 "menu/help/key/enter": "ret", 

1342 "menu/help/key/escape": "esc", 

1343 "menu/help/key/insert": "ins", 

1344 "menu/help/key/delete": "del", 

1345 "menu/help/key/backspace": "bsp", 

1346 "menu/help/key/tab": "tab", 

1347 "menu/help/key/home": "home", 

1348 "menu/help/key/end": "end", 

1349 "menu/help/key/page_up": "pgup", 

1350 "menu/help/key/page_down": "pgdn", 

1351 "menu/help/key/arrow_up": "up", 

1352 "menu/help/key/arrow_down": "down", 

1353 "menu/help/key/arrow_left": "left", 

1354 "menu/help/key/arrow_right": "right", 

1355 "menu/help/key/space": "space", 

1356 "menu/help/key/f1": "f1", 

1357 "menu/help/key/f2": "f2", 

1358 "menu/help/key/f3": "f3", 

1359 "menu/help/key/f4": "f4", 

1360 "menu/help/key/f5": "f5", 

1361 "menu/help/key/f6": "f6", 

1362 "menu/help/key/f7": "f7", 

1363 "menu/help/key/f8": "f8", 

1364 "menu/help/key/f9": "f9", 

1365 "menu/help/key/f10": "f10", 

1366 "menu/help/key/f11": "f11", 

1367 "menu/help/key/f12": "f12", 

1368 } 

1369 

1370 msg_decorations_unicode = { 

1371 "heading/section": "", 

1372 "heading/1": "⣿ ", 

1373 "heading/2": "", 

1374 "heading/3": "", 

1375 "heading/4": "", 

1376 "heading/5": "", 

1377 "heading/6": "", 

1378 "question": "> ", 

1379 "task": "> ", 

1380 "thematic_break": "╌╌╌╌╌╌╌╌", 

1381 "list": "• ", 

1382 "quote": "> ", 

1383 "code": " ", 

1384 "admonition/title": "", 

1385 "admonition/body": " ", 

1386 "overflow": "…", 

1387 "hr/1/left_start": "─", 

1388 "hr/1/left_middle": "─", 

1389 "hr/1/left_end": "╴", 

1390 "hr/1/middle": "─", 

1391 "hr/1/right_start": "╶", 

1392 "hr/1/right_middle": "─", 

1393 "hr/1/right_end": "─", 

1394 "hr/2/left_start": "━", 

1395 "hr/2/left_middle": "━", 

1396 "hr/2/left_end": "╸", 

1397 "hr/2/middle": "━", 

1398 "hr/2/right_start": "╺", 

1399 "hr/2/right_middle": "━", 

1400 "hr/2/right_end": "━", 

1401 "progress_bar/start_symbol": "", 

1402 "progress_bar/end_symbol": "", 

1403 "progress_bar/done_symbol": "■", # "█", 

1404 "progress_bar/pending_symbol": "□", # " ", 

1405 "progress_bar/transition_pattern": "", # "█▉▊▋▌▍▎▏ ", 

1406 "spinner/pattern": "⣤⣤⣤⠶⠛⠛⠛⠶", 

1407 "spinner/static_symbol": "⣿", 

1408 "menuselection_separator": " → ", 

1409 "menu/input/decoration": "> ", 

1410 "menu/input/decoration_search": "/ ", 

1411 "menu/choice/decoration/active_item": "> ", 

1412 "menu/choice/decoration/selected_item": "◉ ", 

1413 "menu/choice/decoration/deselected_item": "○ ", 

1414 "menu/help/decoration": ":", 

1415 "menu/help/key/alt": "M-", 

1416 "menu/help/key/ctrl": "C-", 

1417 "menu/help/key/shift": "S-", 

1418 "menu/help/key/enter": "ret", 

1419 "menu/help/key/escape": "esc", 

1420 "menu/help/key/insert": "ins", 

1421 "menu/help/key/delete": "del", 

1422 "menu/help/key/backspace": "bsp", 

1423 "menu/help/key/tab": "tab", 

1424 "menu/help/key/home": "home", 

1425 "menu/help/key/end": "end", 

1426 "menu/help/key/page_up": "pgup", 

1427 "menu/help/key/page_down": "pgdn", 

1428 "menu/help/key/arrow_up": "↑", 

1429 "menu/help/key/arrow_down": "↓", 

1430 "menu/help/key/arrow_left": "←", 

1431 "menu/help/key/arrow_right": "→", 

1432 "menu/help/key/space": "␣", 

1433 "menu/help/key/f1": "f1", 

1434 "menu/help/key/f2": "f2", 

1435 "menu/help/key/f3": "f3", 

1436 "menu/help/key/f4": "f4", 

1437 "menu/help/key/f5": "f5", 

1438 "menu/help/key/f6": "f6", 

1439 "menu/help/key/f7": "f7", 

1440 "menu/help/key/f8": "f8", 

1441 "menu/help/key/f9": "f9", 

1442 "menu/help/key/f10": "f10", 

1443 "menu/help/key/f11": "f11", 

1444 "menu/help/key/f12": "f12", 

1445 } 

1446 

1447 colors = { 

1448 "note": "accent_color_2", 

1449 # 

1450 # Main settings 

1451 # ------------- 

1452 # This section controls the overall theme look. 

1453 # Most likely you'll want to change accent colors from here. 

1454 "heading_color": "bold primary_color", 

1455 "primary_color": "normal", 

1456 "accent_color": "magenta", 

1457 "accent_color_2": "cyan", 

1458 "secondary_color": "muted", 

1459 "error_color": "red", 

1460 "warning_color": "yellow", 

1461 "success_color": "green", 

1462 "critical_color": "inverse error_color", 

1463 "low_priority_color_a": "muted", 

1464 "low_priority_color_b": "muted", 

1465 # 

1466 # IO messages and text 

1467 # -------------------- 

1468 "msg/decoration": "secondary_color", 

1469 "msg/decoration:heading": "heading_color accent_color", 

1470 "msg/decoration:thematic_break": "secondary_color", 

1471 "msg/text:code": "primary_color", 

1472 "msg/text:heading": "heading_color", 

1473 "msg/text:heading/1": "accent_color", 

1474 "msg/text:heading/section": "green", 

1475 "msg/text:heading/note": "green", 

1476 "msg/text:question": "heading_color", 

1477 "msg/text:error": "error_color", 

1478 "msg/text:error/note": "green", 

1479 "msg/text:warning": "warning_color", 

1480 "msg/text:success": "heading_color success_color", 

1481 "msg/text:failure": "heading_color error_color", 

1482 "msg/text:info": "primary_color", 

1483 "msg/text:thematic_break": "secondary_color", 

1484 "msg/text:help/tail": "dim", 

1485 "msg/text:admonition/title": "heading_color blue", 

1486 "msg/text:admonition/body": "blue", 

1487 "msg/text:admonition/title/attention": "warning_color", 

1488 "msg/text:admonition/body/attention": "warning_color", 

1489 "msg/text:admonition/title/caution": "warning_color", 

1490 "msg/text:admonition/body/caution": "warning_color", 

1491 "msg/text:admonition/title/danger": "error_color", 

1492 "msg/text:admonition/body/danger": "error_color", 

1493 "msg/text:admonition/title/error": "error_color", 

1494 "msg/text:admonition/body/error": "error_color", 

1495 "msg/text:admonition/title/hint": "success_color", 

1496 "msg/text:admonition/body/hint": "success_color", 

1497 "msg/text:admonition/title/important": "warning_color", 

1498 "msg/text:admonition/body/important": "warning_color", 

1499 "msg/text:admonition/title/seealso": "success_color", 

1500 "msg/text:admonition/body/seealso": "success_color", 

1501 "msg/text:admonition/title/tip": "success_color", 

1502 "msg/text:admonition/body/tip": "success_color", 

1503 "msg/text:admonition/title/warning": "warning_color", 

1504 "msg/text:admonition/body/warning": "warning_color", 

1505 "msg/text:admonition/title/versionadded": "success_color", 

1506 "msg/text:admonition/body/versionadded": "success_color", 

1507 "msg/text:admonition/title/versionchanged": "warning_color", 

1508 "msg/text:admonition/body/versionchanged": "warning_color", 

1509 "msg/text:admonition/title/deprecated": "error_color", 

1510 "msg/text:admonition/body/deprecated": "error_color", 

1511 "msg/text:admonition/title/definition": "primary_color", 

1512 "msg/text:admonition/body/definition": "primary_color", 

1513 "msg/text:admonition/title/field": "primary_color", 

1514 "msg/text:admonition/body/field": "primary_color", 

1515 "msg/text:admonition/title/unknown-dir": "primary_color", 

1516 "msg/text:admonition/body/unknown-dir": "primary_color", 

1517 # 

1518 # Log messages 

1519 # ------------ 

1520 "log/name": "dim accent_color_2", 

1521 "log/pathname": "dim", 

1522 "log/filename": "dim", 

1523 "log/module": "dim", 

1524 "log/lineno": "dim", 

1525 "log/funcName": "dim", 

1526 "log/created": "dim", 

1527 "log/asctime": "dim", 

1528 "log/msecs": "dim", 

1529 "log/relativeCreated": "dim", 

1530 "log/thread": "dim", 

1531 "log/threadName": "dim", 

1532 "log/taskName": "dim", 

1533 "log/process": "dim", 

1534 "log/processName": "dim", 

1535 "log/levelno": "log/levelname", 

1536 "log/levelno:critical": "log/levelname:critical", 

1537 "log/levelno:error": "log/levelname:error", 

1538 "log/levelno:warning": "log/levelname:warning", 

1539 "log/levelno:info": "log/levelname:info", 

1540 "log/levelno:debug": "log/levelname:debug", 

1541 "log/levelname": "heading_color", 

1542 "log/levelname:critical": "critical_color", 

1543 "log/levelname:error": "error_color", 

1544 "log/levelname:warning": "warning_color", 

1545 "log/levelname:info": "success_color", 

1546 "log/levelname:debug": "dim", 

1547 "log/message": "primary_color", 

1548 "log/message:critical": "bold error_color", 

1549 "log/message:debug": "dim", 

1550 "log/colMessage": "log/message", 

1551 "log/colMessage:critical": "log/message:critical", 

1552 "log/colMessage:error": "log/message:error", 

1553 "log/colMessage:warning": "log/message:warning", 

1554 "log/colMessage:info": "log/message:info", 

1555 "log/colMessage:debug": "log/message:debug", 

1556 # 

1557 # Tasks and progress bars 

1558 # ----------------------- 

1559 "task": "secondary_color", 

1560 "task/decoration": "msg/decoration:heading", 

1561 "task/decoration:running": "accent_color", 

1562 "task/decoration:done": "success_color", 

1563 "task/decoration:error": "error_color", 

1564 "task/progressbar/done": "accent_color", 

1565 "task/progressbar/done/start": "term/bright_blue", 

1566 "task/progressbar/done/end": "term/bright_magenta", 

1567 "task/progressbar/pending": "secondary_color", 

1568 "task/heading": "heading_color", 

1569 "task/progress": "secondary_color", 

1570 "task/comment": "primary_color", 

1571 # 

1572 # Syntax highlighting 

1573 # ------------------- 

1574 "hl/kwd": "bold", 

1575 "hl/str": "yellow", 

1576 "hl/str/esc": "accent_color", 

1577 "hl/punct": "secondary_color", 

1578 "hl/comment": "green", 

1579 "hl/lit": "blue", 

1580 "hl/type": "cyan", 

1581 "hl/prog": "bold underline", 

1582 "hl/flag": "flag", 

1583 "hl/meta": "accent_color", 

1584 "hl/added": "green", 

1585 "hl/removed": "red", 

1586 "hl/error": "bold error_color", 

1587 "hl/doctest_marker": "accent_color", 

1588 "hl/doctest_marker/continue": "secondary_color", 

1589 "hl/metavar:sh-usage": "bold", 

1590 "tb/heading": "bold error_color", 

1591 "tb/message": "tb/heading", 

1592 "tb/frame/usr/file/module": "accent_color", 

1593 "tb/frame/usr/file/line": "accent_color", 

1594 "tb/frame/usr/file/path": "accent_color", 

1595 "tb/frame/usr/code": "primary_color", 

1596 "tb/frame/usr/highlight": "error_color", 

1597 "tb/frame/lib": "dim", 

1598 "tb/frame/lib/file/module": "tb/frame/usr/file/module", 

1599 "tb/frame/lib/file/line": "tb/frame/usr/file/line", 

1600 "tb/frame/lib/file/path": "tb/frame/usr/file/path", 

1601 "tb/frame/lib/code": "tb/frame/usr/code", 

1602 "tb/frame/lib/highlight": "tb/frame/usr/highlight", 

1603 # 

1604 # Menu and widgets 

1605 # ---------------- 

1606 "menu/text": "primary_color", 

1607 "menu/text/heading": "menu/text heading_color", 

1608 "menu/text/help_info:help": "low_priority_color_a", 

1609 "menu/text/help_msg:help": "low_priority_color_b", 

1610 "menu/text/help_key:help": "low_priority_color_a", 

1611 "menu/text/help_sep:help": "low_priority_color_b", 

1612 "menu/text/help_key:help_menu": "kbd", 

1613 "menu/text/help_sep:help_menu": "secondary_color", 

1614 "menu/text/esc": "white on_magenta", 

1615 "menu/text/error": "bold underline error_color", 

1616 "menu/text/comment": "accent_color_2", 

1617 "menu/text:choice/active": "accent_color", 

1618 "menu/text:choice/active/selected": "bold", 

1619 "menu/text:choice/normal/selected": "accent_color_2 bold", 

1620 "menu/text:choice/normal/dir": "blue", 

1621 "menu/text:choice/normal/exec": "red", 

1622 "menu/text:choice/normal/symlink": "magenta", 

1623 "menu/text:choice/normal/socket": "green", 

1624 "menu/text:choice/normal/pipe": "yellow", 

1625 "menu/text:choice/normal/block_device": "cyan bold", 

1626 "menu/text:choice/normal/char_device": "yellow bold", 

1627 "menu/text/comment:choice/normal/original": "success_color", 

1628 "menu/text/comment:choice/active/original": "success_color", 

1629 "menu/text/comment:choice/normal/corrected": "error_color", 

1630 "menu/text/comment:choice/active/corrected": "error_color", 

1631 "menu/text/prefix:choice/normal": "primary_color", 

1632 "menu/text/prefix:choice/normal/selected": "accent_color_2 bold", 

1633 "menu/text/prefix:choice/active": "accent_color", 

1634 "menu/text/prefix:choice/active/selected": "bold", 

1635 "menu/text/suffix:choice/normal": "primary_color", 

1636 "menu/text/suffix:choice/normal/selected": "accent_color_2 bold", 

1637 "menu/text/suffix:choice/active": "accent_color", 

1638 "menu/text/suffix:choice/active/selected": "bold", 

1639 "menu/text:choice/status_line": "low_priority_color_b", 

1640 "menu/text:choice/status_line/number": "low_priority_color_a", 

1641 "menu/text/placeholder": "secondary_color", 

1642 "menu/decoration": "accent_color", 

1643 "menu/decoration/comment": "secondary_color", 

1644 "menu/decoration:choice/normal": "menu/text", 

1645 "menu/decoration:choice/normal/selected": "accent_color_2 bold", 

1646 "menu/decoration:choice/active/selected": "bold", 

1647 **{f"term/{name}": yuio.color.Color.NONE for name in _COLOR_NAMES}, 

1648 # 

1649 # Documentation roles 

1650 # ------------------- 

1651 "role/footnote": "secondary_color", 

1652 "role/flag": "flag", 

1653 "role/code": "code", 

1654 "role/literal": "em", 

1655 "role/math": "em", 

1656 "role/abbr": "em", 

1657 "role/command": "em", 

1658 "role/dfn": "em", 

1659 "role/mailheader": "em", 

1660 "role/makevar": "em", 

1661 "role/mimetype": "em", 

1662 "role/newsgroup": "em", 

1663 "role/program": "flag", 

1664 "role/regexp": "code", 

1665 "role/cve": "em", 

1666 "role/cwe": "em", 

1667 "role/pep": "em", 

1668 "role/rfc": "em", 

1669 "role/manpage": "em", 

1670 "role/any": "em", 

1671 "role/doc": "em", 

1672 "role/download": "em", 

1673 "role/envvar": "code", 

1674 "role/keyword": "em", 

1675 "role/numref": "em", 

1676 "role/option": "flag", 

1677 "role/cmdoption": "flag", 

1678 "role/ref": "em", 

1679 "role/term": "em", 

1680 "role/token": "em", 

1681 "role/eq": "em", 

1682 "role/kbd": "kbd", 

1683 "role/guilabel": "note", 

1684 "role/guilabel/accelerator": "underline", 

1685 "role/menuselection": "note", 

1686 # "role/menuselection/separator": "secondary_color", 

1687 "role/menuselection/accelerator": "underline", 

1688 "role/file": "path", 

1689 "role/file/variable": "italic", 

1690 "role/samp": "code", 

1691 "role/samp/variable": "italic", 

1692 "role/cli/cfg": "code", 

1693 "role/cli/field": "code", 

1694 "role/cli/obj": "code", 

1695 "role/cli/env": "code", 

1696 "role/cli/any": "code", 

1697 "role/cli/cmd": "flag", 

1698 "role/cli/flag": "flag", 

1699 "role/cli/arg": "flag", 

1700 "role/cli/opt": "flag", 

1701 "role/cli/cli": "flag", 

1702 "role/unknown": "code", 

1703 } 

1704 """ 

1705 Colors for default theme are separated into several sections. 

1706 

1707 The main section (the first one) has common settings which are referenced 

1708 from all other sections. You'll probably want to override 

1709 

1710 """ 

1711 

1712 def __init__(self, term: yuio.term.Term): 

1713 super().__init__() 

1714 

1715 if (colors := term.terminal_theme) is None: 

1716 return 

1717 

1718 # Gradients look bad in other modes. 

1719 if term.supports_colors_true: 

1720 for name in _COLOR_NAMES: 

1721 self._set_color_if_not_overridden( 

1722 f"term/{name}", yuio.color.Color(fore=getattr(colors, name)) 

1723 ) 

1724 

1725 if colors.lightness == yuio.term.Lightness.UNKNOWN: 

1726 return 

1727 

1728 background = colors.background 

1729 foreground = colors.foreground 

1730 

1731 if colors.lightness is colors.lightness.DARK: 

1732 self._set_color_if_not_overridden( 

1733 "low_priority_color_a", 

1734 yuio.color.Color( 

1735 fore=foreground.match_luminosity(background.lighten(0.30)) 

1736 ), 

1737 ) 

1738 self._set_color_if_not_overridden( 

1739 "low_priority_color_b", 

1740 yuio.color.Color( 

1741 fore=foreground.match_luminosity(background.lighten(0.25)) 

1742 ), 

1743 ) 

1744 else: 

1745 self._set_color_if_not_overridden( 

1746 "low_priority_color_a", 

1747 yuio.color.Color( 

1748 fore=foreground.match_luminosity(background.darken(0.30)) 

1749 ), 

1750 ) 

1751 self._set_color_if_not_overridden( 

1752 "low_priority_color_b", 

1753 yuio.color.Color( 

1754 fore=foreground.match_luminosity(background.darken(0.25)) 

1755 ), 

1756 ) 

1757 

1758 

1759def load( 

1760 term: yuio.term.Term, 

1761 theme_ctor: _t.Callable[[yuio.term.Term], Theme] | None = None, 

1762 /, 

1763) -> Theme: 

1764 """ 

1765 Loads a default theme. 

1766 

1767 """ 

1768 

1769 # NOTE: loading themes from json is beta, don't use it yet. 

1770 

1771 if theme_ctor is None: 

1772 theme_ctor = DefaultTheme 

1773 

1774 if not (path := os.environ.get("YUIO_THEME_PATH")): 

1775 return theme_ctor(term) 

1776 

1777 import yuio.config 

1778 import yuio.parse 

1779 

1780 class ThemeData(yuio.config.Config): 

1781 include: list[str] | str | None = None 

1782 progress_bar_width: _t.Annotated[int, yuio.parse.Ge(0)] | None = None 

1783 spinner_update_rate_ms: _t.Annotated[int, yuio.parse.Ge(0)] | None = None 

1784 separate_headings: bool | None = None 

1785 fallback_width: _t.Annotated[int, yuio.parse.Gt(0)] | None = None 

1786 msg_decorations_unicode: dict[str, str] = yuio.config.field( 

1787 default={}, 

1788 merge=lambda l, r: {**l, **r}, 

1789 ) 

1790 msg_decorations_ascii: dict[str, str] = yuio.config.field( 

1791 default={}, 

1792 merge=lambda l, r: {**l, **r}, 

1793 ) 

1794 colors: dict[str, str] = yuio.config.field( 

1795 default={}, 

1796 merge=lambda l, r: {**l, **r}, 

1797 ) 

1798 

1799 seen = set() 

1800 stack = [pathlib.Path(path)] 

1801 loaded_partials = [] 

1802 while stack: 

1803 path = stack.pop() 

1804 if path in seen: 

1805 continue 

1806 if not path.exists(): 

1807 warnings.warn(f"theme file {path} does not exist", ThemeWarning) 

1808 continue 

1809 if not path.is_file(): 

1810 warnings.warn(f"theme file {path} is not a file", ThemeWarning) 

1811 continue 

1812 try: 

1813 loaded = ThemeData.load_from_json_file(path, ignore_unknown_fields=True) 

1814 except yuio.parse.ParsingError as e: 

1815 warnings.warn(str(e), ThemeWarning) 

1816 continue 

1817 loaded_partials.append(loaded) 

1818 include = loaded.include 

1819 if isinstance(include, str): 

1820 include = [include] 

1821 if include: 

1822 stack.extend([path.parent / new_path for new_path in include]) 

1823 

1824 theme_data = ThemeData() 

1825 for partial in reversed(loaded_partials): 

1826 theme_data.update(partial) 

1827 

1828 theme = theme_ctor(term) 

1829 

1830 if theme_data.progress_bar_width is not None: 

1831 theme.progress_bar_width = theme_data.progress_bar_width 

1832 if theme_data.spinner_update_rate_ms is not None: 

1833 theme.spinner_update_rate_ms = theme_data.spinner_update_rate_ms 

1834 if theme_data.separate_headings is not None: 

1835 theme.separate_headings = theme_data.separate_headings 

1836 if theme_data.fallback_width is not None: 

1837 theme.fallback_width = theme_data.fallback_width 

1838 

1839 for k, v in theme_data.msg_decorations_ascii.items(): 

1840 theme.set_msg_decoration_ascii(k, v) 

1841 for k, v in theme_data.msg_decorations_unicode.items(): 

1842 theme.set_msg_decoration_unicode(k, v) 

1843 for k, v in theme_data.colors.items(): 

1844 theme.set_color(k, v) 

1845 

1846 return theme 

1847 

1848 

1849class TableJunction(IntFlag): 

1850 WEST = 1 << 0 

1851 WEST_ALT = 1 << 1 

1852 SOUTH = 1 << 2 

1853 SOUTH_ALT = 1 << 3 

1854 EAST = 1 << 4 

1855 EAST_ALT = 1 << 5 

1856 NORTH = 1 << 6 

1857 NORTH_ALT = 1 << 7 

1858 ALT_STYLE = 1 << 8 

1859 

1860 def __repr__(self) -> str: 

1861 res = "".join( 

1862 [ 

1863 ["", "n", "", "N"][ 

1864 bool(self & self.NORTH) + 2 * bool(self & self.NORTH_ALT) 

1865 ], 

1866 ["", "e", "", "E"][ 

1867 bool(self & self.EAST) + 2 * bool(self & self.EAST_ALT) 

1868 ], 

1869 ["", "s", "", "S"][ 

1870 bool(self & self.SOUTH) + 2 * bool(self & self.SOUTH_ALT) 

1871 ], 

1872 ["", "w", "", "W"][ 

1873 bool(self & self.WEST) + 2 * bool(self & self.WEST_ALT) 

1874 ], 

1875 ["-", "="][bool(self & self.ALT_STYLE)], 

1876 ] 

1877 ) 

1878 return f"<{self.__class__.__name__} {res}>" 

1879 

1880 

1881# fmt: off 

1882_TABLE_SYMBOLS_UNICODE: dict[int, str] = { 

1883 0x000: " ", 0x040: "╵", 0x0C0: "╹", 0x010: "╶", 0x050: "└", 0x0D0: "┖", 0x030: "╺", 

1884 0x070: "┕", 0x0F0: "┗", 0x001: "╴", 0x041: "┘", 0x0C1: "┚", 0x011: "─", 0x051: "┴", 

1885 0x0D1: "┸", 0x031: "╼", 0x071: "┶", 0x0F1: "┺", 0x003: "╸", 0x043: "┙", 0x0C3: "┛", 

1886 0x013: "╾", 0x053: "┵", 0x0D3: "┹", 0x033: "━", 0x073: "┷", 0x0F3: "┻", 0x004: "╷", 

1887 0x044: "│", 0x0C4: "╿", 0x014: "┌", 0x054: "├", 0x0D4: "┞", 0x034: "┍", 0x074: "┝", 

1888 0x0F4: "┡", 0x005: "┐", 0x045: "┤", 0x0C5: "┦", 0x015: "┬", 0x055: "┼", 0x0D5: "╀", 

1889 0x035: "┮", 0x075: "┾", 0x0F5: "╄", 0x007: "┑", 0x047: "┥", 0x0C7: "┩", 0x017: "┭", 

1890 0x057: "┽", 0x0D7: "╃", 0x037: "┯", 0x077: "┿", 0x0F7: "╇", 0x00C: "╻", 0x04C: "╽", 

1891 0x0CC: "┃", 0x01C: "┎", 0x05C: "┟", 0x0DC: "┠", 0x03C: "┎", 0x07C: "┢", 0x0FC: "┣", 

1892 0x00D: "┒", 0x04D: "┧", 0x0CD: "┨", 0x01D: "┰", 0x05D: "╁", 0x0DD: "╂", 0x03D: "┲", 

1893 0x07D: "╆", 0x0FD: "╊", 0x00F: "┓", 0x04F: "┪", 0x0CF: "┫", 0x01F: "┱", 0x05F: "╅", 

1894 0x0DF: "╉", 0x03F: "┳", 0x07F: "╈", 0x0FF: "╋", 0x100: " ", 0x140: "╵", 0x1C0: "║", 

1895 0x110: "╶", 0x150: "└", 0x1D0: "╙", 0x130: "═", 0x170: "╘", 0x1F0: "╚", 0x101: "╴", 

1896 0x141: "┘", 0x1C1: "╜", 0x111: "─", 0x151: "┴", 0x1D1: "╨", 0x131: "═", 0x171: "╧", 

1897 0x1F1: "╩", 0x103: "╸", 0x143: "╛", 0x1C3: "╝", 0x113: "═", 0x153: "╧", 0x1D3: "╩", 

1898 0x133: "═", 0x173: "╧", 0x1F3: "╩", 0x104: "╷", 0x144: "│", 0x1C4: "║", 0x114: "┌", 

1899 0x154: "├", 0x1D4: "╟", 0x134: "╒", 0x174: "╞", 0x1F4: "╠", 0x105: "┐", 0x145: "┤", 

1900 0x1C5: "╢", 0x115: "┬", 0x155: "┼", 0x1D5: "╫", 0x135: "╤", 0x175: "╪", 0x1F5: "╬", 

1901 0x107: "╕", 0x147: "╡", 0x1C7: "╣", 0x117: "╤", 0x157: "╪", 0x1D7: "╬", 0x137: "╤", 

1902 0x177: "╪", 0x1F7: "╬", 0x10C: "║", 0x14C: "║", 0x1CC: "║", 0x11C: "╓", 0x15C: "╟", 

1903 0x1DC: "╟", 0x13C: "╔", 0x17C: "╠", 0x1FC: "╠", 0x10D: "╖", 0x14D: "╢", 0x1CD: "╢", 

1904 0x11D: "╥", 0x15D: "╫", 0x1DD: "╫", 0x13D: "╦", 0x17D: "╬", 0x1FD: "╬", 0x10F: "╗", 

1905 0x14F: "╣", 0x1CF: "╣", 0x11F: "╦", 0x15F: "╬", 0x1DF: "╬", 0x13F: "╦", 0x17F: "╬", 

1906 0x1FF: "╬", 

1907} 

1908# fmt: on 

1909 

1910# fmt: off 

1911_TABLE_SYMBOLS_ASCII: dict[int, str] = { 

1912 0x000: " ", 0x040: "+", 0x0C0: "+", 0x010: "+", 0x050: "+", 0x0D0: "+", 0x030: "+", 

1913 0x070: "+", 0x0F0: "+", 0x001: "+", 0x041: "+", 0x0C1: "+", 0x011: "-", 0x051: "+", 

1914 0x0D1: "+", 0x031: "+", 0x071: "+", 0x0F1: "+", 0x003: "+", 0x043: "+", 0x0C3: "+", 

1915 0x013: "+", 0x053: "+", 0x0D3: "+", 0x033: "=", 0x073: "+", 0x0F3: "+", 0x004: "+", 

1916 0x044: "|", 0x0C4: "+", 0x014: "+", 0x054: "+", 0x0D4: "+", 0x034: "+", 0x074: "+", 

1917 0x0F4: "+", 0x005: "+", 0x045: "+", 0x0C5: "+", 0x015: "+", 0x055: "+", 0x0D5: "+", 

1918 0x035: "+", 0x075: "+", 0x0F5: "+", 0x007: "+", 0x047: "+", 0x0C7: "+", 0x017: "+", 

1919 0x057: "+", 0x0D7: "+", 0x037: "+", 0x077: "+", 0x0F7: "+", 0x00C: "+", 0x04C: "+", 

1920 0x0CC: "|", 0x01C: "+", 0x05C: "+", 0x0DC: "+", 0x03C: "+", 0x07C: "+", 0x0FC: "+", 

1921 0x00D: "+", 0x04D: "+", 0x0CD: "+", 0x01D: "+", 0x05D: "+", 0x0DD: "+", 0x03D: "+", 

1922 0x07D: "+", 0x0FD: "+", 0x00F: "+", 0x04F: "+", 0x0CF: "+", 0x01F: "+", 0x05F: "+", 

1923 0x0DF: "+", 0x03F: "+", 0x07F: "+", 0x0FF: "+", 0x100: " ", 0x140: "+", 0x1C0: "#", 

1924 0x110: "+", 0x150: "+", 0x1D0: "#", 0x130: "#", 0x170: "#", 0x1F0: "#", 0x101: "+", 

1925 0x141: "+", 0x1C1: "#", 0x111: "-", 0x151: "+", 0x1D1: "#", 0x131: "#", 0x171: "#", 

1926 0x1F1: "#", 0x103: "#", 0x143: "#", 0x1C3: "#", 0x113: "#", 0x153: "#", 0x1D3: "#", 

1927 0x133: "*", 0x173: "#", 0x1F3: "#", 0x104: "+", 0x144: "|", 0x1C4: "#", 0x114: "+", 

1928 0x154: "+", 0x1D4: "#", 0x134: "#", 0x174: "#", 0x1F4: "#", 0x105: "+", 0x145: "+", 

1929 0x1C5: "#", 0x115: "+", 0x155: "+", 0x1D5: "#", 0x135: "#", 0x175: "#", 0x1F5: "#", 

1930 0x107: "#", 0x147: "#", 0x1C7: "#", 0x117: "#", 0x157: "#", 0x1D7: "#", 0x137: "#", 

1931 0x177: "#", 0x1F7: "#", 0x10C: "#", 0x14C: "#", 0x1CC: "*", 0x11C: "#", 0x15C: "#", 

1932 0x1DC: "#", 0x13C: "#", 0x17C: "#", 0x1FC: "#", 0x10D: "#", 0x14D: "#", 0x1CD: "#", 

1933 0x11D: "#", 0x15D: "#", 0x1DD: "#", 0x13D: "#", 0x17D: "#", 0x1FD: "#", 0x10F: "#", 

1934 0x14F: "#", 0x1CF: "#", 0x11F: "#", 0x15F: "#", 0x1DF: "#", 0x13F: "#", 0x17F: "#", 

1935 0x1FF: "#", 

1936} 

1937# fmt: on