from textual.app import App, ComposeResult from textual.screen import Screen from textual.widgets import Label, Input, Button, Header, Footer, DataTable from textual.containers import Vertical, Horizontal from textual.binding import Binding from .engine import PenTracker, Pen, InkTracker, Ink # --- TUI SCREENS --- class PenFormScreen(Screen): """A screen for adding or editing a pen.""" def __init__(self, tracker, existing_pen=None): super().__init__() self.tracker = tracker self.existing_pen = existing_pen def compose(self) -> ComposeResult: with Vertical(id="form-container"): yield Label("📝 NEW PEN" if not self.existing_pen else "✏️ EDIT PEN") for header in self.tracker.headers: field = self.tracker.key_map.get(header, header) val = getattr(self.existing_pen, field) if self.existing_pen else "" yield Input(value=val, placeholder=header, id=header) with Horizontal(): yield Button("Save", variant="success", id="save_cmd") yield Button("Cancel", variant="error", id="cancel_cmd") def on_button_pressed(self, event: Button.Pressed) -> None: if event.button.id == "cancel_cmd": self.dismiss() return new_data = {} for header in self.tracker.headers: try: input_widget = self.query_one(f"#{header}", Input) new_data[header] = input_widget.value if input_widget.value.strip() else "N/A" except Exception: new_data[header] = "N/A" # Map to Pen fields mapped_data = {self.tracker.key_map.get(k, k): v for k, v in new_data.items()} new_pen = Pen(**mapped_data) if self.existing_pen is not None and self.existing_pen in self.tracker.pens: idx = self.tracker.pens.index(self.existing_pen) self.tracker.pens[idx] = new_pen else: self.tracker.pens.append(new_pen) # save_data() handles the sorting internally self.tracker.save_data() self.dismiss(new_pen) class InkFormScreen(Screen): """A screen for adding or editing an ink.""" def __init__(self, ink_tracker, existing_ink=None): super().__init__() self.ink_tracker = ink_tracker self.existing_ink = existing_ink def compose(self) -> ComposeResult: with Vertical(id="form-container"): yield Label("🖋️ NEW INK" if not self.existing_ink else "✏️ EDIT INK") for header in self.ink_tracker.headers: val = getattr(self.existing_ink, header) if self.existing_ink else "" yield Input(value=val, placeholder=header, id=header) with Horizontal(): yield Button("Save", variant="success", id="save_cmd") yield Button("Cancel", variant="error", id="cancel_cmd") def on_button_pressed(self, event: Button.Pressed) -> None: if event.button.id == "cancel_cmd": self.dismiss() return new_data = {} for header in self.ink_tracker.headers: try: input_widget = self.query_one(f"#{header}", Input) new_data[header] = input_widget.value if input_widget.value.strip() else "N/A" except Exception: new_data[header] = "N/A" new_ink = Ink(**new_data) if self.existing_ink is not None and self.existing_ink in self.ink_tracker.inks: idx = self.ink_tracker.inks.index(self.existing_ink) self.ink_tracker.inks[idx] = new_ink else: self.ink_tracker.inks.append(new_ink) self.ink_tracker.save_data() self.dismiss(new_ink) class PenTrackerApp(App): """The Main TUI Application.""" CSS = """ #form-container { width: 60%; height: auto; border: heavy white; padding: 1 2; margin: 5 10; background: $panel; } Label { width: 100%; text-align: center; text-style: bold; margin-bottom: 1; } Input { margin-bottom: 1; } Horizontal { align: center middle; height: auto; } Button { margin: 0 1; } DataTable { height: 1fr; } """ BINDINGS = [ Binding("d", "delete_selected", "Delete Selected"), Binding("a", "add_new", "Add New"), Binding("i", "toggle_mode", "Toggle Pens/Inks"), Binding("q", "quit", "Quit"), ] def compose(self) -> ComposeResult: yield Header() yield DataTable(id="pen_table") yield Footer() def on_mount(self) -> None: self.tracker = PenTracker() self.ink_tracker = InkTracker() self.mode = "pens" # "pens" or "inks" self._refresh_table() def _refresh_table(self): table = self.query_one("#pen_table", DataTable) table.clear(columns=True) if self.mode == "pens": display_cols = ['Make', 'Model', 'Nib', 'Nib-Material', 'Body','Cap', 'Current-Ink', 'Inked-date', 'Notes'] table.add_columns(*display_cols) items = self.tracker.pens else: # inks display_cols = ['Vendor', 'Name', 'Color', 'Purchased', 'Size', 'Notes'] table.add_columns(*display_cols) items = self.ink_tracker.inks for idx, item in enumerate(items): if self.mode == "pens": row_values = [getattr(item, c.replace('-','_')) for c in display_cols] else: row_values = [getattr(item, c) for c in display_cols] table.add_row(*row_values, key=str(idx)) def action_add_new(self) -> None: if self.mode == "pens": form = PenFormScreen(self.tracker) else: form = InkFormScreen(self.ink_tracker) self.push_screen(form, self.handle_form_result) def action_edit_selected(self, index_str: str): try: idx = int(index_str) if self.mode == "pens": existing_item = self.tracker.pens[idx] form = PenFormScreen(self.tracker, existing_item) else: existing_item = self.ink_tracker.inks[idx] form = InkFormScreen(self.ink_tracker, existing_item) self.push_screen(form, self.handle_form_result) except (ValueError, IndexError): pass def handle_form_result(self, updated_pen_data) -> None: if updated_pen_data: self._refresh_table() self.notify("Collection Updated & Sorted!", title="Success") def action_delete_selected(self) -> None: table = self.query_one("#pen_table", DataTable) try: table = self.query_one("#pen_table", DataTable) row_node = table.get_row_at_cursor() idx = int(row_node.key.value) if self.mode == "pens": removed = self.tracker.pens.pop(idx) self.tracker.save_data() self.notify(f"Deleted {removed.Make}", title="Removed") else: removed = self.ink_tracker.inks.pop(idx) self.ink_tracker.save_data() self.notify(f"Deleted {removed.Name}", title="Removed") self._refresh_table() except Exception: self.notify("No item selected to delete", title="Error", severity="error") def action_toggle_mode(self) -> None: self.mode = "inks" if self.mode == "pens" else "pens" self._refresh_table() mode_name = "Pens" if self.mode == "pens" else "Inks" self.notify(f"Switched to {mode_name} mode", title="Mode Changed") def on_data_table_cell_selected(self, event: DataTable.CellSelected) -> None: try: idx = int(event.cell_key.row_key.value) self.action_edit_selected(str(idx)) except (AttributeError, ValueError): self.notify("Error selecting row", title="Error", severity="error") def main(): # This is the entry point defined in pyproject.toml app = PenTrackerApp() app.run()