remove tui display as it never really worked #5
8 changed files with 23 additions and 267 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"]
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
textual
|
|
||||||
|
|
@ -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.")
|
||||||
|
|
|
||||||
|
|
@ -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()
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue