pen-tracker/build/lib/pen_tracker/tui.py
2026-05-02 22:25:35 -05:00

231 lines
8.1 KiB
Python

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