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,155 @@
|
||||
from rich.markup import escape
|
||||
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 portfolio
|
||||
from fields import get_path, summary_text, to_form_text
|
||||
from portfolio import Collection
|
||||
from tui.content_pane import ContentPane
|
||||
from tui.dialogs import ConfirmDialog
|
||||
from tui.record_editor import RecordEditor
|
||||
|
||||
|
||||
def record_details(collection: Collection, record: dict) -> str:
|
||||
lines = [f"[b]{escape(portfolio.record_title(collection, record))}[/b]", ""]
|
||||
for spec in collection.fields[1:]:
|
||||
value = get_path(record, spec.path)
|
||||
if value in (None, "", [], {}, False):
|
||||
continue
|
||||
text = "yes" if value is True else to_form_text(spec.kind, value)
|
||||
if "\n" in text or len(text) > 60:
|
||||
lines += [f"[dim]{escape(spec.label)}[/dim]", escape(text), ""]
|
||||
else:
|
||||
lines.append(f"[dim]{escape(spec.label):<14}[/dim]{escape(text)}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
class RecordListPane(ContentPane):
|
||||
BINDINGS = [
|
||||
Binding("a", "add", "Add"),
|
||||
Binding("e", "edit", "Edit"),
|
||||
Binding("d", "delete", "Delete"),
|
||||
Binding("f", "toggle_featured", "Featured"),
|
||||
Binding("K", "move(-1)", "Move up"),
|
||||
Binding("J", "move(1)", "Move down"),
|
||||
Binding("r", "reload", "Reload"),
|
||||
]
|
||||
|
||||
def __init__(self, collection: Collection, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.collection = collection
|
||||
self.set_content_file(collection.file)
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield from self.compose_header()
|
||||
with Horizontal(classes="split-layout"):
|
||||
yield DataTable(cursor_type="row", zebra_stripes=True)
|
||||
with VerticalScroll(classes="detail-scroll"):
|
||||
yield Static("", classes="record-detail")
|
||||
|
||||
def compose_header(self) -> ComposeResult:
|
||||
yield from ()
|
||||
|
||||
def on_mount(self):
|
||||
self.build_columns()
|
||||
self.reload()
|
||||
|
||||
def build_columns(self):
|
||||
table = self.table()
|
||||
table.clear(columns=True)
|
||||
table.add_column("#", key="number")
|
||||
for label, path in self.collection.columns:
|
||||
table.add_column(label, key=path)
|
||||
|
||||
def records(self) -> list[dict]:
|
||||
return portfolio.load_records(self.collection)
|
||||
|
||||
def reload(self, focus_key: str | None = None):
|
||||
table = self.table()
|
||||
table.clear()
|
||||
for index, record in enumerate(self.records()):
|
||||
cells = [summary_text(get_path(record, path)) for _, path in self.collection.columns]
|
||||
table.add_row(str(index + 1), *cells, key=str(index))
|
||||
self.restore_cursor(focus_key)
|
||||
self.show_details(self.selected_key())
|
||||
|
||||
def selected_index(self) -> int | None:
|
||||
key = self.selected_key()
|
||||
return int(key) if key is not None else None
|
||||
|
||||
def show_details(self, key: str | None):
|
||||
records = self.records()
|
||||
detail = self.query_one(".record-detail", Static)
|
||||
if key is None or int(key) >= len(records):
|
||||
detail.update(f"No {self.collection.title.lower()} yet. Press [b]a[/b] to add one.")
|
||||
return
|
||||
detail.update(record_details(self.collection, records[int(key)]))
|
||||
|
||||
@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, index: int | None, record: dict):
|
||||
title = f"Edit {portfolio.record_title(self.collection, record)}" if index is not None else f"New {self.collection.title.lower()} entry"
|
||||
|
||||
def saved(result: dict | None):
|
||||
if result is None:
|
||||
return
|
||||
self.run_now(
|
||||
lambda: portfolio.save_record(self.collection, index, result),
|
||||
f"Saved {portfolio.record_title(self.collection, result)}",
|
||||
str(index) if index is not None else str(len(self.records())),
|
||||
)
|
||||
|
||||
self.app.push_screen(RecordEditor(title, self.collection.fields, record, image_folder=self.image_folder()), saved)
|
||||
|
||||
def image_folder(self) -> str:
|
||||
return ""
|
||||
|
||||
def action_add(self):
|
||||
self.open_editor(None, {})
|
||||
|
||||
def action_edit(self):
|
||||
index = self.selected_index()
|
||||
if index is not None:
|
||||
self.open_editor(index, self.records()[index])
|
||||
|
||||
def action_delete(self):
|
||||
index = self.selected_index()
|
||||
if index is None:
|
||||
return
|
||||
title = portfolio.record_title(self.collection, self.records()[index])
|
||||
|
||||
def confirmed(should_delete: bool | None):
|
||||
if should_delete:
|
||||
self.run_now(lambda: portfolio.remove_record(self.collection, index), f"Removed {title}", str(max(0, index - 1)))
|
||||
|
||||
self.app.push_screen(ConfirmDialog(f"Remove {title}?"), confirmed)
|
||||
|
||||
def action_move(self, step: int):
|
||||
index = self.selected_index()
|
||||
if index is None:
|
||||
return
|
||||
target = index + step
|
||||
self.run_now(lambda: portfolio.move_record(self.collection, index, step), "Moved", str(target))
|
||||
|
||||
def action_toggle_featured(self):
|
||||
index = self.selected_index()
|
||||
if index is None:
|
||||
return
|
||||
if not any(spec.path == "featured" for spec in self.collection.fields):
|
||||
self.notify(f"{self.collection.title} entries have no featured flag", severity="warning")
|
||||
return
|
||||
record = self.records()[index]
|
||||
record["featured"] = not record.get("featured", False)
|
||||
if not record["featured"]:
|
||||
record.pop("featured")
|
||||
state = "Featured" if record.get("featured") else "Unfeatured"
|
||||
self.run_now(lambda: portfolio.save_record(self.collection, index, record), f"{state} {portfolio.record_title(self.collection, record)}", str(index))
|
||||
Reference in new issue
Block a user