task | add ink support

This commit is contained in:
Don Harper 2026-05-02 22:25:35 -05:00
parent 9f9fd41851
commit bf3b1fc456
13 changed files with 595 additions and 37 deletions

View file

@ -3,7 +3,7 @@ import csv
import argparse
import json
from datetime import datetime
from .engine import PenTracker, Pen
from .engine import PenTracker, Pen, InkTracker, Ink
def validate_date(date_str: str) -> bool:
"""Validate date in YYYY-MM-DD format."""
@ -17,6 +17,10 @@ def validate_date(date_str: str) -> bool:
class CLITracker(PenTracker):
def __init__(self, storage_file=None):
super().__init__(storage_file)
self.ink_tracker = InkTracker()
def add_pen(self):
print("\n--- Add New Fountain Pen ---")
new_pen_data = {}
@ -29,6 +33,9 @@ class CLITracker(PenTracker):
for label, key in fields:
while True:
if key == 'Current-Ink':
value = self.select_ink()
break
value = input(f"Enter {label}: ").strip()
if not value:
value = "N/A"
@ -74,6 +81,15 @@ class CLITracker(PenTracker):
field = self.key_map.get(header, header)
current_val = getattr(pen, field)
while True:
if header == 'Current-Ink':
print(f"Current Ink: {current_val}")
change_ink = input("Change ink? (y/n): ").strip().lower()
if change_ink == 'y':
new_val = self.select_ink()
break
else:
new_val = ""
break
new_val = input(f"{header} [{current_val}]: ").strip()
if not new_val:
break
@ -149,6 +165,69 @@ class CLITracker(PenTracker):
except (ValueError, IndexError):
print("[!] Invalid ID.")
def add_ink(self):
print("\n--- Add New Ink ---")
new_ink_data = {}
fields = [
("Vendor", "Vendor"), ("Name", "Name"), ("Color", "Color"),
("Purchased (YYYY-MM-DD)", "Purchased"), ("Size", "Size"), ("Notes", "Notes")
]
for label, key in fields:
while True:
value = input(f"Enter {label}: ").strip()
if not value:
value = "N/A"
break
if key == 'Purchased':
if validate_date(value):
break
else:
print("Invalid date format. Use YYYY-MM-DD.")
else:
break
new_ink_data[key] = value
new_ink = Ink(**new_ink_data)
self.ink_tracker.inks.append(new_ink)
self.ink_tracker.save_data()
print("\n[✔] Ink added successfully to inks.csv!")
def view_all_inks(self):
if not self.ink_tracker.inks:
print("\n[!] Your ink collection is currently empty.")
return
print("\n" + "="*80)
print(f"{'ID':<4} | {'VENDOR':<12} | {'NAME':<20} | {'COLOR':<15} | {'SIZE':<10}")
print("-" * 80)
for idx, ink in enumerate(self.ink_tracker.inks):
vendor = ink.Vendor[:12]
name = ink.Name[:20]
color = ink.Color[:15]
size = ink.Size[:10]
print(f"{idx:<4} | {vendor:<12} | {name:<20} | {color:<15} | {size:<10}")
print("="*80)
def select_ink(self):
"""Helper method to select an ink from the list."""
if not self.ink_tracker.inks:
print("[!] No inks available. Add some inks first.")
return "N/A"
self.view_all_inks()
try:
idx = int(input("\nEnter the ID of the ink to select (or -1 for N/A): "))
if idx == -1:
return "N/A"
ink = self.ink_tracker.inks[idx]
return ink.Name
except (ValueError, IndexError):
print("[!] Invalid ID.")
return "N/A"
def clear_screen():
os.system('cls' if os.name == 'nt' else 'clear')
@ -180,6 +259,17 @@ def main():
export_parser = subparsers.add_parser('export', help='Export to JSON')
export_parser.add_argument('--output', default='pens.json', help='Output file')
# Ink commands
add_ink_parser = subparsers.add_parser('add-ink', help='Add a new ink')
add_ink_parser.add_argument('--vendor', required=True)
add_ink_parser.add_argument('--name', required=True)
add_ink_parser.add_argument('--color')
add_ink_parser.add_argument('--purchased')
add_ink_parser.add_argument('--size')
add_ink_parser.add_argument('--notes')
list_inks_parser = subparsers.add_parser('list-inks', help='List all inks')
args = parser.parse_args()
tracker = CLITracker(args.csv)
@ -217,6 +307,24 @@ def main():
with open(args.output, 'w') as f:
json.dump([asdict(p) for p in tracker.pens], f, indent=2)
print(f"Exported to {args.output}")
elif args.command == 'add-ink':
if args.purchased and not validate_date(args.purchased):
print("Invalid purchased date format.")
return
ink_data = {
'Vendor': args.vendor,
'Name': args.name,
'Color': args.color or 'N/A',
'Purchased': args.purchased or 'N/A',
'Size': args.size or 'N/A',
'Notes': args.notes or 'N/A'
}
new_ink = Ink(**ink_data)
tracker.ink_tracker.inks.append(new_ink)
tracker.ink_tracker.save_data()
print("Ink added successfully.")
elif args.command == 'list-inks':
tracker.view_all_inks()
else:
# Interactive mode
while True:
@ -225,7 +333,9 @@ def main():
print("2. Add New Pen")
print("3. Edit a Pen")
print("4. Delete a Pen")
print("5. Exit")
print("5. View Ink Collection")
print("6. Add New Ink")
print("7. Exit")
choice = input("\nSelect an option: ")
@ -242,6 +352,12 @@ def main():
clear_screen()
tracker.delete_pen()
elif choice == '5':
clear_screen()
tracker.view_all_inks()
elif choice == '6':
clear_screen()
tracker.add_ink()
elif choice == '7':
print("Goodbye! Happy writing! ✒️")
break
else:

