Replace content CLI with Textual TUI
This commit is contained in:
1 parent
0d58ddd430
commit
737700a9a8
28 files changed
+2403
-167
No files matched your search
@@ -0,0 +1,174 @@
|
||||
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'])}")
|
||||
Reference in new issue
Block a user