termcore 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. termcore/__init__.py +6 -0
  2. termcore/cli/__init__.py +3 -0
  3. termcore/cli/output.py +70 -0
  4. termcore/io/__init__.py +7 -0
  5. termcore/io/app_state_storage.py +185 -0
  6. termcore/io/database.py +655 -0
  7. termcore/io/errors.py +71 -0
  8. termcore/io/file.py +263 -0
  9. termcore/io/textfile.py +92 -0
  10. termcore/py.typed +0 -0
  11. termcore/tui/__init__.py +9 -0
  12. termcore/tui/custom_bindings.py +551 -0
  13. termcore/tui/custom_widgets/__init__.py +1 -0
  14. termcore/tui/custom_widgets/custom_checkbox.py +41 -0
  15. termcore/tui/custom_widgets/custom_data_table.py +247 -0
  16. termcore/tui/custom_widgets/custom_selection.py +80 -0
  17. termcore/tui/custom_widgets/multiline_footer.py +240 -0
  18. termcore/tui/question_screen.py +107 -0
  19. termcore/tui/theme_loader.py +495 -0
  20. termcore/tui/themes/__init__.py +2 -0
  21. termcore/tui/themes/classic-black-saturated/style.css +57 -0
  22. termcore/tui/themes/classic-black-saturated/theme.py +22 -0
  23. termcore/tui/themes/classic-black-v1/style.css +57 -0
  24. termcore/tui/themes/classic-black-v1/theme.py +22 -0
  25. termcore/tui/themes/classic-black-v2/style.css +57 -0
  26. termcore/tui/themes/classic-black-v2/theme.py +24 -0
  27. termcore/tui/themes/classic-blue/theme.py +21 -0
  28. termcore/tui/themes/compact-gray/style.tcss +175 -0
  29. termcore/tui/themes/compact-gray/theme.py +22 -0
  30. termcore/tui/themes/mnml-black/style.css +57 -0
  31. termcore/tui/themes/mnml-black/theme.py +21 -0
  32. termcore/tui/themes/mnml-deepblack/style.css +57 -0
  33. termcore/tui/themes/mnml-deepblack/theme.py +21 -0
  34. termcore/tui/themes/pure-amber/theme.py +18 -0
  35. termcore/tui/themes/pure-black/theme.py +18 -0
  36. termcore/tui/themes/pure-blue/theme.py +18 -0
  37. termcore/tui/themes/pure-green/theme.py +18 -0
  38. termcore/tui/themes/pure-sweet16/theme.py +20 -0
  39. termcore/tui/themes/xplore-black/theme.py +18 -0
  40. termcore/tui/themes/xplore-blue/theme.py +18 -0
  41. termcore/tui/themes/xplore-blue-muted/theme.py +18 -0
  42. termcore/tui/themes/xplore-teal/theme.py +18 -0
  43. termcore/util/__init__.py +10 -0
  44. termcore/util/datetime.py +173 -0
  45. termcore/util/debug.py +196 -0
  46. termcore/util/index.py +112 -0
  47. termcore/util/logger.py +36 -0
  48. termcore/util/singleton.py +46 -0
  49. termcore/util/string.py +263 -0
  50. termcore/util/validation.py +31 -0
  51. termcore/util/version.py +56 -0
  52. termcore-0.2.0.dist-info/METADATA +504 -0
  53. termcore-0.2.0.dist-info/RECORD +55 -0
  54. termcore-0.2.0.dist-info/WHEEL +4 -0
  55. termcore-0.2.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,495 @@