View file

@ -6,6 +6,15 @@ from typing import List
logger = logging.getLogger(__name__)
@dataclass
class Ink:
Vendor: str = "N/A"
Name: str = "N/A"
Color: str = "N/A"
Purchased: str = "N/A"
Size: str = "N/A"
Notes: str = "N/A"
@dataclass
class Pen:
Make: str = "N/A"
@ -21,6 +30,65 @@ class Pen:
Inked_date: str = "N/A"
Notes: str = "N/A"
class InkTracker:
def __init__(self, storage_file: str = None):
if storage_file is None:
storage_file = os.path.abspath('inks.csv')
self.storage_file = os.path.abspath(storage_file)
self.headers = ['Vendor', 'Name', 'Color', 'Purchased', 'Size', 'Notes']
self.inks: List[Ink] = self.load_data()
def _sort_inks(self):
"""Sorts the inks list by Vendor, then by Name alphabetically."""
self.inks.sort(key=lambda x: (x.Vendor.lower(), x.Name.lower()))
def load_data(self) -> List[Ink]:
"""Loads data from the CSV file."""
if not os.path.exists(self.storage_file):
self._create_empty_csv()
return []
inks = []
try:
with open(self.storage_file, mode='r', encoding='utf-8-sig') as f:
reader = csv.DictReader(f)
if reader.fieldnames:
reader.fieldnames = [h.strip() for h in reader.fieldnames]
for row in reader:
if None in row:
extras = row.pop(None)
if extras:
extra_text = ",".join(extras).strip()
if extra_text:
row['Notes'] = (row.get('Notes') or '')
if row['Notes']:
row['Notes'] += ", "
row['Notes'] += extra_text
clean_row = {
k.strip(): (v.strip() if v else "N/A")
for k, v in row.items() if k is not None
}
inks.append(Ink(**clean_row))
inks.sort(key=lambda x: (x.Vendor.lower(), x.Name.lower()))
except Exception as e:
logger.error(f"Error loading inks CSV: {e}")
return inks
def _create_empty_csv(self):
"""Creates the CSV file with headers if it is missing."""
with open(self.storage_file, mode='w', newline='', encoding='utf-8') as f:
writer = csv.DictWriter(f, fieldnames=self.headers)
writer.writeheader()
def save_data(self):
"""Sorts the data before writing to ensure persistent alphabetical order."""
self._sort_inks()
with open(self.storage_file, mode='w', newline='', encoding='utf-8') as f:
writer = csv.DictWriter(f, fieldnames=self.headers)
writer.writeheader()
for ink in self.inks:
writer.writerow(asdict(ink))
class PenTracker:
def __init__(self, storage_file: str = None):
if storage_file is None:

View file

