remove tui display as it never really worked #5

Merged
don merged 1 commit from AI-2.0 into main 2026-06-14 00:04:40 -05:00
8 changed files with 23 additions and 267 deletions

View file

@ -30,17 +30,10 @@ pen-tracker list
pen-tracker export --output my_pens.json pen-tracker export --output my_pens.json
``` ```
### TUI
```bash
pen-tui
```
## Features ## Features
- Track fountain pens with details like make, model, nib, ink, etc. - Track fountain pens with details like make, model, nib, ink, etc.
- CLI interface with interactive and command-line modes - CLI interface with interactive and command-line modes
- TUI interface using Textual
- Data stored in CSV format - Data stored in CSV format
- Export to JSON - Export to JSON
- Input validation for dates - Input validation for dates

View file

@ -4,20 +4,17 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "pen-tracker" name = "pen-tracker"
version = "0.4.2" version = "0.5.0"
authors = [ authors = [
{ name="Don Harper", email="don@donharper.org" }, { name="Don Harper", email="don@donharper.org" },
] ]
description = "A fountain pen collection tracker." description = "A fountain pen collection tracker."
readme = "README.md" readme = "README.md"
requires-python = ">=3.8" requires-python = ">=3.8"
dependencies = [ dependencies = []
"textual",
]
[project.scripts] [project.scripts]
pen-tracker = "pen_tracker.cli:main" pen-tracker = "pen_tracker.cli:main"
pen-tui = "pen_tracker.tui:main"
[tool.setuptools.packages.find] [tool.setuptools.packages.find]
where = ["src"] where = ["src"]

View file

@ -1,12 +1,11 @@
Metadata-Version: 2.4 Metadata-Version: 2.4
Name: pen-tracker Name: pen-tracker
Version: 0.4.2 Version: 0.5.0
Summary: A fountain pen collection tracker. Summary: A fountain pen collection tracker.
Author-email: Don Harper <don@donharper.org> Author-email: Don Harper <don@donharper.org>
Requires-Python: >=3.8 Requires-Python: >=3.8
Description-Content-Type: text/markdown Description-Content-Type: text/markdown
License-File: LICENSE License-File: LICENSE
Requires-Dist: textual
Dynamic: license-file Dynamic: license-file
# Pen Tracker # Pen Tracker
@ -41,17 +40,10 @@ pen-tracker list
pen-tracker export --output my_pens.json pen-tracker export --output my_pens.json
``` ```
### TUI
```bash
pen-tui
```
## Features ## Features
- Track fountain pens with details like make, model, nib, ink, etc. - Track fountain pens with details like make, model, nib, ink, etc.
- CLI interface with interactive and command-line modes - CLI interface with interactive and command-line modes
- TUI interface using Textual
- Data stored in CSV format - Data stored in CSV format
- Export to JSON - Export to JSON
- Input validation for dates - Input validation for dates

View file

@ -4,11 +4,9 @@ pyproject.toml
src/pen_tracker/__init__.py src/pen_tracker/__init__.py
src/pen_tracker/cli.py src/pen_tracker/cli.py
src/pen_tracker/engine.py src/pen_tracker/engine.py
src/pen_tracker/tui.py
src/pen_tracker.egg-info/PKG-INFO src/pen_tracker.egg-info/PKG-INFO
src/pen_tracker.egg-info/SOURCES.txt src/pen_tracker.egg-info/SOURCES.txt
src/pen_tracker.egg-info/dependency_links.txt src/pen_tracker.egg-info/dependency_links.txt
src/pen_tracker.egg-info/entry_points.txt src/pen_tracker.egg-info/entry_points.txt
src/pen_tracker.egg-info/requires.txt
src/pen_tracker.egg-info/top_level.txt src/pen_tracker.egg-info/top_level.txt
tests/test_engine.py tests/test_engine.py

View file

@ -1,3 +1,2 @@
[console_scripts] [console_scripts]
pen-tracker = pen_tracker.cli:main pen-tracker = pen_tracker.cli:main
pen-tui = pen_tracker.tui:main

View file

@ -1 +0,0 @@
textual

View file

