fix display of inventory

simplified code to only have one way to build table
This commit is contained in:
Don Harper 2026-06-01 10:24:38 -05:00
parent 3a1ab2be66
commit 6476bc659c
8 changed files with 13 additions and 776 deletions

1
.gitignore vendored
View file

@ -1,2 +1,3 @@
*.csv *.csv
*.code-workspace *.code-workspace
build

View file

@ -1,364 +0,0 @@
import os
import csv
import argparse
import json
from datetime import datetime
from .engine import PenTracker, Pen, InkTracker, Ink
def validate_date(date_str: str) -> bool:
"""Validate date in YYYY-MM-DD format."""
if date_str == "N/A":
return True
try:
datetime.strptime(date_str, '%Y-%m-%d')
return True
except ValueError:
return False
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 = {}
fields = [
("Make", "Make"), ("Model", "Model"), ("Date Purchased (YYYY-MM-DD)", "Date-Purchased"),
("Vendor", "Vendor"), ("Nib Size", "Nib"), ("Nib Material", "Nib-Material"),
("Body Material", "Body"), ("Cap Material", "Cap"), ("Postable", "Post"),
("Current Ink", "Current-Ink"), ("Inked Date (YYYY-MM-DD)", "Inked-date"), ("Notes", "Notes")
]
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"
break
if key in ['Date-Purchased', 'Inked-date']:
if validate_date(value):
break
else:
print("Invalid date format. Use YYYY-MM-DD.")
else:
break
new_pen_data[key] = value
# Map to Pen fields
mapped_data = {self.key_map.get(k, k): v for k, v in new_pen_data.items()}
new_pen = Pen(**mapped_data)
self.pens.append(new_pen)
self.save_data()
print("\n[✔] Pen added successfully to Pens.csv!")
def edit_pen(self):
"""Allows the user to select a pen and modify specific fields."""
if not self.pens:
print("\n[!] No pens available to edit.")
return
# Show summary so user knows which ID to pick
print("\n--- Select Pen to Edit ---")
self.show_summary_list()
try:
idx = int(input("\nEnter the ID of the pen to edit: "))
pen = self.pens[idx]
except (ValueError, IndexError):
print("[!] Invalid ID.")
return
print("\n--- Editing Pen Details ---")
print("(Press ENTER without typing to keep the current value)\n")
# We iterate through headers so we don't miss any column
for header in self.headers:
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
if header in ['Date-Purchased', 'Inked-date']:
if validate_date(new_val):
break
else:
print("Invalid date format. Use YYYY-MM-DD.")
else:
break
if new_val: # If the user actually typed something new
setattr(pen, field, new_val)
self.save_data()
print("\n[✔] Pen updated successfully!")
def show_summary_list(self):
"""Helper to print a list without the interactive menu logic."""
print(f"{'ID':<4} | {'MAKE':<12} | {'MODEL':<12} | {'INK':<15}")
print("-" * 55)
for idx, pen in enumerate(self.pens):
make = pen.Make[:12]
model = pen.Model[:12]
ink = pen.Current_Ink[:15]
print(f"{idx:<4} | {make:<12} | {model:<12} | {ink:<15}")
def view_all_pens(self):
if not self.pens:
print("\n[!] Your collection is currently empty.")
return
print("\n" + "="*85)
print(f"{'ID':<4} | {'MAKE':<12} | {'MODEL':<12} | {'INK':<15} | {'INKED DATE':<12}")
print("-" * 85)
for idx, pen in enumerate(self.pens):
make = pen.Make[:12]
model = pen.Model[:12]
ink = pen.Current_Ink[:15]
inkdate = pen.Inked_date[:12]
print(f"{idx:<4} | {make:<12} | {model:<12} | {ink:<15} | {inkdate:<12}")
print("="*85)
choice = input("\nEnter ID to see full details (or 'b' to go back): ")
if choice.lower() != 'b':
try:
self.view_pen_details(int(choice))
except (ValueError, IndexError):
print("[!] Invalid ID.")
def view_pen_details(self, index):
pen = self.pens[index]
print("\n" + ""*45)
print(f"{' FOUNTAIN PEN DETAILS ':=^45}")
for header in self.headers:
field = self.key_map.get(header, header)
value = getattr(pen, field)
print(f"{header:<20}: {value}")
print(""*45)
input("\nPress Enter to return to menu...")
def delete_pen(self):
if not self.pens:
print("\n[!] Nothing to delete.")
return
self.show_summary_list()
try:
idx = int(input("\nEnter the ID of the pen to delete: "))
removed = self.pens.pop(idx)
self.save_data()
print(f"\n[!] Removed: {removed.Make} {removed.Model}")
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')
def main():
# This is the entry point defined in pyproject.toml
parser = argparse.ArgumentParser(description="Fountain Pen Tracker")
parser.add_argument('--csv', default=None, help='Path to CSV file')
subparsers = parser.add_subparsers(dest='command', help='Commands')
# Add command
add_parser = subparsers.add_parser('add', help='Add a new pen')
add_parser.add_argument('--make', required=True)
add_parser.add_argument('--model', required=True)
add_parser.add_argument('--date-purchased')
add_parser.add_argument('--vendor')
add_parser.add_argument('--nib')
add_parser.add_argument('--nib-material')
add_parser.add_argument('--body')
add_parser.add_argument('--cap')
add_parser.add_argument('--post')
add_parser.add_argument('--current-ink')
add_parser.add_argument('--inked-date')
add_parser.add_argument('--notes')
# List command
list_parser = subparsers.add_parser('list', help='List all pens')
# Export command
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)
if args.command == 'add':
# Validate dates
if args.date_purchased and not validate_date(args.date_purchased):
print("Invalid date-purchased format.")
return
if args.inked_date and not validate_date(args.inked_date):
print("Invalid inked-date format.")
return
pen_data = {
'Make': args.make,
'Model': args.model,
'Date-Purchased': args.date_purchased or 'N/A',
'Vendor': args.vendor or 'N/A',
'Nib': args.nib or 'N/A',
'Nib-Material': args.nib_material or 'N/A',
'Body': args.body or 'N/A',
'Cap': args.cap or 'N/A',
'Post': args.post or 'N/A',
'Current-Ink': args.current_ink or 'N/A',
'Inked-date': args.inked_date or 'N/A',
'Notes': args.notes or 'N/A'
}
mapped_data = {tracker.key_map.get(k, k): v for k, v in pen_data.items()}
new_pen = Pen(**mapped_data)
tracker.pens.append(new_pen)
tracker.save_data()
print("Pen added successfully.")
elif args.command == 'list':
tracker.view_all_pens()
elif args.command == 'export':
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:
print("\n🖋️ FOUNTAIN PEN TRACKER (CSV Edition)")
print("1. View Collection Summary")
print("2. Add New Pen")
print("3. Edit a Pen")
print("4. Delete a Pen")
print("5. View Ink Collection")
print("6. Add New Ink")
print("7. Exit")
choice = input("\nSelect an option: ")
if choice == '1':
clear_screen()
tracker.view_all_pens()
elif choice == '2':
clear_screen()
tracker.add_pen()
elif choice == '3':
clear_screen()
tracker.edit_pen()
elif choice == '4':
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:
print("[!] Invalid selection. Please try again.")