@ -3,7 +3,7 @@ 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
from .engine import PenTracker, Pen, InkTracker, Ink
# --- TUI SCREENS ---
class PenFormScreen(Screen):
@ -54,6 +54,50 @@ class PenFormScreen(Screen):
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."""
@ -89,7 +133,8 @@ class PenTrackerApp(App):
BINDINGS = [
Binding("d", "delete_selected", "Delete Selected"),
Binding("a", "add_new", "Add New Pen"),
Binding("a", "add_new", "Add New"),
Binding("i", "toggle_mode", "Toggle Pens/Inks"),
Binding("q", "quit", "Quit"),
]
@ -100,29 +145,46 @@ class PenTrackerApp(App):
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)
display_cols = ['Make', 'Model', 'Nib', 'Nib-Material', 'Body','Cap', 'Current-Ink', 'Inked-date', 'Notes']
table.add_columns(*display_cols)
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, 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
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:
form = PenFormScreen(self.tracker)
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)
existing_pen = self.tracker.pens[idx]
form = PenFormScreen(self.tracker, existing_pen=existing_pen)
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
@ -135,16 +197,26 @@ class PenTrackerApp(App):
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()
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()
self.notify(f"Deleted {removed.Make}", title="Removed")
except Exception:
self.notify("No pen selected to delete", title="Error", severity="error")
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:

View file

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "pen-tracker"
version = "0.3.0"
version = "0.4.0"
authors = [
{ name="Don Harper", email="don@donharper.org" },
]

View file

@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: pen-tracker
Version: 0.3.0
Version: 0.4.0
Summary: A fountain pen collection tracker.
Author-email: Don Harper <don@donharper.org>
Requires-Python: >=3.8

View file

@ -3,7 +3,7 @@ import csv
import argparse
import json
from datetime import datetime
from .engine import PenTracker, Pen
from .engine import PenTracker, Pen, InkTracker, Ink
def validate_date(date_str: str) -> bool:
"""Validate date in YYYY-MM-DD format."""
@ -17,6 +17,10 @@ def validate_date(date_str: str) -> bool:
class CLITracker(PenTracker):
def __init__(self, storage_file=None):
super().__init__(storage_file)
self.ink_tracker = InkTracker()
def add_pen(self):
print("\n--- Add New Fountain Pen ---")
new_pen_data = {}
@ -29,6 +33,9 @@ class CLITracker(PenTracker):
for label, key in fields:
while True:
if key == 'Current-Ink':
value = self.select_ink()
break
value = input(f"Enter {label}: ").strip()
if not value:
value = "N/A"
@ -74,6 +81,15 @@ class CLITracker(PenTracker):
field = self.key_map.get(header, header)
current_val = getattr(pen, field)
while True:
if header == 'Current-Ink':
print(f"Current Ink: {current_val}")
change_ink = input("Change ink? (y/n): ").strip().lower()
if change_ink == 'y':
new_val = self.select_ink()
break
else:
new_val = ""
break
new_val = input(f"{header} [{current_val}]: ").strip()
if not new_val:
break
@ -149,6 +165,69 @@ class CLITracker(PenTracker):
except (ValueError, IndexError):
print("[!] Invalid ID.")
def add_ink(self):
print("\n--- Add New Ink ---")
new_ink_data = {}
fields = [
("Vendor", "Vendor"), ("Name", "Name"), ("Color", "Color"),
("Purchased (YYYY-MM-DD)", "Purchased"), ("Size", "Size"), ("Notes", "Notes")
]
for label, key in fields:
while True:
value = input(f"Enter {label}: ").strip()
if not value:
value = "N/A"
break
if key == 'Purchased':
if validate_date(value):
break
else:
print("Invalid date format. Use YYYY-MM-DD.")
else:
break
new_ink_data[key] = value
new_ink = Ink(**new_ink_data)
self.ink_tracker.inks.append(new_ink)
self.ink_tracker.save_data()
print("\n[✔] Ink added successfully to inks.csv!")
def view_all_inks(self):
if not self.ink_tracker.inks:
print("\n[!] Your ink collection is currently empty.")
return
print("\n" + "="*80)
print(f"{'ID':<4} | {'VENDOR':<12} | {'NAME':<20} | {'COLOR':<15} | {'SIZE':<10}")
print("-" * 80)
for idx, ink in enumerate(self.ink_tracker.inks):
vendor = ink.Vendor[:12]
name = ink.Name[:20]
color = ink.Color[:15]
size = ink.Size[:10]
print(f"{idx:<4} | {vendor:<12} | {name:<20} | {color:<15} | {size:<10}")
print("="*80)
def select_ink(self):
"""Helper method to select an ink from the list."""
if not self.ink_tracker.inks:
print("[!] No inks available. Add some inks first.")
return "N/A"
self.view_all_inks()
try:
idx = int(input("\nEnter the ID of the ink to select (or -1 for N/A): "))
if idx == -1:
return "N/A"
ink = self.ink_tracker.inks[idx]
return ink.Name
except (ValueError, IndexError):
print("[!] Invalid ID.")
return "N/A"
def clear_screen():
os.system('cls' if os.name == 'nt' else 'clear')
@ -180,6 +259,17 @@ def main():
export_parser = subparsers.add_parser('export', help='Export to JSON')
export_parser.add_argument('--output', default='pens.json', help='Output file')
# Ink commands
add_ink_parser = subparsers.add_parser('add-ink', help='Add a new ink')
add_ink_parser.add_argument('--vendor', required=True)
add_ink_parser.add_argument('--name', required=True)
add_ink_parser.add_argument('--color')
add_ink_parser.add_argument('--purchased')
add_ink_parser.add_argument('--size')
add_ink_parser.add_argument('--notes')
list_inks_parser = subparsers.add_parser('list-inks', help='List all inks')
args = parser.parse_args()
tracker = CLITracker(args.csv)
@ -217,6 +307,24 @@ def main():
with open(args.output, 'w') as f:
json.dump([asdict(p) for p in tracker.pens], f, indent=2)
print(f"Exported to {args.output}")
elif args.command == 'add-ink':
if args.purchased and not validate_date(args.purchased):
print("Invalid purchased date format.")
return
ink_data = {
'Vendor': args.vendor,
'Name': args.name,
'Color': args.color or 'N/A',
'Purchased': args.purchased or 'N/A',
'Size': args.size or 'N/A',
'Notes': args.notes or 'N/A'
}
new_ink = Ink(**ink_data)
tracker.ink_tracker.inks.append(new_ink)
tracker.ink_tracker.save_data()
print("Ink added successfully.")
elif args.command == 'list-inks':
tracker.view_all_inks()
else:
# Interactive mode
while True:
@ -225,7 +333,9 @@ def main():
print("2. Add New Pen")
print("3. Edit a Pen")
print("4. Delete a Pen")
print("5. Exit")
print("5. View Ink Collection")
print("6. Add New Ink")
print("7. Exit")
choice = input("\nSelect an option: ")
@ -242,6 +352,12 @@ def main():
clear_screen()
tracker.delete_pen()
elif choice == '5':
clear_screen()
tracker.view_all_inks()
elif choice == '6':
clear_screen()
tracker.add_ink()
elif choice == '7':
print("Goodbye! Happy writing! ✒️")
break
else:

View file

@ -6,6 +6,15 @@ from typing import List
logger = logging.getLogger(__name__)
@dataclass
class Ink:
Vendor: str = "N/A"
Name: str = "N/A"
Color: str = "N/A"
Purchased: str = "N/A"
Size: str = "N/A"
Notes: str = "N/A"
@dataclass
class Pen:
Make: str = "N/A"
@ -21,6 +30,65 @@ class Pen:
Inked_date: str = "N/A"
Notes: str = "N/A"
class InkTracker:
def __init__(self, storage_file: str = None):
if storage_file is None:
storage_file = os.path.abspath('inks.csv')
self.storage_file = os.path.abspath(storage_file)
self.headers = ['Vendor', 'Name', 'Color', 'Purchased', 'Size', 'Notes']
self.inks: List[Ink] = self.load_data()
def _sort_inks(self):
"""Sorts the inks list by Vendor, then by Name alphabetically."""
self.inks.sort(key=lambda x: (x.Vendor.lower(), x.Name.lower()))
def load_data(self) -> List[Ink]:
"""Loads data from the CSV file."""
if not os.path.exists(self.storage_file):
self._create_empty_csv()
return []
inks = []
try:
with open(self.storage_file, mode='r', encoding='utf-8-sig') as f:
reader = csv.DictReader(f)
if reader.fieldnames:
reader.fieldnames = [h.strip() for h in reader.fieldnames]
for row in reader:
if None in row:
extras = row.pop(None)
if extras:
extra_text = ",".join(extras).strip()
if extra_text:
row['Notes'] = (row.get('Notes') or '')
if row['Notes']:
row['Notes'] += ", "
row['Notes'] += extra_text
clean_row = {
k.strip(): (v.strip() if v else "N/A")
for k, v in row.items() if k is not None
}
inks.append(Ink(**clean_row))
inks.sort(key=lambda x: (x.Vendor.lower(), x.Name.lower()))
except Exception as e:
logger.error(f"Error loading inks CSV: {e}")
return inks
def _create_empty_csv(self):
"""Creates the CSV file with headers if it is missing."""
with open(self.storage_file, mode='w', newline='', encoding='utf-8') as f:
writer = csv.DictWriter(f, fieldnames=self.headers)
writer.writeheader()
def save_data(self):
"""Sorts the data before writing to ensure persistent alphabetical order."""
self._sort_inks()
with open(self.storage_file, mode='w', newline='', encoding='utf-8') as f:
writer = csv.DictWriter(f, fieldnames=self.headers)
writer.writeheader()
for ink in self.inks:
writer.writerow(asdict(ink))
class PenTracker:
def __init__(self, storage_file: str = None):
if storage_file is None:

View file

@ -3,7 +3,7 @@ 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
from .engine import PenTracker, Pen, InkTracker, Ink
# --- TUI SCREENS ---
class PenFormScreen(Screen):
@ -54,6 +54,50 @@ class PenFormScreen(Screen):
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."""
@ -89,7 +133,8 @@ class PenTrackerApp(App):
BINDINGS = [
Binding("d", "delete_selected", "Delete Selected"),
Binding("a", "add_new", "Add New Pen"),
Binding("a", "add_new", "Add New"),
Binding("i", "toggle_mode", "Toggle Pens/Inks"),
Binding("q", "quit", "Quit"),
]
@ -100,29 +145,46 @@ class PenTrackerApp(App):
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)
display_cols = ['Make', 'Model', 'Nib', 'Nib-Material', 'Body','Cap', 'Current-Ink', 'Inked-date', 'Notes']
table.add_columns(*display_cols)
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, 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
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:
form = PenFormScreen(self.tracker)
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)
existing_pen = self.tracker.pens[idx]
form = PenFormScreen(self.tracker, existing_pen=existing_pen)
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
@ -135,16 +197,26 @@ class PenTrackerApp(App):
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()
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()
self.notify(f"Deleted {removed.Make}", title="Removed")
except Exception:
self.notify("No pen selected to delete", title="Error", severity="error")
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:

