Skip to content

Instantly share code, notes, and snippets.

@paulrobello
Created June 14, 2024 01:05
Show Gist options
  • Save paulrobello/77225db05ef64728d6f2d615c2ed20fd to your computer and use it in GitHub Desktop.
Save paulrobello/77225db05ef64728d6f2d615c2ed20fd to your computer and use it in GitHub Desktop.
Textual Theme Manger
"""Theme manager for Textual"""
import os
from typing import Dict, List, Literal, Optional, TypeAlias
import simplejson as json
from textual.design import ColorSystem
ThemeMode: TypeAlias = Literal["dark", "light"]
ThemeModes: List[ThemeMode] = ["dark", "light"]
Theme: TypeAlias = Dict[ThemeMode, ColorSystem]
Themes: TypeAlias = Dict[str, Theme]
class InvalidThemeError(Exception):
"""Raised when an invalid theme is provided."""
def __init__(self, theme_name: str):
"""Initialize the exception with the invalid theme name."""
self.theme_name = theme_name
super().__init__(f"Invalid theme: {theme_name}")
class ThemeModeError(InvalidThemeError):
"""Raised when a theme does not have at least one of 'dark' or 'light' modes."""
def __init__(self, theme_name: str):
"""Initialize the exception with the invalid theme name."""
super().__init__(
f"Theme '{theme_name}' does not have at least one of 'dark' or 'light' modes."
)
class ThemeManager:
"""Theme manager for Textual"""
themes: Themes
theme_folder: str
def __init__(self, theme_folder: Optional[str] = None) -> None:
"""Initialize the theme manager"""
self.theme_folder = theme_folder or f"{os.getcwd()}/parllama/themes"
self.themes = self.load_themes()
def load_theme(self, theme_name: str) -> Theme:
"""Load textual theme from json file"""
theme: Theme = {}
theme_name = os.path.basename(theme_name)
with open(f"{self.theme_folder}/{theme_name}", "r", encoding="utf-8") as f:
theme_def = json.load(f)
if "dark" not in theme_def and "light" not in theme_def:
raise ThemeModeError(theme_name)
for mode in ThemeModes:
if mode in theme_def:
theme[mode] = ColorSystem(**theme_def[mode])
return theme
def load_themes(self) -> Themes:
"""Load textual themes from json files"""
themes: Themes = {}
for file in os.listdir(self.theme_folder):
if file.lower().endswith(".json"):
theme_name = os.path.splitext(file)[0]
themes[theme_name] = self.load_theme(f"{self.theme_folder}/{file}")
return themes
def get_theme(self, theme_name: str) -> Theme:
"""Get theme by name"""
return self.themes[theme_name]
def list_themes(self) -> List[str]:
"""Get list of themes"""
return list(self.themes.keys())
def theme_has_dark(self, theme_name: str) -> bool:
"""Check if theme has dark mode"""
return "dark" in self.themes[theme_name]
def theme_has_light(self, theme_name: str) -> bool:
"""Check if theme has light mode"""
return "light" in self.themes[theme_name]
def get_color_system_for_theme_mode(
self, theme_name: str, dark: bool
) -> ColorSystem:
"""Get color system for theme mode"""
theme = self.themes[theme_name]
if dark:
if "dark" in theme:
return theme["dark"]
return theme["light"]
if "light" in theme:
return theme["light"]
return theme["dark"]
theme_manager = ThemeManager()
# use the following method in your App class
def get_css_variables(self) -> dict[str, str]:
"""Get a mapping of variables used to pre-populate CSS.
May be implemented in a subclass to add new CSS variables.
Returns:
A mapping of variable name to value.
"""
return theme_manager.get_color_system_for_theme_mode(
self.theme, self.dark
).generate()
@paulrobello
Copy link
Author

json theme files take the following format

{
  "dark": {
    "primary": "#e49500",
    "secondary": "#6e4800",
    "warning": "#ffa62b",
    "error": "#ba3c5b",
    "success": "#4EBF71",
    "accent": "#6e4800",
    "panel": "#111",
    "surface":"#1e1e1e",
    "background":"#121212",
    "dark": true
  },
  "light": {
    "primary": "#004578",
    "secondary": "#ffa62b",
    "warning": "#ffa62b",
    "error": "#ba3c5b",
    "success": "#4EBF71",
    "accent": "#0178D4",
    "background":"#efefef",
    "surface":"#f5f5f5",
    "dark": false
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment