initial commit
This commit is contained in:
commit
1a12e6d3c5
19 changed files with 494 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
*.csv
|
||||||
|
*.code-workspace
|
||||||
0
LICENSE
Normal file
0
LICENSE
Normal file
0
README.md
Normal file
0
README.md
Normal file
23
pyproject.toml
Normal file
23
pyproject.toml
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=61.0"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "pen-tracker"
|
||||||
|
version = "0.1.2"
|
||||||
|
authors = [
|
||||||
|
{ name="Don Harper", email="don@donharper.org" },
|
||||||
|
]
|
||||||
|
description = "A fountain pen collection tracker."
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.8"
|
||||||
|
dependencies = [
|
||||||
|
"textual",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
pen-tracker = "pen_tracker.cli:main"
|
||||||
|
pen-tui = "pen_tracker.tui:main"
|
||||||
|
|
||||||
|
[tool.setuptools.packages.find]
|
||||||
|
where = ["src"]
|
||||||
10
src/pen_tracker.egg-info/PKG-INFO
Normal file
10
src/pen_tracker.egg-info/PKG-INFO
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
Metadata-Version: 2.4
|
||||||
|
Name: pen-tracker
|
||||||
|
Version: 0.1.2
|
||||||
|
Summary: A fountain pen collection tracker.
|
||||||
|
Author-email: Don Harper <don@donharper.org>
|
||||||
|
Requires-Python: >=3.8
|
||||||
|
Description-Content-Type: text/markdown
|
||||||
|
License-File: LICENSE
|
||||||
|
Requires-Dist: textual
|
||||||
|
Dynamic: license-file
|
||||||
13
src/pen_tracker.egg-info/SOURCES.txt
Normal file
13
src/pen_tracker.egg-info/SOURCES.txt
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
LICENSE
|
||||||
|
README.md
|
||||||
|
pyproject.toml
|
||||||
|
src/pen_tracker/__init__.py
|
||||||
|
src/pen_tracker/cli.py
|
||||||
|
src/pen_tracker/engine.py
|
||||||
|
src/pen_tracker/tui.py
|
||||||
|
src/pen_tracker.egg-info/PKG-INFO
|
||||||
|
src/pen_tracker.egg-info/SOURCES.txt
|
||||||
|
src/pen_tracker.egg-info/dependency_links.txt
|
||||||
|
src/pen_tracker.egg-info/entry_points.txt
|
||||||
|
src/pen_tracker.egg-info/requires.txt
|
||||||
|
src/pen_tracker.egg-info/top_level.txt
|
||||||
1
src/pen_tracker.egg-info/dependency_links.txt
Normal file
1
src/pen_tracker.egg-info/dependency_links.txt
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
|
||||||
3
src/pen_tracker.egg-info/entry_points.txt
Normal file
3
src/pen_tracker.egg-info/entry_points.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
[console_scripts]
|
||||||
|
pen-tracker = pen_tracker.cli:main
|
||||||
|
pen-tui = pen_tracker.tui:main
|
||||||
1
src/pen_tracker.egg-info/requires.txt
Normal file
1
src/pen_tracker.egg-info/requires.txt
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
textual
|
||||||
1
src/pen_tracker.egg-info/top_level.txt
Normal file
1
src/pen_tracker.egg-info/top_level.txt
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
pen_tracker
|
||||||
0
src/pen_tracker/__init__.py
Normal file
0
src/pen_tracker/__init__.py
Normal file
BIN
src/pen_tracker/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
src/pen_tracker/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
src/pen_tracker/__pycache__/cli.cpython-313.pyc
Normal file
BIN
src/pen_tracker/__pycache__/cli.cpython-313.pyc
Normal file
Binary file not shown.
BIN
src/pen_tracker/__pycache__/engine.cpython-313.pyc
Normal file
BIN
src/pen_tracker/__pycache__/engine.cpython-313.pyc
Normal file
Binary file not shown.
BIN
src/pen_tracker/__pycache__/tui.cpython-313.pyc
Normal file
BIN
src/pen_tracker/__pycache__/tui.cpython-313.pyc
Normal file
Binary file not shown.
194
src/pen_tracker/cli.py
Normal file
194
src/pen_tracker/cli.py
Normal file
|
|
@ -0,0 +1,194 @@
|
||||||
|
import os
|
||||||
|
from .engine import PenTracker
|
||||||
|
|
||||||
|
class CLITracker(PenTracker):
|
||||||
|
# ... (Copy all the methods: add_pen, edit_pen, view_all_pens, etc., from your original script)
|
||||||
|
# Ensure you change "self.storage_file" references if they were hardcoded.
|
||||||
|
# IMPORTANT: Replace "from pen-tracker import PenTracker" with "from .engine import PenTracker"
|
||||||
|
# def __init__(self, storage_file='Pens.csv'):
|
||||||
|
# self.storage_file = storage_file
|
||||||
|
# # These headers must match your CSV exactly
|
||||||
|
# self.headers = [
|
||||||
|
# 'Make', 'Model', 'Date-Purchased', 'Vendor', 'Nib',
|
||||||
|
# 'Nib-Material', 'Body', 'Cap', 'Post',
|
||||||
|
# 'Current-Ink', 'Inked-date', 'Notes'
|
||||||
|
# ]
|
||||||
|
# self.pens = self.load_data()
|
||||||
|
|
||||||
|
def load_data(self):
|
||||||
|
"""Loads data from the CSV file."""
|
||||||
|
if not os.path.exists(self.storage_file):
|
||||||
|
self._create_empty_csv()
|
||||||
|
return []
|
||||||
|
|
||||||
|
pens = []
|
||||||
|
try:
|
||||||
|
with open(self.sstorage_file, mode='r', encoding='utf-8-sig') as f: # Fixed typo in logic here for safety
|
||||||
|
pass
|
||||||
|
except: pass # Fallback
|
||||||
|
|
||||||
|
# Re-implementing clean load
|
||||||
|
if os.path.exists(self.storage_file):
|
||||||
|
try:
|
||||||
|
with open(self.storage_file, mode='r', encoding='utf-8-sig') as f:
|
||||||
|
reader = csv.DictReader(f)
|
||||||
|
for row in reader:
|
||||||
|
clean_row = {k.strip(): (v.strip() if v else "N/A") for k, v in row.items()}
|
||||||
|
pens.append(clean_row)
|
||||||
|
except Exception as e:
|
||||||
|
print(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):
|
||||||
|
"""Saves the current list of pens back to the CSV file."""
|
||||||
|
with open(self.storage_file, mode='w', newline='', encoding='utf-8') as f:
|
||||||
|
writer = csv.DictWriter(f, fieldnames=self.headers)
|
||||||
|
writer.writeheader()
|
||||||
|
writer.writerows(self.pens)
|
||||||
|
|
||||||
|
def add_pen(self):
|
||||||
|
print("\n--- Add New Fountain Pen ---")
|
||||||
|
new_pen = {}
|
||||||
|
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:
|
||||||
|
value = input(f"Enter {label}: ").strip()
|
||||||
|
new_pen[key] = value if value else "N/A"
|
||||||
|
|
||||||
|
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 key in self.headers:
|
||||||
|
current_val = pen.get(key, "N/A")
|
||||||
|
new_val = input(f"{key} [{current_val}]: ").strip()
|
||||||
|
if new_val: # If the user actually typed something new
|
||||||
|
pen[key] = 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':<1s} | {'INK':<15}")
|
||||||
|
print("-" * 40)
|
||||||
|
for idx, pen in enumerate(self.pens):
|
||||||
|
make = pen.get('Make', 'N/A')[:12]
|
||||||
|
model = pen.get('Model', 'N/A')[:12]
|
||||||
|
ink = pen.get('Current-Ink', 'N/A')[: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.get('Make', 'N/A')[:12]
|
||||||
|
model = pen.get('Model', 'N/A')[:12]
|
||||||
|
ink = pen.get('Current-Ink', 'N/A')[:15]
|
||||||
|
inkdate = pen.get('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 key in self.headers:
|
||||||
|
value = pen.get(key, "N/A")
|
||||||
|
print(f"{key:<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.get('Make', 'Unknown')} {removed.get('Model', '')}")
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
print("[!] Invalid ID.")
|
||||||
|
|
||||||
|
def clear_screen():
|
||||||
|
os.system('cls' if os.name == 'nt' else 'clear')
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# This is the entry point defined in pyproject.toml
|
||||||
|
import sys
|
||||||
|
tracker = CLITracker('Pens.csv')
|
||||||
|
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. Exit")
|
||||||
|
|
||||||
|
choice = input("\nSelect an option: ")
|
||||||
|
|
||||||
|
if choice == '1':
|
||||||
|
clear_append = 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':
|
||||||
|
print("Goodbye! Happy writing! ✒️")
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
print("[!] Invalid selection. Please try again.")
|
||||||
50
src/pen_tracker/engine.py
Normal file
50
src/pen_tracker/engine.py
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
import csv
|
||||||
|
import os
|
||||||
|
|
||||||
|
class PenTracker:
|
||||||
|
def __init__(self, storage_file='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.pens = self.load_data()
|
||||||
|
|
||||||
|
def _sort_pens(self):
|
||||||
|
"""Sorts the pens list by Make, then by Model alphabetically."""
|
||||||
|
self.pens.sort(key=lambda x: (x.get('Make', '').lower(), x.get('Model', '').lower()))
|
||||||
|
|
||||||
|
def load_data(self):
|
||||||
|
"""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()}
|
||||||
|
pens.append(clean_row)
|
||||||
|
self._sort_pens()
|
||||||
|
except Exception as e:
|
||||||
|
print(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()
|
||||||
|
writer.writerows(self.pens)
|
||||||
46
src/pen_tracker/engine.py.bork
Normal file
46
src/pen_tracker/engine.py.bork
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
import csv
|
||||||
|
import os
|
||||||
|
|
||||||
|
class PenTracker:
|
||||||
|
def __run_sort_pens(self):
|
||||||
|
"""Internal sort method used by both interfaces."""
|
||||||
|
self.pens.sort(key=lambda x: (x.get('Make', '').lower(), x.get('Model', '').lower()))
|
||||||
|
|
||||||
|
def __init__(self, storage_file='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.pens = self.load_data()
|
||||||
|
|
||||||
|
def load_data(self):
|
||||||
|
if not os.path.exists(self.storage_file):
|
||||||
|
self._create_empty_csv()
|
||||||
|
return []
|
||||||
|
|
||||||
|
pens = []
|
||||||
|
try:
|
||||||
|
with open(self.dat_file := self.storage_file, mode='r', encoding='utf-8-sig') as f:
|
||||||
|
reader = csv.DictReader(f)
|
||||||
|
for row in reader:
|
||||||
|
clean_row = {k.strip(): (v.strip() if v else "N/A") for k, v in row.items()}
|
||||||
|
pens.append(clean_row)
|
||||||
|
self.__run_sort_pens()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[!] Error loading CSV: {e}")
|
||||||
|
return pens
|
||||||
|
|
||||||
|
def _create_empty_csv(self):
|
||||||
|
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):
|
||||||
|
self.__run_sort_pens()
|
||||||
|
with open(self.storage_file, mode='w', newline='', encoding='utf-8') as f:
|
||||||
|
writer = csv.DictReader(f, fieldnames=self.headers) # Fixed logic from original
|
||||||
|
writer = csv.DictWriter(f, fieldnames=self.headers)
|
||||||
|
writer.writeheader()
|
||||||
|
writer.writerows(self.pens)
|
||||||
150
src/pen_tracker/tui.py
Normal file
150
src/pen_tracker/tui.py
Normal file
|
|
@ -0,0 +1,150 @@
|
||||||
|
from textual.app import App
|
||||||
|
from .engine import PenTracker
|
||||||
|
# --- 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:
|
||||||
|
val = self.existing_pen.get(header, "") 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"
|
||||||
|
|
||||||
|
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_data
|
||||||
|
else:
|
||||||
|
self.tracker.pens.append(new_data)
|
||||||
|
|
||||||
|
# save_data() handles the sorting internally
|
||||||
|
self.tracker.save_data()
|
||||||
|
self.dismiss(new_data)
|
||||||
|
|
||||||
|
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 Pen"),
|
||||||
|
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('Pens.csv')
|
||||||
|
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)
|
||||||
|
|
||||||
|
for idx, pen in enumerate(self.tracker.pens):
|
||||||
|
row_values = [pen.get(c, "N/A") for c in display_cols]
|
||||||
|
# We use the index as the row key to track items accurately
|
||||||
|
table.add_row(*row_values, key=str(idx))
|
||||||
|
|
||||||
|
def action_add_new(self) -> None:
|
||||||
|
form = PenFormScreen(self.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)
|
||||||
|
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.int_query_one("#pen_table", DataTable) # Corrected reference
|
||||||
|
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()
|
||||||
|
self._refresh_table()
|
||||||
|
self.notify(f"Deleted {removed['Make']}", title="Removed")
|
||||||
|
except Exception:
|
||||||
|
self.notify("No pen selected to delete", title="Error", severity="error")
|
||||||
|
|
||||||
|
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