View file

@ -1,7 +1,7 @@
import unittest
import tempfile
import os
from pen_tracker.engine import PenTracker, Pen
from pen_tracker.engine import PenTracker, Pen, InkTracker, Ink
class TestPenTracker(unittest.TestCase):
def setUp(self):
@ -33,5 +33,51 @@ class TestPenTracker(unittest.TestCase):
self.assertEqual(new_tracker.pens[0].Make, "A")
self.assertEqual(new_tracker.pens[1].Make, "Z")
class TestInkTracker(unittest.TestCase):
def setUp(self):
self.temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.csv')
self.temp_file.close()
self.tracker = InkTracker(self.temp_file.name)
def tearDown(self):
os.unlink(self.temp_file.name)
def test_add_and_load_ink(self):
ink = Ink(Vendor="Diamine", Name="Pumpkin", Color="Orange")
self.tracker.inks.append(ink)
self.tracker.save_data()
new_tracker = InkTracker(self.temp_file.name)
self.assertEqual(len(new_tracker.inks), 1)
self.assertEqual(new_tracker.inks[0].Vendor, "Diamine")
self.assertEqual(new_tracker.inks[0].Name, "Pumpkin")
def test_sorting(self):
self.tracker.inks = [
Ink(Vendor="Z", Name="A"),
Ink(Vendor="A", Name="B")
]
self.tracker.save_data()
new_tracker = InkTracker(self.temp_file.name)
self.assertEqual(new_tracker.inks[0].Vendor, "A")
self.assertEqual(new_tracker.inks[1].Vendor, "Z")
def test_loads_row_with_extra_trailing_field(self):
csv_content = (
"Vendor,Name,Color,Purchased,Size,Notes\n"
"Waterman,Intense Black,Black,,\"Cartridge,International Short\",\n"
"Pilot,Black Cartridge,Black,2024-01-01,Cartridge,Good ink\n"
)
with open(self.temp_file.name, 'w', encoding='utf-8') as f:
f.write(csv_content)
new_tracker = InkTracker(self.temp_file.name)
self.assertEqual(len(new_tracker.inks), 2)
self.assertEqual(new_tracker.inks[0].Vendor, "Pilot")
self.assertEqual(new_tracker.inks[1].Vendor, "Waterman")
self.assertEqual(new_tracker.inks[1].Notes, "N/A")
if __name__ == '__main__':
unittest.main()