View file

@ -1,163 +0,0 @@
import csv
import os
import logging
from dataclasses import dataclass, asdict
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"
Model: str = "N/A"
Date_Purchased: str = "N/A"
Vendor: str = "N/A"
Nib: str = "N/A"
Nib_Material: str = "N/A"
Body: str = "N/A"
Cap: str = "N/A"
Post: str = "N/A"
Current_Ink: str = "N/A"
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.getenv('INK_TRACKER_CSV')
if storage_file is None:
data_home = os.getenv('XDG_DATA_HOME', os.path.expanduser('~/.local/share'))
app_data_dir = os.path.join(data_home, 'pen-tracker')
os.makedirs(app_data_dir, exist_ok=True)
storage_file = os.path.join(app_data_dir, 'inks.csv')
self.storage_file = 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:
storage_file = os.getenv('PEN_TRACKER_CSV')
if storage_file is None:
data_home = os.getenv('XDG_DATA_HOME', os.path.expanduser('~/.local/share'))
app_data_dir = os.path.join(data_home, 'pen-tracker')
os.makedirs(app_data_dir, exist_ok=True)
storage_file = os.path.join(app_data_dir, 'pens.csv')
self.storage_file = storage_file
self.headers = [
'Make', 'Model', 'Date-Purchased', 'Vendor', 'Nib',
'Nib-Material', 'Body', 'Cap', 'Post',
'Current-Ink', 'Inked-date', 'Notes'
]
self.key_map = {
'Date-Purchased': 'Date_Purchased',
'Inked-date': 'Inked_date',
'Nib-Material': 'Nib_Material',
'Current-Ink': 'Current_Ink'
}
self.reverse_key_map = {v: k for k, v in self.key_map.items()}
self.pens: List[Pen] = self.load_data()
def _sort_pens(self):
"""Sorts the pens list by Make, then by Model alphabetically."""
self.pens.sort(key=lambda x: (x.Make.lower(), x.Model.lower()))
def load_data(self) -> List[Pen]:
"""Loads data from the CSV file."""
if not os.path.exists(self.storage_file):
self._create_empty_csv()
return []
pens = []
try:
# Standard, clean way to open the file
with open(self.storage_file, mode='r', encoding='utf-8-sig') as f:
reader = csv.DictReader(f)
for row in reader:
# Strip whitespace from keys and values, handle empty values
clean_row = {k.strip(): (v.strip() if v else "N/A") for k, v in row.items()}
# Map keys to dataclass field names
mapped_row = {self.key_map.get(k, k): v for k, v in clean_row.items()}
pens.append(Pen(**mapped_row))
pens.sort(key=lambda x: (x.Make.lower(), x.Model.lower()))
except Exception as e:
logger.error(f"Error loading CSV: {e}")
return pens
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_pens()
with open(self.storage_file, mode='w', newline='', encoding='utf-8') as f:
writer = csv.DictWriter(f, fieldnames=self.headers)
writer.writeheader()
for pen in self.pens:
row = asdict(pen)
# Map back to CSV keys
csv_row = {k.replace('_', '-'): v for k, v in row.items()}
writer.writerow(csv_row)

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

