From 1a12e6d3c5571636cb30ef45829601c8ec1cf884 Mon Sep 17 00:00:00 2001 From: Don Harper Date: Sun, 26 Apr 2026 22:33:44 -0500 Subject: [PATCH] initial commit --- .gitignore | 2 + LICENSE | 0 README.md | 0 pyproject.toml | 23 +++ src/pen_tracker.egg-info/PKG-INFO | 10 + src/pen_tracker.egg-info/SOURCES.txt | 13 ++ src/pen_tracker.egg-info/dependency_links.txt | 1 + src/pen_tracker.egg-info/entry_points.txt | 3 + src/pen_tracker.egg-info/requires.txt | 1 + src/pen_tracker.egg-info/top_level.txt | 1 + src/pen_tracker/__init__.py | 0 .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 154 bytes .../__pycache__/cli.cpython-313.pyc | Bin 0 -> 11109 bytes .../__pycache__/engine.cpython-313.pyc | Bin 0 -> 3865 bytes .../__pycache__/tui.cpython-313.pyc | Bin 0 -> 8732 bytes src/pen_tracker/cli.py | 194 ++++++++++++++++++ src/pen_tracker/engine.py | 50 +++++ src/pen_tracker/engine.py.bork | 46 +++++ src/pen_tracker/tui.py | 150 ++++++++++++++ 19 files changed, 494 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 pyproject.toml create mode 100644 src/pen_tracker.egg-info/PKG-INFO create mode 100644 src/pen_tracker.egg-info/SOURCES.txt create mode 100644 src/pen_tracker.egg-info/dependency_links.txt create mode 100644 src/pen_tracker.egg-info/entry_points.txt create mode 100644 src/pen_tracker.egg-info/requires.txt create mode 100644 src/pen_tracker.egg-info/top_level.txt create mode 100644 src/pen_tracker/__init__.py create mode 100644 src/pen_tracker/__pycache__/__init__.cpython-313.pyc create mode 100644 src/pen_tracker/__pycache__/cli.cpython-313.pyc create mode 100644 src/pen_tracker/__pycache__/engine.cpython-313.pyc create mode 100644 src/pen_tracker/__pycache__/tui.cpython-313.pyc create mode 100644 src/pen_tracker/cli.py create mode 100644 src/pen_tracker/engine.py create mode 100644 src/pen_tracker/engine.py.bork create mode 100644 src/pen_tracker/tui.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dd5859d --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.csv +*.code-workspace diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e69de29 diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..5a7f7c3 --- /dev/null +++ b/pyproject.toml @@ -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"] diff --git a/src/pen_tracker.egg-info/PKG-INFO b/src/pen_tracker.egg-info/PKG-INFO new file mode 100644 index 0000000..1343b95 --- /dev/null +++ b/src/pen_tracker.egg-info/PKG-INFO @@ -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 +Requires-Python: >=3.8 +Description-Content-Type: text/markdown +License-File: LICENSE +Requires-Dist: textual +Dynamic: license-file diff --git a/src/pen_tracker.egg-info/SOURCES.txt b/src/pen_tracker.egg-info/SOURCES.txt new file mode 100644 index 0000000..4d050c2 --- /dev/null +++ b/src/pen_tracker.egg-info/SOURCES.txt @@ -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 \ No newline at end of file diff --git a/src/pen_tracker.egg-info/dependency_links.txt b/src/pen_tracker.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/pen_tracker.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/src/pen_tracker.egg-info/entry_points.txt b/src/pen_tracker.egg-info/entry_points.txt new file mode 100644 index 0000000..5530550 --- /dev/null +++ b/src/pen_tracker.egg-info/entry_points.txt @@ -0,0 +1,3 @@ +[console_scripts] +pen-tracker = pen_tracker.cli:main +pen-tui = pen_tracker.tui:main diff --git a/src/pen_tracker.egg-info/requires.txt b/src/pen_tracker.egg-info/requires.txt new file mode 100644 index 0000000..a75a51d --- /dev/null +++ b/src/pen_tracker.egg-info/requires.txt @@ -0,0 +1 @@ +textual diff --git a/src/pen_tracker.egg-info/top_level.txt b/src/pen_tracker.egg-info/top_level.txt new file mode 100644 index 0000000..fcae849 --- /dev/null +++ b/src/pen_tracker.egg-info/top_level.txt @@ -0,0 +1 @@ +pen_tracker diff --git a/src/pen_tracker/__init__.py b/src/pen_tracker/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/pen_tracker/__pycache__/__init__.cpython-313.pyc b/src/pen_tracker/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e6088d782dddc712d3f40c10ffbab5332bc839ba GIT binary patch literal 154 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 J7G{7>`G0XwULybi literal 0 HcmV?d00001 diff --git a/src/pen_tracker/__pycache__/engine.cpython-313.pyc b/src/pen_tracker/__pycache__/engine.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8b9c1b60a6444dc69997813fcf95ed227e0ee287 GIT binary patch literal 3865 zcmbVPU2GKB6~6Pge;#{T+Y2@rXKWnfP4F%$&;-Pf78?o$FJ{MUiVO_n-P!fj>zVDH zF~)v~P#-E=Y6Pi-Nc$3^K47FuNuS!M3IbBoHx|3Gb>RW2dC5!V;%h&qR>JOoP8(l(}kr8HP26J+d&hb+Q^D?&`VL|4B z`(y#QU-khH$bK9w1Y#i)8q|y-tY*hG{5AD0>rG2vLS+vAt7n1TMnBn7gr`dhaQr-z zK}E*XWKdf039#$6-5u*KjTbzfvZhODMnN~Ucxjph6h$|5TTw_;b(iCwl5UpO0qh1^ zZtGbLw7z8dme4H!OK~NHqOEsQAEp+XyJqphQLDvkPZ(x zRj#1fL@1h58jdl9xo*)nNVu6A1r8V0iP4;Tl zyo9&GFmQnz*QQf37B^B4c>1mwgXtEo_bjAToFLIneQTg2qQGvT7Xk5Ra4vYWVXmRP zi}c&}Q)rQ1jK8LG!@cfzhN!DF@2}OZv7N z@(x)-3DgCDgK0-2b)M1Yvs2*6+(C{-20J2pwrRTd8tOzD*7L>SGF%fnbp;`V&+zF; zjVUxeUz*UcFzmCqy%6#+q8Fh_*p1R~Mo7~WsUdGv#^Pb2#>fd;g%va@uwEEs`gfWFC< z@=PANbH)5KJI_K|wIrHXr93t#oCMP+4@)%r#tDOGf|GW>_q|?AFVK`b0SSf#G$U)~ zbfYlCr4q^1%(jt^%hCyqO)SwY4IL6paY%|a5Z1IHU#s>Q@oCo~Wm!(XCJ`lzHAwr4 zHc_&tm8^A*@Fqks;iwqu*RyuoN&H06ZEeCLf@Nd9M0{!qta7*$7EFRCu4lE9t(yiB zN?14SSkQ?}H&U5A@xeRb@*o^GrwB7nm}?}MEo!QvI10_8sFyr)QVgcxBh7=At2SP_ zcWC@u{{nUceHD(}oSK`u`TpGdALW+X6Xo{AQh0AUymwjLvLtqv#m+^sYq=>pd-_E< zGRv>9q2N1TMWVNIOIvrBx9*@>{~~!ea&Oxw+kV^nr;hpapI*3s;dd7wM4xt?EDtHSPM4*t z%grr!&U|?0Zt%%0eW9s$6@IY`Dnj@ z9=0a>_H&;H_V>j@pC4`k`B8HV=pV&>RNf!zXZ??k`a$zp2=(`HkHZc9yST@@1duo6 zIGXvL9QObO$Z>#pmmqF7$#QOXk@}!3nhc9PWk|@f+Gy5vmis8(Q}n63XjZF=wa~nw5(A@E*`Qp@bm^A~#|#V2Blr z>6J(7rf9=`DNIR|Bi>Itd`<_p=(Yp$UC_Txd)n54{=dSL`egM31Mt^JZgnj+?kYF# zn&0&qe(C9%CMwl9hj0J33#S*kz# zW&PRL7Tx$8JVkKrg9^0h7AWZ z&IdttJe4Y#so$ult*R%0DO!ux5O_roq=u?D$M;0q$!a<1cNhfCA1ZAwrM zO@gKb#WUnxD7iP3g_4OOGsf`|aLj=pK1!uylqG?sAeYi4TLU!QM|E^Tn1X=>T{M|f zmXmbx2~ci;771GFHO;Nt*Jss=mIB$j3d}8w!gFD$ub+P(h&ga;D~oL(4Sjs^*B2MX z-LL%XK}XAxd&yAp5clv<=y<>%_WH^JOC|Twz`Y8inSUJ-pEIPthmzxC5yvtb}F( vOe+UxJU&k)T6uVNv+Mp*oCY1uvQ8zs!Z8f<427Pd;441P^t?pCoMHa~Uh^ah literal 0 HcmV?d00001 diff --git a/src/pen_tracker/__pycache__/tui.cpython-313.pyc b/src/pen_tracker/__pycache__/tui.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..894ebbc5b1e08d1d125cecebe3924c949753f6f7 GIT binary patch literal 8732 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 U9Cw~u?As!&u{pr=oQv)Ee?*-WwEzGB literal 0 HcmV?d00001 diff --git a/src/pen_tracker/cli.py b/src/pen_tracker/cli.py new file mode 100644 index 0000000..ab79293 --- /dev/null +++ b/src/pen_tracker/cli.py @@ -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.") diff --git a/src/pen_tracker/engine.py b/src/pen_tracker/engine.py new file mode 100644 index 0000000..b952ce5 --- /dev/null +++ b/src/pen_tracker/engine.py @@ -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) diff --git a/src/pen_tracker/engine.py.bork b/src/pen_tracker/engine.py.bork new file mode 100644 index 0000000..127b3ad --- /dev/null +++ b/src/pen_tracker/engine.py.bork @@ -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) diff --git a/src/pen_tracker/tui.py b/src/pen_tracker/tui.py new file mode 100644 index 0000000..92cd264 --- /dev/null +++ b/src/pen_tracker/tui.py @@ -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()