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 # --- 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 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 Pen"), 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('Pens.csv') self._refresh_table() def _refresh_table(self): table = self.query_one("#pen_table", DataTable) table.clear(columns=True) display_cols = ['Make', 'Model', 'Nib', 'Nib-Material', 'Body','Cap', 'Current-Ink', 'Inked-date', 'Notes'] table.add_columns(*display_cols) for idx, pen in enumerate(self.tracker.pens): row_values = [getattr(pen, c.replace('-','_')) for c in display_cols] # We use the index as the row key to track items accurately table.add_row(*row_values, key=str(idx)) def action_add_new(self) -> None: form = PenFormScreen(self.tracker) self.push_screen(form, self.handle_form_result) def action_edit_selected(self, index_str: str): try: idx = int(index_str) existing_pen = self.tracker.pens[idx] form = PenFormScreen(self.tracker, existing_pen=existing_pen) 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: # Accessing via the table's current cursor table = self.query_one("#pen_table", DataTable) row_node = table.get_row_at_cursor() idx = int(row_node.key.value) removed = self.tracker.pens.pop(idx) self.tracker.save_data() self._refresh_table() self.notify(f"Deleted {removed.Make}", title="Removed") except Exception: self.notify("No pen selected to delete", title="Error", severity="error") 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()