@ -68,7 +68,9 @@ class CLITracker(PenTracker):
try: try:
idx = int(input("\nEnter the ID of the pen to edit: ")) idx = int(input("\nEnter the ID of the pen to edit: "))
pen = self.pens[idx] if idx < 1:
raise ValueError
pen = self.pens[idx - 1]
except (ValueError, IndexError): except (ValueError, IndexError):
print("[!] Invalid ID.") print("[!] Invalid ID.")
return return
@ -110,7 +112,7 @@ class CLITracker(PenTracker):
"""Helper to print a list without the interactive menu logic.""" """Helper to print a list without the interactive menu logic."""
print(f"{'ID':<4} | {'MAKE':<12} | {'MODEL':<12} | {'BODY':<12} | {'CAP':<12} | {'NIB':<3} | {'INK':<15} | {'INKED DATE':<12}") print(f"{'ID':<4} | {'MAKE':<12} | {'MODEL':<12} | {'BODY':<12} | {'CAP':<12} | {'NIB':<3} | {'INK':<15} | {'INKED DATE':<12}")
print("-" * 102) print("-" * 102)
for idx, pen in enumerate(self.pens): for idx, pen in enumerate(self.pens, start=1):
make = pen.Make[:12] make = pen.Make[:12]
model = pen.Model[:12] model = pen.Model[:12]
body = pen.Body[:12] body = pen.Body[:12]
@ -131,7 +133,10 @@ class CLITracker(PenTracker):
choice = input("\nEnter ID to see full details (or 'b' to go back): ") choice = input("\nEnter ID to see full details (or 'b' to go back): ")
if choice.lower() != 'b': if choice.lower() != 'b':
try: try:
self.view_pen_details(int(choice)) idx = int(choice)
if idx < 1:
raise ValueError
self.view_pen_details(idx - 1)
except (ValueError, IndexError): except (ValueError, IndexError):
print("[!] Invalid ID.") print("[!] Invalid ID.")
@ -153,7 +158,9 @@ class CLITracker(PenTracker):
self.show_summary_list() self.show_summary_list()
try: try:
idx = int(input("\nEnter the ID of the pen to delete: ")) idx = int(input("\nEnter the ID of the pen to delete: "))
removed = self.pens.pop(idx) if idx < 1:
raise ValueError
removed = self.pens.pop(idx - 1)
self.save_data() self.save_data()
print(f"\n[!] Removed: {removed.Make} {removed.Model}") print(f"\n[!] Removed: {removed.Make} {removed.Model}")
except (ValueError, IndexError): except (ValueError, IndexError):
@ -192,18 +199,18 @@ class CLITracker(PenTracker):
print("\n[!] Your ink collection is currently empty.") print("\n[!] Your ink collection is currently empty.")
return return
print("\n" + "="*80) print("\n" + "="*100)
print(f"{'ID':<4} | {'VENDOR':<12} | {'NAME':<20} | {'COLOR':<15} | {'SIZE':<30}") print(f"{'ID':<4} | {'VENDOR':<12} | {'NAME':<20} | {'COLOR':<15} | {'SIZE':<30}")
print("-" * 80) print("-" * 100)
for idx, ink in enumerate(self.ink_tracker.inks): for idx, ink in enumerate(self.ink_tracker.inks, start=1):
vendor = ink.Vendor[:12] vendor = ink.Vendor[:12]
name = ink.Name[:20] name = ink.Name[:20]
color = ink.Color[:15] color = ink.Color[:15]
size = ink.Size[:30] size = ink.Size[:30]
print(f"{idx:<4} | {vendor:<12} | {name:<20} | {color:<15} | {size:<30}") print(f"{idx:<4} | {vendor:<12} | {name:<20} | {color:<15} | {size:<30}")
print("="*80) print("="*100)
def select_ink(self): def select_ink(self):
"""Helper method to select an ink from the list.""" """Helper method to select an ink from the list."""
@ -213,10 +220,12 @@ class CLITracker(PenTracker):
self.view_all_inks() self.view_all_inks()
try: try:
idx = int(input("\nEnter the ID of the ink to select (or -1 for N/A): ")) idx = int(input("\nEnter the ID of the ink to select (or 0 for N/A): "))
if idx == -1: if idx == 0:
return "N/A" return "N/A"
ink = self.ink_tracker.inks[idx] if idx < 0:
raise ValueError
ink = self.ink_tracker.inks[idx - 1]
return ink.Name return ink.Name
except (ValueError, IndexError): except (ValueError, IndexError):
print("[!] Invalid ID.") print("[!] Invalid ID.")

View file

@ -1,231 +0,0 @@
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()