1
+ """Discovery, registration and stylesheet handling of Textual themes."""
2
+
3
+ import importlib.util
4
+ import json
5
+ import logging
6
+ from dataclasses import dataclass, replace
7
+ from pathlib import Path
8
+ from types import ModuleType
9
+ from typing import cast
10
+
11
+ from textual.app import App
12
+ from textual.theme import Theme
13
+
14
+ __all__ = [
15
+ "DEFAULT_CUSTOM_THEME_PREFIX",
16
+ "DEFAULT_TERMCORE_THEME_PREFIX",
17
+ "SCRIPT_DIR",
18
+ "STANDARD_THEMES_DIR",
19
+ "ThemeData",
20
+ "ThemeLoader",
21
+ ]
22
+
23
+ DEFAULT_TERMCORE_THEME_PREFIX = "TERMCORE_"
24
+ DEFAULT_CUSTOM_THEME_PREFIX = "CUSTOM_"
25
+ SCRIPT_DIR = Path(__file__).parent.parent
26
+ STANDARD_THEMES_DIR = SCRIPT_DIR / "tui/themes"
27
+
28
+ _logger = logging.getLogger(__name__)
29
+
30
+
31
+ @dataclass(frozen=True, slots=True)
32
+ class ThemeData:
33
+ """Data class to hold information of a single theme."""
34
+
35
+ prefix: str
36
+ textual_theme: Theme
37
+ css_files: list[str] | None = None
38
+
39
+
40
+ class ThemeLoader:
41
+ """
42
+ Loads and manages themes for Textual applications.
43
+
44
+ The themes are expected to be in subfolders of the theme folder, each
45
+ containing a `theme.py` file defining a `TEXTUAL_THEME` variable.
46
+ Additionally, any number of `.css` or `.tcss` files can be included in the
47
+ theme folder.
48
+
49
+ A theme is identified by the `name` of its `TEXTUAL_THEME` plus the prefix
50
+ it is registered under; the name of its folder carries no meaning and is
51
+ free to differ.
52
+
53
+ This class dynamically imports the theme modules, registers them and makes
54
+ them available for use in the application.
55
+ """
56
+
57
+ _theme_folder: str | None
58
+ _termcore_theme_prefix: str
59
+ _custom_theme_prefix: str
60
+ _theme_names: list[str]
61
+ _theme_data: dict[str, ThemeData]
62
+
63
+
64
+ def __init__(
65
+ self, theme_folder: str | None = None,
66
+ termcore_theme_prefix: str = DEFAULT_TERMCORE_THEME_PREFIX,
67
+ custom_theme_prefix: str = DEFAULT_CUSTOM_THEME_PREFIX
68
+ ) -> None:
69
+ """
70
+ Loads the themes that ship with termcore, plus the given folder.
71
+
72
+ Use `ThemeLoader.custom_only` for a loader that leaves the bundled
73
+ themes out.
74
+ """
75
+ self._configure(
76
+ theme_folder, termcore_theme_prefix, custom_theme_prefix
77
+ )
78
+ self._load_standard_themes()
79
+ self._load_custom_themes()
80
+ self._theme_names.sort()
81
+
82
+ @classmethod
83
+ def custom_only(
84
+ cls, theme_folder: str,
85
+ termcore_theme_prefix: str = DEFAULT_TERMCORE_THEME_PREFIX,
86
+ custom_theme_prefix: str = DEFAULT_CUSTOM_THEME_PREFIX
87
+ ) -> "ThemeLoader":
88
+ """
89
+ Returns a loader for the given folder alone, without termcore's themes.
90
+
91
+ Parameters
92
+ ----------
93
+ theme_folder : str
94
+ Path to the folder holding the theme directories.
95
+ termcore_theme_prefix : str, optional
96
+ Kept so that a later comparison against a bundled theme name
97
+ still resolves, even though none are loaded.
98
+ custom_theme_prefix : str, optional
99
+ Prefix the themes of this folder are registered under.
100
+
101
+ Returns
102
+ -------
103
+ ThemeLoader
104
+ A loader holding only the themes of `theme_folder`.
105
+ """
106
+ # A factory of this very class, which is what SLF001 cannot tell
107
+ # apart from reaching into a foreign object.
108
+ loader = cls.__new__(cls)
109
+ loader._configure( # noqa: SLF001
110
+ theme_folder, termcore_theme_prefix, custom_theme_prefix
111
+ )
112
+ loader._load_custom_themes() # noqa: SLF001
113
+ loader._theme_names.sort() # noqa: SLF001
114
+
115
+ return loader
116
+
117
+ def _configure(
118
+ self, theme_folder: str | None,
119
+ termcore_theme_prefix: str,
120
+ custom_theme_prefix: str
121
+ ) -> None:
122
+ """Stores the settings and starts from an empty registry."""
123
+ self._theme_folder = theme_folder
124
+ self._termcore_theme_prefix = termcore_theme_prefix
125
+ self._custom_theme_prefix = custom_theme_prefix
126
+ self._theme_names = []
127
+ self._theme_data = {}
128
+
129
+ def _load_standard_themes(self) -> None:
130
+ """Loads and registers the themes that ship with termcore."""
131
+ self._load_folder(
132
+ self._termcore_theme_prefix, STANDARD_THEMES_DIR.resolve()
133
+ )
134
+
135
+ def _load_custom_themes(self) -> None:
136
+ """Loads and registers the themes of the configured folder, if any."""
137
+ if not self._theme_folder:
138
+ return
139
+
140
+ self._load_folder(
141
+ self._custom_theme_prefix, Path(self._theme_folder).resolve()
142
+ )
143
+
144
+ def _load_folder(self, prefix: str, theme_folder_path: Path) -> None:
145
+ """Loads every theme directory below the given folder."""
146
+ if not theme_folder_path.is_dir():
147
+ _logger.warning(
148
+ "Theme folder %r not found. Skipping.", str(theme_folder_path)
149
+ )
150
+ return
151
+
152
+ count_before = len(self._theme_names)
153
+ self._process_themes(prefix, theme_folder_path)
154
+ _logger.info(
155
+ "Found %d themes in %r",
156
+ len(self._theme_names) - count_before, str(theme_folder_path)
157
+ )
158
+
159
+ def _process_themes(self, prefix: str, theme_folder_path: Path) -> None:
160
+ """Imports and registers themes from the given folder path."""
161
+ # Sorted, so that a refused duplicate is always the same one
162
+ for full_path in sorted(theme_folder_path.iterdir()):
163
+ if full_path.name.startswith((".", "_")) \
164
+ or not full_path.is_dir():
165
+ continue
166
+
167
+ self._import_and_register_theme(prefix, full_path)
168
+
169
+ def _import_and_register_theme(
170
+ self, prefix: str, theme_folder_path: Path
171
+ ) -> None:
172
+ """
173
+ Imports a theme module and registers its theme.
174
+
175
+ Raises
176
+ ------
177
+ Exception
178
+ If any error occurs while executing the theme module.
179
+ """
180
+ folder_name = theme_folder_path.name
181
+ theme_file = theme_folder_path / "theme.py"
182
+ if not theme_file.is_file():
183
+ _logger.warning(
184
+ "Skipping theme folder %r (no theme.py)", folder_name
185
+ )
186
+ return
187
+
188
+ try:
189
+ theme_module = self._import_theme_module(theme_file)
190
+ textual_theme = getattr(theme_module, "TEXTUAL_THEME", None)
191
+
192
+ # Abort if no TEXTUAL_THEME variable is defined
193
+ if not isinstance(textual_theme, Theme):
194
+ _logger.warning(
195
+ "Skipping theme folder %r (no TEXTUAL_THEME defined)",
196
+ folder_name
197
+ )
198
+ return
199
+
200
+ # Register the theme
201
+ css_files = self._get_css_files_for_theme(theme_folder_path)
202
+ self._save_theme_data(prefix, textual_theme, css_files)
203
+ except Exception:
204
+ _logger.exception("Error loading theme folder %r", folder_name)
205
+
206
+ def _import_theme_module(self, theme_file: Path) -> ModuleType:
207
+ """
208
+ Imports a theme.py by path, leaving sys.path and sys.modules alone.
209
+
210
+ Loading by path is what keeps theme folders independent of each
211
+ other. Importing them as a package named after the folder made the
212
+ first folder loaded in a process shadow every later one of the same
213
+ name, so a second application lost the themes of the toolkit.
214
+ """
215
+ spec = importlib.util.spec_from_file_location(
216
+ f"_termcore_theme_{theme_file.parent.name}", theme_file
217
+ )
218
+ if spec is None or spec.loader is None:
219
+ raise ImportError(f"Cannot load a theme module from {theme_file}")
220
+
221
+ theme_module = importlib.util.module_from_spec(spec)
222
+ spec.loader.exec_module(theme_module)
223
+ return theme_module
224
+
225
+ def _get_css_files_for_theme(self, theme_folder_path: Path) -> list[str]:
226
+ """Generates a list of CSS files in the given folder."""
227
+ return [
228
+ str(path) for path in sorted(theme_folder_path.iterdir())
229
+ if path.suffix in (".css", ".tcss")
230
+ ]
231
+
232
+ def _save_theme_data(
233
+ self, prefix: str,
234
+ theme_instance: Theme,
235
+ css_files: list[str] | None = None
236
+ ) -> None:
237
+ """
238
+ Stores the theme data and records its registered name.
239
+
240
+ The prefix is applied to a copy of the theme, so that the
241
+ `TEXTUAL_THEME` constant of the imported module keeps the name it was
242
+ written with and repeated registration cannot stack prefixes.
243
+ """
244
+ registered_name = f"{prefix}{theme_instance.name}"
245
+ if registered_name in self._theme_data:
246
+ _logger.error(
247
+ "Duplicate theme %r, keeping the one that was loaded first",
248
+ registered_name
249
+ )
250
+ return
251
+
252
+ self._theme_names.append(registered_name)
253
+ self._theme_data[registered_name] = ThemeData(
254
+ prefix=prefix,
255
+ textual_theme=replace(
256
+ theme_instance,
257
+ name=registered_name,
258
+ variables=dict(theme_instance.variables)
259
+ ),
260
+ css_files=css_files
261
+ )
262
+ _logger.info("Registered theme: %r", registered_name)
263
+
264
+ def get_previously_used_theme(
265
+ self, theme_config_file: Path, default_theme_name: str
266
+ ) -> str:
267
+ """
268
+ Returns the name of the previously used theme from the config file.
269
+
270
+ Parameters
271
+ ----------
272
+ theme_config_file : Path
273
+ Path to the config file which contains the name of the theme.
274
+ default_theme_name : str
275
+ The default theme name to return if no previous theme is found.
276
+
277
+ Returns
278
+ -------
279
+ str
280
+ The name of the last used theme or the default name.
281
+
282
+ Raises
283
+ ------
284
+ json.JSONDecodeError
285
+ If the config file contains invalid JSON.
286
+ IOError
287
+ If there's an error reading the config file.
288
+ """
289
+ if theme_config_file.exists():
290
+ try:
291
+ with theme_config_file.open() as f:
292
+ config = cast("dict[str, str]", json.load(f))
293
+ if "theme" not in config:
294
+ _logger.warning(
295
+ "Invalid theme config format in %r",
296
+ str(theme_config_file)
297
+ )
298
+ return default_theme_name
299
+ return config["theme"]
300
+ except (OSError, json.JSONDecodeError):
301
+ return default_theme_name
302
+ return default_theme_name
303
+
304
+ def register_themes_in_textual_app(self, app: App[None]) -> None:
305
+ """
306
+ Registers all loaded themes in the given Textual application.
307
+
308
+ Registration reads the stored themes without modifying them, so the
309
+ same loader can be registered in more than one application.
310
+
311
+ Parameters
312
+ ----------
313
+ app : App
314
+ The instance of the Textual application.
315
+ """
316
+ # Sort themes, first TERMCORE_THEME_PREFIX, then CUSTOM_THEME_PREFIX
317
+ self._theme_names.sort(key=self._registration_order)
318
+
319
+ # Loop through name list instead of dict to keep alphabetic order
320
+ for theme_name in self._theme_names:
321
+ app.register_theme(self._theme_data[theme_name].textual_theme)
322
+
323
+ def _registration_order(self, theme_name: str) -> tuple[int, str]:
324
+ """Sorts termcore themes before custom ones, then alphabetically."""
325
+ is_custom = \
326
+ self._theme_data[theme_name].prefix != self._termcore_theme_prefix
327
+ return int(is_custom), theme_name
328
+
329
+ def set_previous_theme_in_textual_app(
330
+ self, app: App[None], default_theme_name: str, theme_config_file: Path
331
+ ) -> None:
332
+ """
333
+ Set the previously used theme in the given Textual application.
334
+
335
+ Parameters
336
+ ----------
337
+ app : App
338
+ The instance of the Textual application.
339
+ default_theme_name : str
340
+ The default theme name to use if no previous theme is found.
341
+ theme_config_file : Path
342
+ Path to the config file containing the previous theme.
343
+ """
344
+ theme_name = self.get_previously_used_theme(
345
+ theme_config_file, default_theme_name
346
+ )
347
+
348
+ # A theme removed between two runs is the ordinary case, and a caller
349
+ # that passes a default expects it to be used rather than ignored
350
+ if theme_name not in app.available_themes:
351
+ _logger.warning(
352
+ "Stored theme %r is not registered, falling back to %r",
353
+ theme_name, default_theme_name
354
+ )
355
+ theme_name = default_theme_name
356
+
357
+ if theme_name not in app.available_themes:
358
+ _logger.error(
359
+ "Default theme %r is not registered either, leaving the "
360
+ "theme as it is", theme_name
361
+ )
362
+ return
363
+
364
+ app.theme = theme_name
365
+ _logger.info("Set previous theme: %r", theme_name)
366
+
367
+ def save_theme_to_config(
368
+ self, theme_name: str, theme_config_file: Path
369
+ ) -> None:
370
+ """
371
+ Saves the name of the active theme in the config file.
372
+
373
+ Raises
374
+ ------
375
+ IOError
376
+ If there's an error writing to the config file.
377
+ """
378
+ try:
379
+ with theme_config_file.open("w") as f:
380
+ json.dump({"theme": theme_name}, f)
381
+ except OSError:
382
+ _logger.exception("Could not save theme config")
383
+
384
+ def load_theme_css(self, theme_name: str, app: App[None]) -> None:
385
+ """
386
+ Loads the CSS files for the current theme.
387
+
388
+ A theme without a stylesheet and a theme this loader never registered
389
+ (one of Textual's built-ins, for instance) are both normal: the CSS of
390
+ the previous theme is removed and nothing is loaded in its place.
391
+
392
+ Parameters
393
+ ----------
394
+ theme_name : str
395
+ The registered name of the theme, including its prefix.
396
+ app : App
397
+ The instance of the Textual application.
398
+ """
399
+ # Remove CSS from previous theme
400
+ self._remove_all_theme_css(app)
401
+
402
+ theme_data = self._theme_data.get(theme_name)
403
+ css_files = theme_data.css_files if theme_data else None
404
+ if css_files:
405
+ self._read_css_files(css_files, app)
406
+ else:
407
+ _logger.debug("Theme %r has no stylesheet", theme_name)
408
+
409
+ self._apply_stylesheet(app)
410
+
411
+ def _read_css_files(self, css_files: list[str], app: App[None]) -> None:
412
+ """Reads the given CSS files into the app's stylesheet."""
413
+ for css_file in css_files:
414
+ try:
415
+ app.stylesheet.read(css_file)
416
+ _logger.debug("Loaded CSS file: %r", css_file)
417
+ except Exception:
418
+ _logger.exception("Error loading CSS file %r", css_file)
419
+
420
+ def _apply_stylesheet(self, app: App[None]) -> None:
421
+ """
422
+ Re-parses the stylesheet so that changes take effect.
423
+
424
+ A theme shipping invalid TCSS would otherwise take the whole
425
+ application down from inside this library. Keeping the previous
426
+ stylesheet is a defined state; the failure goes to the log.
427
+ """
428
+ try:
429
+ app.stylesheet.reparse()
430
+ except Exception:
431
+ _logger.exception(
432
+ "Theme stylesheet could not be parsed, keeping the previous "
433
+ "one"
434
+ )
435
+ return
436
+
437
+ try:
438
+ app.stylesheet.update(app.screen)
439
+ except Exception:
440
+ _logger.exception("Error updating stylesheet")
441
+
442
+ def _remove_all_theme_css(self, app: App[None]) -> None:
443
+ """
444
+ Remove all CSS files that were loaded from a theme folder.
445
+
446
+ This is necessary when switching themes to avoid conflicts
447
+ between styles from different themes.
448
+
449
+ The files this loader actually loaded are the yardstick, not a fixed
450
+ directory - otherwise the stylesheet of a consumer's own theme folder
451
+ would never be removed.
452
+
453
+ Parameters
454
+ ----------
455
+ app : App
456
+ The instance of the Textual application.
457
+ """
458
+ theme_css_paths = {
459
+ Path(css_file).resolve()
460
+ for theme_data in self._theme_data.values()
461
+ for css_file in (theme_data.css_files or [])
462
+ }
463
+
464
+ for key in list(app.stylesheet.source.keys()):
465
+ path_str, _ = key
466
+ try:
467
+ css_path = Path(path_str).resolve()
468
+ except (OSError, ValueError):
469
+ # An unresolvable path is simply not one of ours
470
+ continue
471
+
472
+ if css_path in theme_css_paths:
473
+ _logger.debug("Removing CSS file: %r", path_str)
474
+ del app.stylesheet.source[key]
475
+
476
+ def change_to_next_or_previous_theme(
477
+ self, direction: int, app: App[None]
478
+ ) -> None:
479
+ """
480
+ Change to the next or previous theme in the list.
481
+
482
+ Cycling deliberately walks every theme the app knows, including
483
+ Textual's built-ins, which simply carry no termcore stylesheet.
484
+
485
+ Parameters
486
+ ----------
487
+ direction : int
488
+ 1 for next theme, -1 for previous theme.
489
+ app : App
490
+ The instance of the Textual application.
491
+ """
492
+ themes = list(app.available_themes)
493
+ current_index = themes.index(app.theme)
494
+ next_index = (current_index + direction) % len(themes)
495
+ app.theme = themes[next_index]
@@ -0,0 +1,2 @@
1
+ """The themes that ship with termcore, one directory each."""
2
+
@@ -0,0 +1,57 @@
1
+ * {
2
+ scrollbar-background: $panel;
3
+ }
4
+
5
+ .text-area--cursor, .input--cursor {
6
+ color: #80e98c;
7
+ }
8
+
9
+ ToggleButton, CustomCheckbox {
10
+ background: $panel-lighten-2;
11
+ }
12
+
13
+ ToggleButton, SelectionList {
14
+ /* Not-selected items */
15
+ & > .toggle--button,
16
+ & > .selection-list--button, & > .selection-list--button-highlighted {
17
+ background: $secondary;
18
+ }
19
+
20
+ /* Selected items */
21
+ &.-on > .toggle--button,
22
+ & > .selection-list--button-selected,
23
+ & > .selection-list--button-selected-highlighted {
24
+ background: $success-muted !important;
25
+ color: $success !important;
26
+ }
27
+ }
28
+
29
+ RadioButton {
30
+ /* Not-selected items */
31
+ & > .toggle--button,
32
+ & > .selection-list--button, & > .selection-list--button-highlighted {
33
+ color: $secondary !important;
34
+ background: $secondary-muted !important;
35
+ }
36
+
37
+ /* Selected items */
38
+ &.-on > .toggle--button,
39
+ & > .selection-list--button-selected,
40
+ & > .selection-list--button-selected-highlighted {
41
+ background: $success-muted !important;
42
+ color: $success !important;
43
+ }
44
+ }
45
+
46
+ Switch {
47
+ & .switch--slider { color: $primary; background: $panel-darken-2 }
48
+ &.-on .switch--slider { color: $success }
49
+ &:hover {
50
+ & > .switch--slider { color: $primary-lighten-2 }
51
+ &.-on > .switch--slider { color: $success-lighten-2 }
52
+ }
53
+ }
54
+
55
+ Switch {
56
+ & .switch--slider { background: $secondary; }
57
+ }
@@ -0,0 +1,22 @@
1
+ from textual.theme import Theme
2
+
3
+ TEXTUAL_THEME = Theme(
4
+ name="classic-black-saturated",
5
+ primary="#5C9AC3",
6
+ secondary="#5B5B5B",
7
+ accent="#A9B7C6",
8
+ foreground="#E5E5E5",
9
+ background="#0D0E10",
10
+ surface="#1E2022",
11
+ panel="#101112",
12
+ boost="#016F60",
13
+ success="#84D667",
14
+ warning="#E6B863",
15
+ error="#DA5E57",
16
+ dark=True,
17
+ variables={
18
+ "block-hover-background": "#0c4d5e",
19
+ "footer-key-foreground": "#D2D05F",
20
+ "input-cursor-text-style": "reverse",
21
+ }
22
+ )
@@ -0,0 +1,57 @@
1
+ * {
2
+ scrollbar-background: $panel;
3
+ }
4
+
5
+ .text-area--cursor, .input--cursor {
6
+ color: #80e98c;
7
+ }
8
+
9
+ ToggleButton, CustomCheckbox {
10
+ background: $panel-lighten-2;
11
+ }
12
+
13
+ ToggleButton, SelectionList {
14
+ /* Not-selected items */
15
+ & > .toggle--button,
16
+ & > .selection-list--button, & > .selection-list--button-highlighted {
17
+ background: $secondary;
18
+ }
19
+
20
+ /* Selected items */
21
+ &.-on > .toggle--button,
22
+ & > .selection-list--button-selected,
23
+ & > .selection-list--button-selected-highlighted {
24
+ background: $success-muted !important;
25
+ color: $success !important;
26
+ }
27
+ }
28
+
29
+ RadioButton {
30
+ /* Not-selected items */
31
+ & > .toggle--button,
32
+ & > .selection-list--button, & > .selection-list--button-highlighted {
33
+ color: $secondary !important;
34
+ background: $secondary-muted !important;
35
+ }
36
+
37
+ /* Selected items */
38
+ &.-on > .toggle--button,
39
+ & > .selection-list--button-selected,
40
+ & > .selection-list--button-selected-highlighted {
41
+ background: $success-muted !important;
42
+ color: $success !important;
43
+ }
44
+ }
45
+
46
+ Switch {
47
+ & .switch--slider { color: $primary; background: $panel-darken-2 }
48
+ &.-on .switch--slider { color: $success }
49
+ &:hover {
50
+ & > .switch--slider { color: $primary-lighten-2 }
51
+ &.-on > .switch--slider { color: $success-lighten-2 }
52
+ }
53
+ }
54
+
55
+ Switch {
56
+ & .switch--slider { background: $secondary; }
57
+ }
@@ -0,0 +1,22 @@
1
+ from textual.theme import Theme
2
+
3
+ TEXTUAL_THEME = Theme(
4
+ name="classic-black-v1",
5
+ primary="#7CA6C2",
6
+ secondary="#5B5B5B",
7
+ accent="#A9B7C6",
8
+ foreground="#E5E5E5",
9
+ background="#151618",
10
+ surface="#1E2022",
11
+ panel="#242628",
12
+ boost="#016F60",
13
+ success="#A3C995",
14
+ warning="#E6C384",
15
+ error="#D67B76",
16
+ dark=True,
17
+ variables={
18
+ "block-hover-background": "#335D48",
19
+ "footer-key-foreground": "#D2D05F",
20
+ "input-cursor-text-style": "reverse",
21
+ }
22
+ )