175 lines
7.0 KiB
Python
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'])}")
|