Coverage for yuio / theme.py: 98%

357 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-05 11:41 +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/{part}:{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/punct``: punctuation, 

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

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

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

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

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

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

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

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

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

201 

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

203 `tb/message` 

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

205 

206 For highlighted tracebacks: 

207 

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

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

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

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

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

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

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

215 

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

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

218 

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

220 

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

222 level is lowercase name of logging level. 

223 

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

225 

226 .. seealso:: 

227 

228 :class:`yuio.io.Formatter`. 

229 

230.. color-path:: input widget 

231 

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

233 

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

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

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

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

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

239 

240.. color-path:: grid widgets 

241 

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

243 similar widgets: 

244 

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

246 decoration before a grid item, 

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

248 decoration around comments for a grid item, 

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

250 text of a grid item, 

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

252 comment for a grid item, 

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

254 prefix before the main text of a grid item 

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

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

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

258 suffix after the main text of a grid item 

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

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

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

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

263 

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

265 

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

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

268 

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

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

271 

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

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

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

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

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

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

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

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

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

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

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

283 

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

285 

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

287 

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

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

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

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

292 

293.. color-path:: inline help menu 

294 

295 Colors for help items rendered under a widget: 

296 

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

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

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

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

301 

302 

303.. _all-decorations: 

304 

305Decorations 

306----------- 

307 

308.. decoration-path:: `info` 

309 

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

311 

312.. decoration-path:: `warning` 

313 

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

315 

316.. decoration-path:: `error` 

317 

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

319 

320.. decoration-path:: `success` 

321 

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

323 

324.. decoration-path:: `failure` 

325 

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

327 

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

329 

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

331 

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

333 

334 First-level headings in CLI help. 

335 

336.. decoration-path:: `question` 

337 

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

339 

340.. decoration-path:: `list` 

341 

342 Bullets in markdown. 

343 

344.. decoration-path:: `quote` 

345 

346 Quote decorations in markdown. 

347 

348.. decoration-path:: `code` 

349 

350 Code decorations in markdown. 

351 

352.. decoration-path:: `thematic_break` 

353 

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

355 

356.. decoration-path:: `overflow` 

357 

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

359 

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

361 

362 Decorations for progress bars. 

363 

364 Available positions are: 

365 

366 :``start_symbol``: 

367 Start of the progress bar. 

368 :``done_symbol``: 

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

370 :``pending_symbol``: 

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

372 :``end_symbol``: 

373 End of the progress bar. 

374 :``transition_pattern``: 

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

376 parts of the progress bar. 

377 

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

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

380 must be one character wide. 

381 

382 .. raw:: html 

383 

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

385 <div class="highlight"> 

386 <pre class="ascii-graphics"> 

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

388 │└┬───┘│└┬───────┘│ 

389 │ │ │ │ end_symbol 

390 │ │ │ └ pending_symbol 

391 │ │ └ transition_pattern 

392 │ └ done_symbol 

393 └ start_symbol 

394 </pre> 

395 </div> 

396 </div> 

397 

398 **Example:** 

399 

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

401 

402 .. code-block:: python 

403 

404 class BlockProgressTheme(yuio.theme.DefaultTheme): 

405 msg_decorations = { 

406 "progress_bar/start_symbol": "|", 

407 "progress_bar/end_symbol": "|", 

408 "progress_bar/done_symbol": "█", 

409 "progress_bar/pending_symbol": " ", 

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

411 } 

412 

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

414 

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

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

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

418 

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

420 

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

422 

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

424 

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

426 

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

428 

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

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

431 

432 Default theme defines three weights: 

433 

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

435 - ``1`` prints normal ruler, 

436 - ``2`` prints bold ruler. 

437 

438 Available positions are: 

439 

440 :``left_start``: 

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

442 :``left_middle``: 

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

444 :``left_end``: 

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

446 :``middle``: 

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

448 :``right_start``: 

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

450 :``right_middle``: 

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

452 :``right_end``: 

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

454 

455 .. raw:: html 

456 

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

458 <div class="highlight"> 

459 <pre class="ascii-graphics"> 

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

461 │└┬───┘│ │└┬───┘│ 

462 │ │ │ │ │ right_end 

463 │ │ │ │ └ right_middle 

464 │ │ │ └ right_start 

465 │ │ └ left_end 

466 │ └ left_middle 

467 └ left_start 

468 

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

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

471 │ middle right_end 

472 └ left_start 

473 </pre> 

474 </div> 

475 </div> 

476 

477.. decoration-path:: input widget 

478 

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

480 

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

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

483 

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

485 

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

487 

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

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

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

491 

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

493 

494 Decorations for widget help: 

495 

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

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

498 

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

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

501 ``page_down``, ``arrow_up``, ``arrow_down``, ``arrow_left``, ``arrow_right``, 

502 ``space``, ``f1``...\\ ``f12``. 

503 

504""" 

505 

506from __future__ import annotations 

507 

508import dataclasses 

509import functools 

510import os 

511import pathlib 

512import warnings 

513from dataclasses import dataclass 

514from enum import IntFlag 

515 

516import yuio.color 

517import yuio.term 

518 

519from typing import TYPE_CHECKING 

520 

521if TYPE_CHECKING: 

522 import typing_extensions as _t 

523else: 

524 from yuio import _typing as _t 

525 

526__all__ = [ 

527 "BaseTheme", 

528 "DefaultTheme", 

529 "RecursiveThemeWarning", 

530 "TableJunction", 

531 "Theme", 

532 "ThemeWarning", 

533 "load", 

534] 

535 

536K = _t.TypeVar("K") 

537V = _t.TypeVar("V") 

538 

539 

540class ThemeWarning(yuio.YuioWarning): 

541 pass 

542 

543 

544class RecursiveThemeWarning(ThemeWarning): 

545 pass 

546 

547 

548_COLOR_NAMES = [ 

549 "background", 

550 "foreground", 

551 "black", 

552 "bright_black", 

553 "red", 

554 "bright_red", 

555 "green", 

556 "bright_green", 

557 "yellow", 

558 "bright_yellow", 

559 "blue", 

560 "bright_blue", 

561 "magenta", 

562 "bright_magenta", 

563 "cyan", 

564 "bright_cyan", 

565 "white", 

566 "bright_white", 

567] 

568 

569 

570@_t.final 

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

572 def __init__( 

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

574 ): 

575 self.__data = data 

576 self.__sources = sources 

577 self.__attr = attr 

578 

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

580 return self.__data.items() 

581 

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

583 return self.__data.keys() 

584 

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

586 return self.__data.values() 

587 

588 def __len__(self): 

589 return len(self.__data) 

590 

591 def __getitem__(self, key): 

592 return self.__data[key] 

593 

594 def __iter__(self): 

595 return iter(self.__data) 

596 

597 def __contains__(self, key): 

598 return key in self.__data 

599 

600 def __repr__(self): 

601 return repr(self.__data) 

602 

603 def __setitem__(self, key, item): 

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

605 

606 def __delitem__(self, key): 

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

608 

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

610 return self.__class__( 

611 self.__data.copy(), 

612 self.__sources.copy(), 

613 self.__attr, 

614 ) 

615 

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

617 self.__data[key] = value 

618 self.__sources[key] = source 

619 

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

621 if source is None: 

622 raise TypeError( 

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

624 "outside of __init__" 

625 ) 

626 prev_source = self.__sources.get(key) 

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

628 self._set(key, value, source) 

629 

630 

631@_t.final 

632class _ReadOnlyDescriptor: 

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

634 self.__attr = attr 

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

636 

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

638 if instance is None: # pragma: no cover 

639 return self 

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

641 return data 

642 else: 

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

644 instance.__dict__[self.__private_name] = data 

645 return data 

646 

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

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

649 

650 def __delete__(self, instance: object): 

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

652 

653 

654class _ThemeMeta(type): 

655 # BEWARE OF MAGIC! 

656 # 

657 # 

658 # Descriptors 

659 # ----------- 

660 # 

661 # _ThemeMeta.__dict__["colors"] 

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

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

664 # 

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

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

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

668 # 

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

670 # 

671 # Theme.__dict__["colors"] 

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

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

674 # 

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

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

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

678 # 

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

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

681 # 

682 # theme.__dict__["colors"] 

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

684 # by its descriptor. 

685 # 

686 # 

687 # Data 

688 # ---- 

689 # 

690 # Theme.__dict__["_Theme__colors"] 

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

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

693 # 

694 # Theme.__dict__["_Theme__colors__orig"] 

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

696 # 

697 # For example: 

698 # 

699 # class MyTheme(Theme): 

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

701 # 

702 # In this class: 

703 # 

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

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

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

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

708 # 

709 # theme.__dict__["_Theme__colors"] 

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

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

712 

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

714 for _attr in _managed_attrs: 

715 locals()[_attr] = _ReadOnlyDescriptor() 

716 del _attr # type: ignore 

717 

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

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

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

721 for attr in mcs._managed_attrs: 

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

723 ns[attr] = _ReadOnlyDescriptor() 

724 

725 # Create metaclass instance. 

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

727 

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

729 for attr in mcs._managed_attrs: 

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

731 

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

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

734 

735 @functools.wraps(init) 

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

737 prev_expected_source = self._Theme__expected_source 

738 self._Theme__expected_source = cls 

739 try: 

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

741 finally: 

742 self._Theme__expected_source = prev_expected_source 

743 

744 setattr(cls, "__init__", _wrapped_init) 

745 

746 return cls 

747 

748 def _collect_data(cls, attr): 

749 attr_orig = f"_Theme__{attr}__orig" 

750 data = {} 

751 sources = {} 

752 for base in reversed(cls.__mro__): 

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

754 data.update(base_data) 

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

756 return _ImmutableDict(data, sources, attr) 

757 

758 

759class Theme(metaclass=_ThemeMeta): 

760 """ 

761 Base class for Yuio themes. 

762 

763 .. warning:: 

764 

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

766 Otherwise there's a risc of race conditions. 

767 

768 """ 

769 

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

771 """ 

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

773 list items, etc. 

774 

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

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

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

778 

779 """ 

780 

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

782 """ 

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

784 

785 """ 

786 

787 progress_bar_width: int = 15 

788 """ 

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

790 

791 """ 

792 

793 spinner_update_rate_ms: int = 200 

794 """ 

795 How often the spinner pattern changes. 

796 

797 """ 

798 

799 separate_headings: bool = True 

800 """ 

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

802 

803 """ 

804 

805 fallback_width: int = 80 

806 """ 

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

808 to a file. 

809 

810 """ 

811 

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

813 """ 

814 Mapping of color paths to actual colors. 

815 

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

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

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

819 two parts separated by a colon. 

820 

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

822 

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

824 of an object that we're coloring. 

825 

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

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

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

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

830 

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

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

833 the final color will be bold green. 

834 

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

836 or with another path. 

837 

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

839 with that particular color. 

840 

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

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

843 

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

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

846 

847 For example: 

848 

849 .. code-block:: python 

850 

851 colors = { 

852 "heading_color": "bold", 

853 "error_color": "red", 

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

855 } 

856 

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

858 

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

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

861 in order of method resolution. 

862 

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

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

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

866 

867 """ 

868 

869 __expected_source: type[Theme] | None = None 

870 """ 

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

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

873 

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

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

876 

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

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

879 

880 """ 

881 

882 def __init__(self): 

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

884 

885 def _set_msg_decoration_unicode_if_not_overridden( 

886 self, 

887 name: str, 

888 msg_decoration: str, 

889 /, 

890 ): 

891 """ 

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

893 in a subclass. 

894 

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

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

897 if it was not overridden by any child class. 

898 

899 """ 

900 

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

902 proxy._set_if_not_overridden( 

903 name, 

904 msg_decoration, 

905 self.__expected_source, 

906 ) 

907 

908 def set_msg_decoration_unicode( 

909 self, 

910 name: str, 

911 msg_decoration: str, 

912 /, 

913 ): 

914 """ 

915 Set Unicode message decoration by name. 

916 

917 """ 

918 

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

920 proxy._set( 

921 name, 

922 msg_decoration, 

923 self.__expected_source or type(self), 

924 ) 

925 

926 def _set_msg_decoration_ascii_if_not_overridden( 

927 self, 

928 name: str, 

929 msg_decoration: str, 

930 /, 

931 ): 

932 """ 

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

934 in a subclass. 

935 

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

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

938 if it was not overridden by any child class. 

939 

940 """ 

941 

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

943 proxy._set_if_not_overridden( 

944 name, 

945 msg_decoration, 

946 self.__expected_source, 

947 ) 

948 

949 def set_msg_decoration_ascii( 

950 self, 

951 name: str, 

952 msg_decoration: str, 

953 /, 

954 ): 

955 """ 

956 Set ASCII message decoration by name. 

957 

958 """ 

959 

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

961 proxy._set( 

962 name, 

963 msg_decoration, 

964 self.__expected_source or type(self), 

965 ) 

966 

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

968 """ 

969 Get message decoration by name. 

970 

971 """ 

972 

973 msg_decorations = ( 

974 self.msg_decorations_unicode if is_unicode else self.msg_decorations_ascii 

975 ) 

976 return msg_decorations.get(key, "") 

977 

978 def _set_color_if_not_overridden( 

979 self, 

980 path: str, 

981 color: str | yuio.color.Color, 

982 /, 

983 ): 

984 """ 

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

986 

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

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

989 by any child class. 

990 

991 """ 

992 

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

994 proxy._set_if_not_overridden( 

995 path, 

996 color, 

997 self.__expected_source, 

998 ) 

999 self.__color_cache.clear() 

1000 self.__dict__.pop("_Theme__color_tree", None) 

1001 

1002 def set_color( 

1003 self, 

1004 path: str, 

1005 color: str | yuio.color.Color, 

1006 /, 

1007 ): 

1008 """ 

1009 Set color by path. 

1010 

1011 """ 

1012 

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

1014 proxy._set( 

1015 path, 

1016 color, 

1017 self.__expected_source or type(self), 

1018 ) 

1019 self.__color_cache.clear() 

1020 self.__dict__.pop("_Theme__color_tree", None) 

1021 

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

1023 class __ColorTree: 

1024 """ 

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

1026 

1027 """ 

1028 

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

1030 """ 

1031 Colors in this node. 

1032 

1033 """ 

1034 

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

1036 """ 

1037 Location part of the tree. 

1038 

1039 """ 

1040 

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

1042 """ 

1043 Context part of the tree. 

1044 

1045 """ 

1046 

1047 @functools.cached_property 

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

1049 root = self.__ColorTree() 

1050 

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

1052 loc, ctx = self.__parse_path(path) 

1053 

1054 node = root 

1055 

1056 for part in loc: 

1057 if part not in node.loc: 

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

1059 node = node.loc[part] 

1060 

1061 for part in ctx: 

1062 if part not in node.ctx: 

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

1064 node = node.ctx[part] 

1065 

1066 node.colors = colors 

1067 

1068 return root 

1069 

1070 @staticmethod 

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

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

1073 if len(path_parts) == 1: 

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

1075 else: 

1076 loc, ctx = path_parts 

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

1078 

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

1080 """ 

1081 Lookup a color by path. 

1082 

1083 """ 

1084 

1085 color = yuio.color.Color.NONE 

1086 for path in paths.split(): 

1087 color |= self.__get_color(path) 

1088 return color 

1089 

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

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

1092 path, yuio.MISSING 

1093 ) 

1094 if res is None: 

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

1096 return yuio.color.Color.NONE 

1097 elif res is not yuio.MISSING: 

1098 return res 

1099 

1100 self.__color_cache[path] = None 

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

1102 try: 

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

1104 except ValueError as e: 

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

1106 res = yuio.color.Color.NONE 

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

1108 try: 

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

1110 except ValueError as e: 

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

1112 res = yuio.color.Color.NONE 

1113 else: 

1114 loc, ctx = self.__parse_path(path) 

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

1116 self.__color_cache[path] = res 

1117 return res 

1118 

1119 def __get_color_in_loc( 

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

1121 ): 

1122 color = yuio.color.Color.NONE 

1123 

1124 for part in loc: 

1125 if part not in node.loc: 

1126 break 

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

1128 node = node.loc[part] 

1129 

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

1131 

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

1133 color = yuio.color.Color.NONE 

1134 

1135 for part in ctx: 

1136 if part not in node.ctx: 

1137 break 

1138 color |= self.__get_color_in_node(node) 

1139 node = node.ctx[part] 

1140 

1141 return color | self.__get_color_in_node(node) 

1142 

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

1144 color = yuio.color.Color.NONE 

1145 

1146 if isinstance(node.colors, str): 

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

1148 else: 

1149 color |= node.colors 

1150 

1151 return color 

1152 

1153 def to_color( 

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

1155 ) -> yuio.color.Color: 

1156 """ 

1157 Convert color or color path to color. 

1158 

1159 """ 

1160 

1161 if color_or_path is None: 

1162 return yuio.color.Color.NONE 

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

1164 return color_or_path 

1165 else: 

1166 return self.get_color(color_or_path) 

1167 

1168 def check(self): 

1169 """ 

1170 Check theme for recursion. 

1171 

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

1173 

1174 """ 

1175 

1176 if "" in self.colors: 

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

1178 

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

1180 if not v: 

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

1182 

1183 err_path = None 

1184 with warnings.catch_warnings(): 

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

1186 for k in self.colors: 

1187 try: 

1188 self.get_color(k) 

1189 except RecursiveThemeWarning: 

1190 err_path = k 

1191 if err_path is None: 

1192 return 

1193 

1194 self.__color_cache.clear() 

1195 recursive_path = [] 

1196 get_color_inner = self.__get_color 

1197 

1198 def get_color(path: str): 

1199 recursive_path.append(path) 

1200 return get_color_inner(path) 

1201 

1202 self.__get_color = get_color 

1203 

1204 try: 

1205 with warnings.catch_warnings(): 

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

1207 self.get_color(err_path) 

1208 except RecursiveThemeWarning: 

1209 self.__get_color = get_color_inner 

1210 else: 

1211 assert False, ( 

1212 "unreachable, please report hitting this assert " 

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

1214 ) 

1215 

1216 raise RecursiveThemeWarning( 

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

1218 + "\n ".join( 

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

1220 ) 

1221 ) 

1222 

1223 

1224class BaseTheme(Theme): 

1225 """ 

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

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

1228 

1229 """ 

1230 

1231 colors = { 

1232 # 

1233 # Common tags 

1234 # ----------- 

1235 "code": "italic", 

1236 "note": "cyan", 

1237 "path": "code", 

1238 "flag": "note", 

1239 # 

1240 # Styles 

1241 # ------ 

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

1243 "b": "bold", 

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

1245 "d": "dim", 

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

1247 "i": "italic", 

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

1249 "u": "underline", 

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

1251 # 

1252 # Foreground 

1253 # ---------- 

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

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

1256 # `inverse` instead. 

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

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

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

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

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

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

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

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

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

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

1267 } 

1268 

1269 

1270class DefaultTheme(BaseTheme): 

1271 """ 

1272 Default Yuio theme. Adapts for terminal background color, 

1273 if one can be detected. 

1274 

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

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

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

1278 

1279 """ 

1280 

1281 msg_decorations_ascii = { 

1282 "heading/section": "", 

1283 "heading/1": "# ", 

1284 "heading/2": "", 

1285 "heading/3": "", 

1286 "heading/4": "", 

1287 "heading/5": "", 

1288 "heading/6": "", 

1289 "question": "> ", 

1290 "task": "> ", 

1291 "thematic_break": "-" * 8, 

1292 "list": "* ", 

1293 "quote": "> ", 

1294 "code": " " * 8, 

1295 "overflow": "~", 

1296 "progress_bar/start_symbol": "[", 

1297 "progress_bar/end_symbol": "]", 

1298 "progress_bar/done_symbol": "-", 

1299 "progress_bar/pending_symbol": " ", 

1300 "progress_bar/transition_pattern": ">", 

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

1302 "spinner/static_symbol": ">", 

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

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

1305 "hr/1/left_end": " ", 

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

1307 "hr/1/right_start": " ", 

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

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

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

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

1312 "hr/2/left_end": " ", 

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

1314 "hr/2/right_start": " ", 

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

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

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

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

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

1320 "menu/input/decoration_search": "/ ", 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1352 } 

1353 

1354 msg_decorations_unicode = { 

1355 "heading/section": "", 

1356 "heading/1": "⣿ ", 

1357 "heading/2": "", 

1358 "heading/3": "", 

1359 "heading/4": "", 

1360 "heading/5": "", 

1361 "heading/6": "", 

1362 "question": "> ", 

1363 "task": "> ", 

1364 "thematic_break": "╌╌╌╌╌╌╌╌", 

1365 "list": "• ", 

1366 "quote": "> ", 

1367 "code": " " * 8, 

1368 "overflow": "…", 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1383 "progress_bar/start_symbol": "", 

1384 "progress_bar/end_symbol": "", 

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

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

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

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

1389 "spinner/static_symbol": "⣿", 

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

1391 "menu/input/decoration_search": "/ ", 

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

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

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

1395 "menu/help/decoration": ":", 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1426 } 

1427 

1428 colors = { 

1429 "note": "accent_color_2", 

1430 # 

1431 # Main settings 

1432 # ------------- 

1433 # This section controls the overall theme look. 

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

1435 "heading_color": "bold primary_color", 

1436 "primary_color": "normal", 

1437 "accent_color": "magenta", 

1438 "accent_color_2": "cyan", 

1439 "secondary_color": "muted", 

1440 "error_color": "red", 

1441 "warning_color": "yellow", 

1442 "success_color": "green", 

1443 "critical_color": "inverse error_color", 

1444 "low_priority_color_a": "muted", 

1445 "low_priority_color_b": "muted", 

1446 # 

1447 # IO messages and text 

1448 # -------------------- 

1449 "msg/decoration": "secondary_color", 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1465 # 

1466 # Log messages 

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

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

1469 "log/pathname": "dim", 

1470 "log/filename": "dim", 

1471 "log/module": "dim", 

1472 "log/lineno": "dim", 

1473 "log/funcName": "dim", 

1474 "log/created": "dim", 

1475 "log/asctime": "dim", 

1476 "log/msecs": "dim", 

1477 "log/relativeCreated": "dim", 

1478 "log/thread": "dim", 

1479 "log/threadName": "dim", 

1480 "log/taskName": "dim", 

1481 "log/process": "dim", 

1482 "log/processName": "dim", 

1483 "log/levelno": "log/levelname", 

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

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

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

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

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

1489 "log/levelname": "heading_color", 

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

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

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

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

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

1495 "log/message": "primary_color", 

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

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

1498 "log/colMessage": "log/message", 

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

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

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

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

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

1504 # 

1505 # Tasks and progress bars 

1506 # ----------------------- 

1507 "task": "secondary_color", 

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

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

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

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

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

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

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

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

1516 "task/heading": "heading_color", 

1517 "task/progress": "secondary_color", 

1518 "task/comment": "primary_color", 

1519 # 

1520 # Syntax highlighting 

1521 # ------------------- 

1522 "hl/kwd": "bold", 

1523 "hl/str": "yellow", 

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

1525 "hl/punct": "secondary_color", 

1526 "hl/comment": "green", 

1527 "hl/lit": "blue", 

1528 "hl/type": "cyan", 

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

1530 "hl/flag": "flag", 

1531 "hl/meta": "accent_color", 

1532 "hl/added": "green", 

1533 "hl/removed": "red", 

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

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

1536 "tb/message": "tb/heading", 

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

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

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

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

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

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

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

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

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

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

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

1548 # 

1549 # Menu and widgets 

1550 # ---------------- 

1551 "menu/text": "primary_color", 

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

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

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

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

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

1557 "menu/text/help_key:help_menu": "accent_color_2", 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1587 "menu/decoration": "accent_color", 

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

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

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

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

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

1593 } 

1594 """ 

1595 Colors for default theme are separated into several sections. 

1596 

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

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

1599 

1600 """ 

1601 

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

1603 super().__init__() 

1604 

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

1606 return 

1607 

1608 # Gradients look bad in other modes. 

1609 if term.supports_colors_true: 

1610 for name in _COLOR_NAMES: 

1611 self._set_color_if_not_overridden( 

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

1613 ) 

1614 

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

1616 return 

1617 

1618 background = colors.background 

1619 foreground = colors.foreground 

1620 

1621 if colors.lightness is colors.lightness.DARK: 

1622 self._set_color_if_not_overridden( 

1623 "low_priority_color_a", 

1624 yuio.color.Color( 

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

1626 ), 

1627 ) 

1628 self._set_color_if_not_overridden( 

1629 "low_priority_color_b", 

1630 yuio.color.Color( 

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

1632 ), 

1633 ) 

1634 else: 

1635 self._set_color_if_not_overridden( 

1636 "low_priority_color_a", 

1637 yuio.color.Color( 

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

1639 ), 

1640 ) 

1641 self._set_color_if_not_overridden( 

1642 "low_priority_color_b", 

1643 yuio.color.Color( 

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

1645 ), 

1646 ) 

1647 

1648 

1649def load( 

1650 term: yuio.term.Term, 

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

1652 /, 

1653) -> Theme: 

1654 """ 

1655 Loads a default theme. 

1656 

1657 """ 

1658 

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

1660 

1661 if theme_ctor is None: 

1662 theme_ctor = DefaultTheme 

1663 

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

1665 return theme_ctor(term) 

1666 

1667 import yuio.config 

1668 import yuio.parse 

1669 

1670 class ThemeData(yuio.config.Config): 

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

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

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

1674 separate_headings: bool | None = None 

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

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

1677 default={}, 

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

1679 ) 

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

1681 default={}, 

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

1683 ) 

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

1685 default={}, 

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

1687 ) 

1688 

1689 seen = set() 

1690 stack = [pathlib.Path(path)] 

1691 loaded_partials = [] 

1692 while stack: 

1693 path = stack.pop() 

1694 if path in seen: 

1695 continue 

1696 if not path.exists(): 

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

1698 continue 

1699 if not path.is_file(): 

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

1701 continue 

1702 try: 

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

1704 except yuio.parse.ParsingError as e: 

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

1706 continue 

1707 loaded_partials.append(loaded) 

1708 include = loaded.include 

1709 if isinstance(include, str): 

1710 include = [include] 

1711 if include: 

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

1713 

1714 theme_data = ThemeData() 

1715 for partial in reversed(loaded_partials): 

1716 theme_data.update(partial) 

1717 

1718 theme = theme_ctor(term) 

1719 

1720 if theme_data.progress_bar_width is not None: 

1721 theme.progress_bar_width = theme_data.progress_bar_width 

1722 if theme_data.spinner_update_rate_ms is not None: 

1723 theme.spinner_update_rate_ms = theme_data.spinner_update_rate_ms 

1724 if theme_data.separate_headings is not None: 

1725 theme.separate_headings = theme_data.separate_headings 

1726 if theme_data.fallback_width is not None: 

1727 theme.fallback_width = theme_data.fallback_width 

1728 

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

1730 theme.set_msg_decoration_ascii(k, v) 

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

1732 theme.set_msg_decoration_unicode(k, v) 

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

1734 theme.set_color(k, v) 

1735 

1736 return theme 

1737 

1738 

1739class TableJunction(IntFlag): 

1740 WEST = 1 << 0 

1741 WEST_ALT = 1 << 1 

1742 SOUTH = 1 << 2 

1743 SOUTH_ALT = 1 << 3 

1744 EAST = 1 << 4 

1745 EAST_ALT = 1 << 5 

1746 NORTH = 1 << 6 

1747 NORTH_ALT = 1 << 7 

1748 ALT_STYLE = 1 << 8 

1749 

1750 def __repr__(self) -> str: 

1751 res = "".join( 

1752 [ 

1753 ["", "n", "", "N"][ 

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

1755 ], 

1756 ["", "e", "", "E"][ 

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

1758 ], 

1759 ["", "s", "", "S"][ 

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

1761 ], 

1762 ["", "w", "", "W"][ 

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

1764 ], 

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

1766 ] 

1767 ) 

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

1769 

1770 

1771# fmt: off 

1772_TABLE_SYMBOLS_UNICODE: dict[int, str] = { 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1796 0x1FF: "╬", 

1797} 

1798# fmt: on 

1799 

1800# fmt: off 

1801_TABLE_SYMBOLS_ASCII: dict[int, str] = { 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1825 0x1FF: "#", 

1826} 

1827# fmt: on