View file

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

View file

@ -1,6 +1,6 @@
Metadata-Version: 2.4 Metadata-Version: 2.4
Name: pen-tracker Name: pen-tracker
Version: 0.4.1 Version: 0.4.2
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

View file

@ -108,31 +108,25 @@ class CLITracker(PenTracker):
def show_summary_list(self): def show_summary_list(self):
"""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} | {'INK':<15}") print(f"{'ID':<4} | {'MAKE':<12} | {'MODEL':<12} | {'BODY':<12} | {'CAP':<12} | {'NIB':<3} | {'INK':<15} | {'INKED DATE':<12}")
print("-" * 55) print("-" * 102)
for idx, pen in enumerate(self.pens): for idx, pen in enumerate(self.pens):
make = pen.Make[:12] make = pen.Make[:12]
model = pen.Model[:12] model = pen.Model[:12]
body = pen.Body[:12]
cap = pen.Cap[:12]
nib = pen.Nib[:3]
ink = pen.Current_Ink[:15] ink = pen.Current_Ink[:15]
print(f"{idx:<4} | {make:<12} | {model:<12} | {ink:<15}") inkdate = pen.Inked_date[:12]
print(f"{idx:<4} | {make:<12} | {model:<12} | {body:<12} | {cap:<12} | {nib:<3} | {ink:<15} | {inkdate:<12}")
def view_all_pens(self): def view_all_pens(self):
if not self.pens: if not self.pens:
print("\n[!] Your collection is currently empty.") print("\n[!] Your collection is currently empty.")
return return
print("\n" + "="*85) self.show_summary_list()
print(f"{'ID':<4} | {'MAKE':<12} | {'MODEL':<12} | {'INK':<15} | {'INKED DATE':<12}") print("="*102)
print("-" * 85)
for idx, pen in enumerate(self.pens):
make = pen.Make[:12]
model = pen.Model[:12]
ink = pen.Current_Ink[:15]
inkdate = pen.Inked_date[:12]
print(f"{idx:<4} | {make:<12} | {model:<12} | {ink:<15} | {inkdate:<12}")
print("="*85)
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':
@ -302,7 +296,7 @@ def main():
tracker.save_data() tracker.save_data()
print("Pen added successfully.") print("Pen added successfully.")
elif args.command == 'list': elif args.command == 'list':
tracker.view_all_pens() tracker.show_summary_list()
elif args.command == 'export': elif args.command == 'export':
with open(args.output, 'w') as f: with open(args.output, 'w') as f:
json.dump([asdict(p) for p in tracker.pens], f, indent=2) json.dump([asdict(p) for p in tracker.pens], f, indent=2)