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))