Files
website/tools/content/tui/themes_pane.py
T

175 lines
7.0 KiB
Python

import copy
from rich.markup import escape
from rich.text import Text
from textual import on
from textual.app import ComposeResult
from textual.binding import Binding
from textual.containers import Horizontal, VerticalScroll
from textual.widgets import DataTable, Static
import site_themes
from fields import FieldSpec
from tui.content_pane import ContentPane
from tui.dialogs import ConfirmDialog
from tui.record_editor import RecordEditor
SWATCH_KEYS = ["accent", "red", "yellow", "orange", "green", "cyan", "blue", "magenta"]
THEME_FIELDS = [
FieldSpec("id", "Id", required=True),
FieldSpec("name", "Name", required=True),
FieldSpec("mode", "Mode", "select", options=site_themes.THEME_MODES),
FieldSpec("wallpaper", "Wallpaper", "image"),
FieldSpec("borderStart", "Active border start", "color"),
FieldSpec("borderEnd", "Active border end", "color"),
*[FieldSpec(f"colors.{key}", key.replace("_", " ").capitalize(), "color", required=True) for key in site_themes.COLOR_KEYS],
]
def palette_swatches(theme: dict) -> Text:
colors = theme.get("colors", {})
return Text.assemble(*[("██", colors[key]) for key in SWATCH_KEYS if key in colors])
def to_form_record(theme: dict) -> dict:
record = copy.deepcopy(theme)
border = record.pop("activeBorder", None) or []
if len(border) == 2:
record["borderStart"], record["borderEnd"] = border
return record
def from_form_record(record: dict) -> dict:
theme = copy.deepcopy(record)
start, end = theme.pop("borderStart", None), theme.pop("borderEnd", None)
if start and end:
theme["activeBorder"] = [start, end]
elif start or end:
raise ValueError("Set both active border colors, or leave both empty")
ordered_keys = ["id", "name", "mode", "wallpaper", "activeBorder", "colors"]
return {key: theme[key] for key in ordered_keys if key in theme}
def theme_details(theme: dict, is_default: bool) -> str:
colors = theme.get("colors", {})
lines = [f"[b]{escape(theme.get('name', theme.get('id', '')))}[/b]{' [dim](default)[/dim]' if is_default else ''}", ""]
lines.append(f"[dim]{'Id':<20}[/dim]{escape(theme.get('id', ''))}")
lines.append(f"[dim]{'Mode':<20}[/dim]{escape(theme.get('mode', ''))}")
if theme.get("wallpaper"):
lines.append(f"[dim]{'Wallpaper':<20}[/dim]{escape(theme['wallpaper'])}")
if theme.get("activeBorder"):
start, end = theme["activeBorder"]
lines.append(f"[dim]{'Active border':<20}[/dim][{start}]██[/] {start} [{end}]██[/] {end}")
lines.append("")
for key in site_themes.COLOR_KEYS:
value = colors.get(key, "")
swatch = f"[{value}]██████[/]" if value else " "
lines.append(f"{swatch} [dim]{key.replace('_', ' '):<19}[/dim]{escape(value)}")
return "\n".join(lines)
class ThemesPane(ContentPane):
BINDINGS = [
Binding("a", "add", "Duplicate"),
Binding("e", "edit", "Edit"),
Binding("d", "delete", "Delete"),
Binding("s", "set_default", "Set default"),
Binding("p", "preview", "Preview"),
Binding("r", "reload", "Reload"),
]
content_file = site_themes.THEMES_FILE
def compose(self) -> ComposeResult:
with Horizontal(classes="split-layout"):
yield DataTable(cursor_type="row", zebra_stripes=True)
with VerticalScroll(classes="detail-scroll"):
yield Static("", id="theme-detail")
def on_mount(self):
table = self.table()
table.add_column("", key="default")
table.add_column("Id", key="id")
table.add_column("Name", key="name")
table.add_column("Mode", key="mode")
table.add_column("Palette", key="palette")
self.reload()
def reload(self, focus_key: str | None = None):
default_id = site_themes.default_theme_id()
table = self.table()
table.clear()
for theme in site_themes.load_themes():
marker = "*" if theme.get("id") == default_id else ""
table.add_row(marker, theme.get("id", ""), theme.get("name", ""), theme.get("mode", ""), palette_swatches(theme), key=theme.get("id"))
self.restore_cursor(focus_key)
self.show_details(self.selected_key())
def selected_theme(self) -> dict | None:
return site_themes.find_theme(site_themes.load_themes(), self.selected_key() or "")
def show_details(self, theme_id: str | None):
theme = site_themes.find_theme(site_themes.load_themes(), theme_id or "")
detail = self.query_one("#theme-detail", Static)
detail.update(theme_details(theme, theme_id == site_themes.default_theme_id()) if theme else "")
@on(DataTable.RowHighlighted)
def highlight_changed(self, event: DataTable.RowHighlighted):
self.show_details(event.row_key.value if event.row_key else None)
@on(DataTable.RowSelected)
def row_selected(self):
self.action_edit()
def open_editor(self, original_id: str | None, theme: dict, title: str):
def saved(record: dict | None):
if record is None:
return
try:
updated = from_form_record(record)
site_themes.save_theme(original_id, updated)
except (OSError, ValueError) as error:
self.notify(str(error), title="Not saved", severity="error")
return
self.app.reload_site_themes(updated["id"])
self.reload(updated["id"])
self.notify(f"Saved {updated['name']}")
validate = lambda record: site_themes.check_theme(from_form_record(record))
editor = RecordEditor(title, THEME_FIELDS, to_form_record(theme), validate=validate, image_folder="wallpapers")
self.app.push_screen(editor, saved)
def action_edit(self):
theme = self.selected_theme()
if theme:
self.open_editor(theme["id"], theme, f"Edit theme {theme.get('name', theme['id'])}")
def action_add(self):
template = self.selected_theme() or {"mode": "dark", "colors": {}}
theme = copy.deepcopy(template)
theme["id"] = f"{template.get('id', 'theme')}-copy"
theme["name"] = f"{template.get('name', 'New theme')} Copy"
self.open_editor(None, theme, "New theme")
def action_delete(self):
theme = self.selected_theme()
if not theme:
return
def confirmed(should_delete: bool | None):
if should_delete:
self.run_now(lambda: site_themes.remove_theme(theme["id"]), f"Removed {theme.get('name', theme['id'])}")
self.app.push_screen(ConfirmDialog(f"Remove the {theme.get('name', theme['id'])} theme?"), confirmed)
def action_set_default(self):
theme = self.selected_theme()
if theme:
self.run_now(lambda: site_themes.set_default_theme(theme["id"]), f"{theme.get('name', theme['id'])} is now the default theme", theme["id"])
def action_preview(self):
theme = self.selected_theme()
if theme:
self.app.reload_site_themes(theme["id"])
self.notify(f"Previewing {theme.get('name', theme['id'])}")