diff options
| author | Craig Jennings <c@cjennings.net> | 2026-07-02 17:09:42 -0400 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-07-02 17:09:42 -0400 |
| commit | 6b7791ccf8195247649914a7ce3cbda9bbcdaf06 (patch) | |
| tree | f80bea045f44a2b7d812fb795f9c80ce2c4027fc /pocketbook | |
| parent | d9d454a608d1a6a7ae690af0b68472401c477fd6 (diff) | |
| download | archsetup-6b7791ccf8195247649914a7ce3cbda9bbcdaf06.tar.gz archsetup-6b7791ccf8195247649914a7ce3cbda9bbcdaf06.zip | |
chore(pocketbook)!: remove the in-tree pocketbook package
Craig's finish-or-cancel checkpoint resolved to remove: the app left daily use in May and the org-capture popup covers quick notes. The pip install, launcher, and Super+P bind went with it (dotfiles a750cb4); the three pocketbook tasks are closed.
Diffstat (limited to 'pocketbook')
| -rw-r--r-- | pocketbook/.gitignore | 6 | ||||
| -rw-r--r-- | pocketbook/Makefile | 16 | ||||
| -rw-r--r-- | pocketbook/pyproject.toml | 21 | ||||
| -rw-r--r-- | pocketbook/src/pocketbook/__init__.py | 0 | ||||
| -rw-r--r-- | pocketbook/src/pocketbook/__main__.py | 26 | ||||
| -rw-r--r-- | pocketbook/src/pocketbook/app.py | 243 | ||||
| -rw-r--r-- | pocketbook/src/pocketbook/layer_shell.py | 24 | ||||
| -rw-r--r-- | pocketbook/src/pocketbook/note.py | 28 | ||||
| -rw-r--r-- | pocketbook/src/pocketbook/note_widget.py | 93 | ||||
| -rw-r--r-- | pocketbook/src/pocketbook/panel.py | 97 | ||||
| -rw-r--r-- | pocketbook/src/pocketbook/store.py | 73 | ||||
| -rw-r--r-- | pocketbook/src/pocketbook/style.css | 102 | ||||
| -rw-r--r-- | pocketbook/tests/__init__.py | 0 | ||||
| -rw-r--r-- | pocketbook/tests/conftest.py | 17 | ||||
| -rw-r--r-- | pocketbook/tests/test_app_toggle.py | 96 | ||||
| -rw-r--r-- | pocketbook/tests/test_note.py | 69 | ||||
| -rw-r--r-- | pocketbook/tests/test_panel.py | 40 | ||||
| -rw-r--r-- | pocketbook/tests/test_store.py | 103 |
18 files changed, 0 insertions, 1054 deletions
diff --git a/pocketbook/.gitignore b/pocketbook/.gitignore deleted file mode 100644 index b9c0bb9..0000000 --- a/pocketbook/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -__pycache__/ -*.egg-info/ -dist/ -build/ -.pytest_cache/ -*.pyc diff --git a/pocketbook/Makefile b/pocketbook/Makefile deleted file mode 100644 index f09dbbb..0000000 --- a/pocketbook/Makefile +++ /dev/null @@ -1,16 +0,0 @@ -.PHONY: test lint install uninstall clean - -test: - python -m pytest tests/ -v - -lint: - python -m ruff check src/ tests/ - -install: - pip install --user -e . - -uninstall: - pip uninstall -y quicknotes - -clean: - rm -rf build/ dist/ src/*.egg-info diff --git a/pocketbook/pyproject.toml b/pocketbook/pyproject.toml deleted file mode 100644 index afc5c71..0000000 --- a/pocketbook/pyproject.toml +++ /dev/null @@ -1,21 +0,0 @@ -[build-system] -requires = ["setuptools>=68.0"] -build-backend = "setuptools.build_meta" - -[project] -name = "pocketbook" -version = "0.1.0" -description = "GTK4 layer-shell notes panel for Hyprland" -requires-python = ">=3.12" -dependencies = [ - "PyGObject", -] - -[project.scripts] -pocketbook = "pocketbook.__main__:main" - -[tool.setuptools.packages.find] -where = ["src"] - -[tool.pytest.ini_options] -testpaths = ["tests"] diff --git a/pocketbook/src/pocketbook/__init__.py b/pocketbook/src/pocketbook/__init__.py deleted file mode 100644 index e69de29..0000000 --- a/pocketbook/src/pocketbook/__init__.py +++ /dev/null diff --git a/pocketbook/src/pocketbook/__main__.py b/pocketbook/src/pocketbook/__main__.py deleted file mode 100644 index daac841..0000000 --- a/pocketbook/src/pocketbook/__main__.py +++ /dev/null @@ -1,26 +0,0 @@ -import argparse -import ctypes -import os - -# gtk4-layer-shell must be loaded before libwayland-client. -# See: https://github.com/wmww/gtk4-layer-shell/blob/main/linking.md -_LIB = "/usr/lib/libgtk4-layer-shell.so" -if os.path.exists(_LIB): - ctypes.cdll.LoadLibrary(_LIB) - - -def main(): - parser = argparse.ArgumentParser(description="Pocketbook panel") - parser.add_argument( - "--hidden", - action="store_true", - help="Start with panel hidden (daemon mode)", - ) - args = parser.parse_args() - - from pocketbook.app import run_app - raise SystemExit(run_app(start_hidden=args.hidden)) - - -if __name__ == "__main__": - main() diff --git a/pocketbook/src/pocketbook/app.py b/pocketbook/src/pocketbook/app.py deleted file mode 100644 index be13fec..0000000 --- a/pocketbook/src/pocketbook/app.py +++ /dev/null @@ -1,243 +0,0 @@ -import sys -from pathlib import Path - -APP_ID = "net.cjennings.pocketbook" -DATA_DIR = Path.home() / ".local" / "share" / "pocketbook" -CSS_PATH = Path(__file__).parent / "style.css" - - -class ToggleStateMachine: - """Testable toggle logic, separated from GTK.""" - - def __init__(self, start_hidden: bool = False): - self.visible = not start_hidden - - def toggle(self): - self.visible = not self.visible - - -def navigate(current_index: int | None, total: int, direction: int) -> int | None: - """Return the target index for note navigation. - - Args: - current_index: Index of focused note, or None if no note focused. - total: Number of notes. - direction: +1 for next, -1 for previous. - - Returns: - Target index, or None if no notes exist. - """ - if total == 0: - return None - if current_index is None: - return 0 if direction == 1 else total - 1 - target = current_index + direction - return max(0, min(target, total - 1)) - - -class EscapeStateMachine: - """Testable escape logic: editing → browse → hidden.""" - - def escape(self, is_editing: bool, toggle: ToggleStateMachine): - """Returns action: 'exit_edit', 'hide', or None.""" - if is_editing: - return "exit_edit" - else: - toggle.toggle() - return "hide" - - -def _build_gtk_app_class(): - """Lazy-load GTK classes to keep ToggleStateMachine importable without GTK.""" - import gi - gi.require_version("Gtk", "4.0") - from gi.repository import Gtk, Gdk - - from pocketbook.store import NoteStore - from pocketbook.panel import PanelController, PanelWidget - from pocketbook.layer_shell import configure_layer_shell - - class PocketbookApp(Gtk.Application): - def __init__(self, start_hidden: bool = False): - super().__init__(application_id=APP_ID) - self._start_hidden = start_hidden - self._window = None - self._panel = None - self._toggle = ToggleStateMachine(start_hidden=start_hidden) - self._esc_sm = EscapeStateMachine() - self._first_activate = True - - def do_activate(self): - if self._first_activate: - self._first_activate = False - self._build_ui() - if not self._start_hidden: - self._window.set_visible(True) - self._focus_first_note() - else: - self._toggle.toggle() - self._window.set_visible(self._toggle.visible) - if self._toggle.visible: - self._focus_first_note() - - def _focus_first_note(self): - rows = self._get_note_rows() - if rows: - rows[0].grab_focus() - - def _build_ui(self): - self._window = Gtk.Window(application=self) - self._window.set_default_size(420, -1) - - configure_layer_shell(self._window) - self._load_css() - - store = NoteStore(DATA_DIR) - controller = PanelController(store) - self._panel = PanelWidget(controller) - self._window.set_child(self._panel) - - self._setup_shortcuts() - - def _load_css(self): - if CSS_PATH.exists(): - provider = Gtk.CssProvider() - provider.load_from_path(str(CSS_PATH)) - Gtk.StyleContext.add_provider_for_display( - Gdk.Display.get_default(), - provider, - Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION, - ) - - def _setup_shortcuts(self): - from pocketbook.note_widget import NoteRow - - sc = Gtk.ShortcutController() - sc.set_scope(Gtk.ShortcutScope.GLOBAL) - - for key, handler in [ - ("Escape", self._on_escape), - ("<Control>n", self._on_ctrl_n), - ("<Control>j", self._on_next_note), - ("<Control>k", self._on_prev_note), - ("Delete", self._on_delete_note), - ("Return", self._on_enter), - ]: - sc.add_shortcut(Gtk.Shortcut( - trigger=Gtk.ShortcutTrigger.parse_string(key), - action=Gtk.CallbackAction.new(handler), - )) - - self._window.add_controller(sc) - self._note_row_class = NoteRow - - # Arrow keys need EventControllerKey to beat GTK's focus navigation - key_ctrl = Gtk.EventControllerKey() - key_ctrl.set_propagation_phase(Gtk.PropagationPhase.CAPTURE) - key_ctrl.connect("key-pressed", self._on_key_pressed) - self._window.add_controller(key_ctrl) - - def _get_focused_note_row(self): - """Find the NoteRow that currently has focus (or contains the focused widget).""" - focus = self._window.get_focus() - widget = focus - while widget: - if isinstance(widget, self._note_row_class): - return widget - widget = widget.get_parent() - return None - - def _get_note_rows(self): - """Get all NoteRow children from the panel.""" - rows = [] - child = self._panel._list_box.get_first_child() - while child: - if isinstance(child, self._note_row_class): - rows.append(child) - child = child.get_next_sibling() - return rows - - def _on_escape(self, _widget, _args): - focus = self._window.get_focus() - is_editing = bool(focus and isinstance(focus, (Gtk.Entry, Gtk.TextView, Gtk.Text))) - action = self._esc_sm.escape(is_editing, self._toggle) - if action == "exit_edit": - row = self._get_focused_note_row() - if row: - row.exit_edit_mode() - else: - self._window.set_focus(None) - elif action == "hide": - self._window.set_visible(False) - return True - - def _on_ctrl_n(self, _widget, _args): - self._panel.add_note() - return True - - def _on_next_note(self, _widget, _args): - self._navigate_notes(1) - return True - - def _on_prev_note(self, _widget, _args): - self._navigate_notes(-1) - return True - - def _navigate_notes(self, direction): - rows = self._get_note_rows() - current = self._get_focused_note_row() - current_idx = rows.index(current) if current in rows else None - target_idx = navigate(current_idx, len(rows), direction) - if target_idx is not None: - rows[target_idx].grab_focus() - - def _on_key_pressed(self, _controller, keyval, _keycode, _state): - from gi.repository import Gdk as _Gdk - focus = self._window.get_focus() - is_editing = isinstance(focus, (Gtk.Entry, Gtk.TextView, Gtk.Text)) - if keyval == _Gdk.KEY_Down and not is_editing: - self._on_next_note(None, None) - return True # stop propagation - if keyval == _Gdk.KEY_Up and not is_editing: - self._on_prev_note(None, None) - return True - if keyval == _Gdk.KEY_s and is_editing and (_state & _Gdk.ModifierType.CONTROL_MASK): - row = self._get_focused_note_row() - if row: - row.exit_edit_mode() - return True - if keyval == _Gdk.KEY_Return and not is_editing: - row = self._get_focused_note_row() - if row: - row.focus_title() - return True - return False # let GTK handle it - - def _on_delete_note(self, _widget, _args): - focus = self._window.get_focus() - # Only delete in browse mode (NoteRow focused, not a text field) - if isinstance(focus, (Gtk.Entry, Gtk.TextView, Gtk.Text)): - return False # pass through to text widget - row = self._get_focused_note_row() - if row: - self._panel._on_note_deleted(row._filename) - self._focus_first_note() - return True - - def _on_enter(self, _widget, _args): - # Only enter edit mode if a NoteRow itself is focused (browse mode), - # not when already editing a text field - focus = self._window.get_focus() - if isinstance(focus, self._note_row_class): - focus.focus_title() - return True - # Let Enter pass through normally in text fields - return False - - return PocketbookApp - - -def run_app(start_hidden: bool = False) -> int: - PocketbookApp = _build_gtk_app_class() - app = PocketbookApp(start_hidden=start_hidden) - return app.run(sys.argv[:1]) diff --git a/pocketbook/src/pocketbook/layer_shell.py b/pocketbook/src/pocketbook/layer_shell.py deleted file mode 100644 index bb36373..0000000 --- a/pocketbook/src/pocketbook/layer_shell.py +++ /dev/null @@ -1,24 +0,0 @@ -import gi -gi.require_version("Gtk", "4.0") -gi.require_version("Gtk4LayerShell", "1.0") -from gi.repository import Gtk, Gtk4LayerShell - - -def configure_layer_shell(window: Gtk.Window): - """Configure a GTK4 window as a layer-shell surface.""" - Gtk4LayerShell.init_for_window(window) - Gtk4LayerShell.set_layer(window, Gtk4LayerShell.Layer.OVERLAY) - Gtk4LayerShell.set_namespace(window, "pocketbook") - - # Anchor top-right - Gtk4LayerShell.set_anchor(window, Gtk4LayerShell.Edge.TOP, True) - Gtk4LayerShell.set_anchor(window, Gtk4LayerShell.Edge.RIGHT, True) - - # Margins: below waybar, inset from edge - Gtk4LayerShell.set_margin(window, Gtk4LayerShell.Edge.TOP, 50) - Gtk4LayerShell.set_margin(window, Gtk4LayerShell.Edge.RIGHT, 10) - - # On-demand keyboard: receives keys when focused, doesn't steal focus on appear - Gtk4LayerShell.set_keyboard_mode( - window, Gtk4LayerShell.KeyboardMode.ON_DEMAND - ) diff --git a/pocketbook/src/pocketbook/note.py b/pocketbook/src/pocketbook/note.py deleted file mode 100644 index e812eea..0000000 --- a/pocketbook/src/pocketbook/note.py +++ /dev/null @@ -1,28 +0,0 @@ -from dataclasses import dataclass -from datetime import datetime -import secrets - - -@dataclass -class Note: - title: str - body: str - - def to_file_content(self) -> str: - return f"{self.title}\n\n{self.body}" - - @classmethod - def from_file_content(cls, content: str) -> "Note": - parts = content.split("\n\n", 1) - title = parts[0] - body = parts[1] if len(parts) > 1 else "" - return cls(title=title, body=body) - - def generate_filename(self, order: int) -> str: - ts = datetime.now().strftime("%Y%m%d-%H%M%S") - short_id = secrets.token_hex(3)[:5] - return f"{order:04d}-{ts}-{short_id}.txt" - - @staticmethod - def parse_order(filename: str) -> int: - return int(filename.split("-", 1)[0]) diff --git a/pocketbook/src/pocketbook/note_widget.py b/pocketbook/src/pocketbook/note_widget.py deleted file mode 100644 index 3467ad0..0000000 --- a/pocketbook/src/pocketbook/note_widget.py +++ /dev/null @@ -1,93 +0,0 @@ -from typing import Callable - -import gi -gi.require_version("Gtk", "4.0") -from gi.repository import Gtk - -from pocketbook.note import Note - - -class NoteRow(Gtk.Box): - """A single note row with browse/edit modes.""" - - def __init__( - self, - filename: str, - note: Note, - on_changed: Callable[[str, str, str], None], - on_deleted: Callable[[str], None], - ): - super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=4) - self.add_css_class("note-row") - self._filename = filename - self._on_changed = on_changed - self._on_deleted = on_deleted - - # Title row with delete button - title_row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=4) - - self._title_entry = Gtk.Entry() - self._title_entry.set_text(note.title) - self._title_entry.set_hexpand(True) - self._title_entry.add_css_class("note-title") - self._title_entry.set_placeholder_text("Title") - self._title_entry.set_can_focus(False) - self._title_entry.set_focusable(False) - self._title_entry.connect("changed", self._on_text_changed) - self._title_entry.connect("notify::has-focus", self._on_title_focus_changed) - title_row.append(self._title_entry) - - delete_btn = Gtk.Button(label="") - delete_btn.add_css_class("delete-button") - delete_btn.set_focusable(False) - delete_btn.connect("clicked", self._on_delete_clicked) - title_row.append(delete_btn) - - self.append(title_row) - - # Body text view - self._body_buffer = Gtk.TextBuffer() - self._body_buffer.set_text(note.body) - self._body_buffer.connect("changed", self._on_text_changed) - - self._body_view = Gtk.TextView(buffer=self._body_buffer) - self._body_view.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) - self._body_view.add_css_class("note-body") - self._body_view.set_vexpand(False) - self._body_view.set_can_focus(False) - self._body_view.set_focusable(False) - self.append(self._body_view) - - # NoteRow itself is focusable for browse mode - self.set_focusable(True) - - def focus_title(self): - """Enter edit mode — focus and select title text.""" - self._title_entry.set_can_focus(True) - self._title_entry.set_focusable(True) - self._body_view.set_can_focus(True) - self._body_view.set_focusable(True) - self._title_entry.grab_focus() - self._title_entry.select_region(0, -1) - - def exit_edit_mode(self): - """Return to browse mode — defocus text fields.""" - self._title_entry.set_can_focus(False) - self._title_entry.set_focusable(False) - self._body_view.set_can_focus(False) - self._body_view.set_focusable(False) - self.grab_focus() - - def _on_title_focus_changed(self, entry, _pspec): - if not entry.has_focus(): - entry.select_region(0, 0) - - def _on_text_changed(self, _widget): - title = self._title_entry.get_text() - start = self._body_buffer.get_start_iter() - end = self._body_buffer.get_end_iter() - body = self._body_buffer.get_text(start, end, False) - self._on_changed(self._filename, title, body) - - def _on_delete_clicked(self, _button): - self._on_deleted(self._filename) diff --git a/pocketbook/src/pocketbook/panel.py b/pocketbook/src/pocketbook/panel.py deleted file mode 100644 index e0a708c..0000000 --- a/pocketbook/src/pocketbook/panel.py +++ /dev/null @@ -1,97 +0,0 @@ -from pocketbook.store import NoteStore -from pocketbook.note import Note - -import gi -gi.require_version("Gtk", "4.0") -from gi.repository import Gtk - - -class PanelController: - """Testable controller logic for the notes panel.""" - - def __init__(self, store: NoteStore): - self.store = store - - def add_note(self) -> str: - return self.store.create("New Note", "") - - def delete_note(self, filename: str): - self.store.delete(filename) - - def update_note(self, filename: str, title: str, body: str): - self.store.update(filename, title, body) - - def get_notes(self) -> list[tuple[str, Note]]: - return self.store.list_notes() - - -class PanelWidget(Gtk.Box): - """Main panel widget — scrollable list of notes with add button.""" - - def __init__(self, controller: PanelController): - super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=0) - self.controller = controller - self.add_css_class("panel") - - # Header with title and add button - header = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8) - header.add_css_class("panel-header") - - title_label = Gtk.Label(label="Pocketbook") - title_label.add_css_class("panel-title") - title_label.set_hexpand(True) - title_label.set_halign(Gtk.Align.START) - header.append(title_label) - - add_btn = Gtk.Button(label="+") - add_btn.add_css_class("add-button") - add_btn.set_focusable(False) - add_btn.connect("clicked", self._on_add_clicked) - header.append(add_btn) - - self.append(header) - - # Scrollable note list - self._scroll = Gtk.ScrolledWindow() - self._scroll.set_vexpand(True) - self._scroll.set_propagate_natural_height(True) - self._scroll.set_max_content_height(550) - self._scroll.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) - - self._list_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4) - self._list_box.add_css_class("note-list") - self._scroll.set_child(self._list_box) - self.append(self._scroll) - - self._refresh() - - def _refresh(self): - # Clear existing children - child = self._list_box.get_first_child() - while child: - next_child = child.get_next_sibling() - self._list_box.remove(child) - child = next_child - - from pocketbook.note_widget import NoteRow - for filename, note in self.controller.get_notes(): - row = NoteRow(filename, note, self._on_note_changed, self._on_note_deleted) - self._list_box.append(row) - - def add_note(self): - self.controller.add_note() - self._refresh() - # Focus the title entry of the newly added note (last child) - last = self._list_box.get_last_child() - if last: - last.focus_title() - - def _on_add_clicked(self, _button): - self.add_note() - - def _on_note_changed(self, filename: str, title: str, body: str): - self.controller.update_note(filename, title, body) - - def _on_note_deleted(self, filename: str): - self.controller.delete_note(filename) - self._refresh() diff --git a/pocketbook/src/pocketbook/store.py b/pocketbook/src/pocketbook/store.py deleted file mode 100644 index 83d3b74..0000000 --- a/pocketbook/src/pocketbook/store.py +++ /dev/null @@ -1,73 +0,0 @@ -from pathlib import Path -import re -from pocketbook.note import Note - -FILENAME_RE = re.compile(r"^\d{4}-.+\.txt$") - - -class NoteStore: - def __init__(self, directory: Path): - self.directory = directory - - def _ensure_dir(self): - self.directory.mkdir(parents=True, exist_ok=True) - - def _valid_note_files(self) -> list[tuple[str, Path]]: - """Return sorted list of (filename, path) for valid note files.""" - if not self.directory.exists(): - return [] - files = [] - for p in self.directory.iterdir(): - if p.is_file() and FILENAME_RE.match(p.name): - try: - Note.parse_order(p.name) - files.append((p.name, p)) - except (ValueError, IndexError): - continue - files.sort(key=lambda x: Note.parse_order(x[0])) - return files - - def _next_order(self) -> int: - files = self._valid_note_files() - if not files: - return 1 - return Note.parse_order(files[-1][0]) + 1 - - def create(self, title: str, body: str) -> str: - self._ensure_dir() - note = Note(title=title, body=body) - order = self._next_order() - filename = note.generate_filename(order) - (self.directory / filename).write_text(note.to_file_content()) - return filename - - def list_notes(self) -> list[tuple[str, Note]]: - """Return list of (filename, Note) sorted by order.""" - result = [] - for fname, path in self._valid_note_files(): - content = path.read_text() - note = Note.from_file_content(content) - result.append((fname, note)) - return result - - def update(self, filename: str, title: str, body: str): - path = self.directory / filename - if not path.exists(): - raise FileNotFoundError(f"Note not found: {filename}") - note = Note(title=title, body=body) - path.write_text(note.to_file_content()) - - def delete(self, filename: str): - path = self.directory / filename - if not path.exists(): - raise FileNotFoundError(f"Note not found: {filename}") - path.unlink() - - def reorder(self): - """Renumber all note files sequentially starting from 1.""" - files = self._valid_note_files() - for i, (fname, path) in enumerate(files, start=1): - current_order = Note.parse_order(fname) - if current_order != i: - new_name = f"{i:04d}{fname[4:]}" - path.rename(self.directory / new_name) diff --git a/pocketbook/src/pocketbook/style.css b/pocketbook/src/pocketbook/style.css deleted file mode 100644 index 92df265..0000000 --- a/pocketbook/src/pocketbook/style.css +++ /dev/null @@ -1,102 +0,0 @@ -/* Pocketbook — Dupre theme */ -/* Colors from dupre-palette.org */ - -window { - background-color: rgba(21, 19, 17, 0.95); - border: 2px solid #d7af5f; - border-radius: 16px; - font-family: "BerkeleyMono Nerd Font", "Berkeley Mono", monospace; - font-size: 14px; - color: #969385; -} - -.panel { - padding: 12px; -} - -.panel-header { - padding: 4px 4px 8px 4px; - margin-bottom: 4px; - border-bottom: 1px solid #474544; -} - -.panel-title { - font-size: 16px; - font-weight: bold; - color: #d7af5f; -} - -.add-button { - background-color: transparent; - color: #d7af5f; - border: 1px solid #d7af5f; - border-radius: 8px; - padding: 2px 10px; - font-size: 18px; - font-weight: bold; - min-width: 32px; - min-height: 32px; -} - -.add-button:hover { - background-color: #d7af5f; - color: #151311; -} - -.note-list { - padding: 4px 0; -} - -.note-row { - background-color: rgba(71, 69, 68, 0.3); - border-radius: 8px; - padding: 8px; - margin: 4px 0; - border: 1px solid transparent; -} - -.note-row:focus { - border: 1px solid #d7af5f; -} - -.note-title { - background-color: transparent; - border: none; - color: #b2c3cc; - font-weight: bold; - font-size: 14px; - padding: 4px; -} - -.note-title:focus { - outline: none; - border-bottom: 1px solid #d7af5f; -} - -.note-body { - background-color: transparent; - color: #d0cbc0; - font-size: 13px; - padding: 4px; - border-radius: 4px; -} - -.note-body:focus { - background-color: rgba(71, 69, 68, 0.4); -} - -.delete-button { - background-color: transparent; - color: #969385; - border: none; - border-radius: 8px; - padding: 2px 8px; - font-size: 16px; - min-width: 28px; - min-height: 28px; -} - -.delete-button:hover { - color: #d47c59; - background-color: rgba(212, 124, 89, 0.2); -} diff --git a/pocketbook/tests/__init__.py b/pocketbook/tests/__init__.py deleted file mode 100644 index e69de29..0000000 --- a/pocketbook/tests/__init__.py +++ /dev/null diff --git a/pocketbook/tests/conftest.py b/pocketbook/tests/conftest.py deleted file mode 100644 index db04f88..0000000 --- a/pocketbook/tests/conftest.py +++ /dev/null @@ -1,17 +0,0 @@ -import pytest - - -@pytest.fixture -def notes_dir(tmp_path): - """Temporary directory for note storage.""" - d = tmp_path / "quicknotes" - d.mkdir() - return d - - -@pytest.fixture -def sample_note_file(notes_dir): - """A sample note file on disk.""" - path = notes_dir / "0001-20260101-120000-abc12.txt" - path.write_text("Shopping List\n\nMilk\nEggs\nBread\n") - return path diff --git a/pocketbook/tests/test_app_toggle.py b/pocketbook/tests/test_app_toggle.py deleted file mode 100644 index cb5ab89..0000000 --- a/pocketbook/tests/test_app_toggle.py +++ /dev/null @@ -1,96 +0,0 @@ -from pocketbook.app import ToggleStateMachine, EscapeStateMachine, navigate - - -class TestToggleStateMachine: - def test_initial_state_visible(self): - sm = ToggleStateMachine(start_hidden=False) - assert sm.visible is True - - def test_initial_state_hidden(self): - sm = ToggleStateMachine(start_hidden=True) - assert sm.visible is False - - def test_toggle_alternates(self): - sm = ToggleStateMachine(start_hidden=False) - assert sm.visible is True - sm.toggle() - assert sm.visible is False - sm.toggle() - assert sm.visible is True - - def test_toggle_from_hidden(self): - sm = ToggleStateMachine(start_hidden=True) - assert sm.visible is False - sm.toggle() - assert sm.visible is True - sm.toggle() - assert sm.visible is False - - -class TestEscapeStateMachine: - def test_escape_while_editing_returns_exit_edit(self): - esc = EscapeStateMachine() - toggle = ToggleStateMachine(start_hidden=False) - action = esc.escape(is_editing=True, toggle=toggle) - assert action == "exit_edit" - # Toggle state should not change - assert toggle.visible is True - - def test_escape_while_browsing_returns_hide(self): - esc = EscapeStateMachine() - toggle = ToggleStateMachine(start_hidden=False) - action = esc.escape(is_editing=False, toggle=toggle) - assert action == "hide" - assert toggle.visible is False - - def test_escape_edit_then_browse_hides(self): - """Simulates: editing → Escape (exit edit) → Escape (hide).""" - esc = EscapeStateMachine() - toggle = ToggleStateMachine(start_hidden=False) - - # First escape: exit edit mode - action1 = esc.escape(is_editing=True, toggle=toggle) - assert action1 == "exit_edit" - assert toggle.visible is True - - # Second escape: now in browse mode, hide panel - action2 = esc.escape(is_editing=False, toggle=toggle) - assert action2 == "hide" - assert toggle.visible is False - - def test_escape_hide_does_not_change_when_already_hidden(self): - esc = EscapeStateMachine() - toggle = ToggleStateMachine(start_hidden=True) - assert toggle.visible is False - action = esc.escape(is_editing=False, toggle=toggle) - assert action == "hide" - # Toggled again — now visible (edge case if called when hidden) - assert toggle.visible is True - - -class TestNavigate: - def test_no_notes(self): - assert navigate(None, 0, 1) is None - assert navigate(None, 0, -1) is None - - def test_no_focus_next_goes_to_first(self): - assert navigate(None, 3, 1) == 0 - - def test_no_focus_prev_goes_to_last(self): - assert navigate(None, 3, -1) == 2 - - def test_next_from_middle(self): - assert navigate(1, 3, 1) == 2 - - def test_prev_from_middle(self): - assert navigate(1, 3, -1) == 0 - - def test_next_clamps_at_end(self): - assert navigate(2, 3, 1) == 2 - - def test_prev_clamps_at_start(self): - assert navigate(0, 3, -1) == 0 - - def test_single_note(self): - assert navigate(0, 1, 1) == 0 - assert navigate(0, 1, -1) == 0 diff --git a/pocketbook/tests/test_note.py b/pocketbook/tests/test_note.py deleted file mode 100644 index 539451a..0000000 --- a/pocketbook/tests/test_note.py +++ /dev/null @@ -1,69 +0,0 @@ -from pocketbook.note import Note - - -class TestNoteSerialisation: - def test_round_trip(self): - note = Note(title="Shopping", body="Milk\nEggs\nBread") - content = note.to_file_content() - restored = Note.from_file_content(content) - assert restored.title == note.title - assert restored.body == note.body - - def test_empty_body(self): - note = Note(title="Empty", body="") - content = note.to_file_content() - restored = Note.from_file_content(content) - assert restored.title == "Empty" - assert restored.body == "" - - def test_empty_title(self): - note = Note(title="", body="some body") - content = note.to_file_content() - restored = Note.from_file_content(content) - assert restored.title == "" - assert restored.body == "some body" - - def test_unicode(self): - note = Note(title="日本語タイトル", body="Ünïcödé bödý 🎉") - content = note.to_file_content() - restored = Note.from_file_content(content) - assert restored.title == "日本語タイトル" - assert restored.body == "Ünïcödé bödý 🎉" - - def test_multiline_body(self): - body = "Line 1\nLine 2\n\nLine 4\n" - note = Note(title="Multi", body=body) - content = note.to_file_content() - restored = Note.from_file_content(content) - assert restored.body == body - - def test_file_content_format(self): - """Title on line 1, blank line, then body.""" - note = Note(title="Title", body="Body text") - content = note.to_file_content() - assert content == "Title\n\nBody text" - - def test_from_file_content_no_blank_line(self): - """Gracefully handle files without a blank separator.""" - restored = Note.from_file_content("JustTitle") - assert restored.title == "JustTitle" - assert restored.body == "" - - -class TestNoteFilename: - def test_generate_filename(self): - note = Note(title="Test", body="") - filename = note.generate_filename(order=1) - assert filename.startswith("0001-") - assert filename.endswith(".txt") - # Format: 0001-YYYYMMDD-HHMMSS-shortid.txt - parts = filename.split("-") - assert len(parts) == 4 - assert len(parts[0]) == 4 # order - assert len(parts[1]) == 8 # date - # parts[2] = HHMMSS + shortid.txt combined via split on - - # Actually: 0001-20260225-143012-abc12.txt has 4 parts - - def test_parse_order_from_filename(self): - assert Note.parse_order("0005-20260101-120000-abc12.txt") == 5 - assert Note.parse_order("0001-20260101-120000-xyz99.txt") == 1 diff --git a/pocketbook/tests/test_panel.py b/pocketbook/tests/test_panel.py deleted file mode 100644 index 92f8648..0000000 --- a/pocketbook/tests/test_panel.py +++ /dev/null @@ -1,40 +0,0 @@ -from unittest.mock import MagicMock -from pocketbook.note import Note - - -class TestPanelController: - """Test panel controller logic with a mocked store.""" - - def _make_controller(self): - from pocketbook.panel import PanelController - store = MagicMock() - controller = PanelController(store) - return controller, store - - def test_add_note_calls_store_create(self): - controller, store = self._make_controller() - store.create.return_value = "0001-20260101-120000-abc12.txt" - controller.add_note() - store.create.assert_called_once_with("New Note", "") - - def test_delete_note_calls_store_delete(self): - controller, store = self._make_controller() - controller.delete_note("0001-20260101-120000-abc12.txt") - store.delete.assert_called_once_with("0001-20260101-120000-abc12.txt") - - def test_update_note_calls_store_update(self): - controller, store = self._make_controller() - controller.update_note("0001-20260101-120000-abc12.txt", "New Title", "New Body") - store.update.assert_called_once_with( - "0001-20260101-120000-abc12.txt", "New Title", "New Body" - ) - - def test_get_notes_calls_store_list(self): - controller, store = self._make_controller() - store.list_notes.return_value = [ - ("0001-20260101-120000-abc12.txt", Note(title="A", body="a")), - ] - notes = controller.get_notes() - store.list_notes.assert_called_once() - assert len(notes) == 1 - assert notes[0][1].title == "A" diff --git a/pocketbook/tests/test_store.py b/pocketbook/tests/test_store.py deleted file mode 100644 index fab5bd6..0000000 --- a/pocketbook/tests/test_store.py +++ /dev/null @@ -1,103 +0,0 @@ -import pytest -from pocketbook.store import NoteStore -from pocketbook.note import Note - - -class TestNoteStoreCreate: - def test_create_note(self, notes_dir): - store = NoteStore(notes_dir) - filename = store.create("My Title", "My Body") - assert (notes_dir / filename).exists() - content = (notes_dir / filename).read_text() - assert content == "My Title\n\nMy Body" - - def test_create_assigns_incrementing_order(self, notes_dir): - store = NoteStore(notes_dir) - f1 = store.create("First", "") - f2 = store.create("Second", "") - assert Note.parse_order(f1) == 1 - assert Note.parse_order(f2) == 2 - - def test_create_auto_creates_directory(self, tmp_path): - d = tmp_path / "nonexistent" / "pocketbook" - store = NoteStore(d) - filename = store.create("Test", "body") - assert d.exists() - assert (d / filename).exists() - - -class TestNoteStoreList: - def test_list_empty(self, notes_dir): - # Remove the sample file if any fixture created one - for f in notes_dir.iterdir(): - f.unlink() - store = NoteStore(notes_dir) - assert store.list_notes() == [] - - def test_list_returns_sorted_by_order(self, notes_dir): - (notes_dir / "0002-20260101-120000-abc12.txt").write_text("B\n\nbody b") - (notes_dir / "0001-20260101-120000-def34.txt").write_text("A\n\nbody a") - (notes_dir / "0003-20260101-120000-ghi56.txt").write_text("C\n\nbody c") - store = NoteStore(notes_dir) - notes = store.list_notes() - assert len(notes) == 3 - assert notes[0][1].title == "A" - assert notes[1][1].title == "B" - assert notes[2][1].title == "C" - - def test_list_skips_non_txt_files(self, notes_dir): - (notes_dir / "0001-20260101-120000-abc12.txt").write_text("Note\n\nbody") - (notes_dir / "README.md").write_text("not a note") - store = NoteStore(notes_dir) - assert len(store.list_notes()) == 1 - - def test_list_skips_corrupted_filenames(self, notes_dir): - (notes_dir / "0001-20260101-120000-abc12.txt").write_text("Good\n\nbody") - (notes_dir / "bad-name.txt").write_text("Bad\n\nbody") - store = NoteStore(notes_dir) - notes = store.list_notes() - assert len(notes) == 1 - assert notes[0][1].title == "Good" - - -class TestNoteStoreUpdate: - def test_update_note(self, notes_dir): - fname = "0001-20260101-120000-abc12.txt" - (notes_dir / fname).write_text("Old Title\n\nOld body") - store = NoteStore(notes_dir) - store.update(fname, "New Title", "New body") - content = (notes_dir / fname).read_text() - assert content == "New Title\n\nNew body" - - def test_update_nonexistent_raises(self, notes_dir): - store = NoteStore(notes_dir) - with pytest.raises(FileNotFoundError): - store.update("0099-20260101-120000-nope0.txt", "T", "B") - - -class TestNoteStoreDelete: - def test_delete_note(self, notes_dir): - fname = "0001-20260101-120000-abc12.txt" - (notes_dir / fname).write_text("Delete me\n\nbody") - store = NoteStore(notes_dir) - store.delete(fname) - assert not (notes_dir / fname).exists() - - def test_delete_nonexistent_raises(self, notes_dir): - store = NoteStore(notes_dir) - with pytest.raises(FileNotFoundError): - store.delete("0099-20260101-120000-nope0.txt") - - -class TestNoteStoreReorder: - def test_reorder_renumbers_files(self, notes_dir): - # Create files with gaps in ordering - (notes_dir / "0001-20260101-120000-aaa11.txt").write_text("A\n\na") - (notes_dir / "0005-20260101-120000-bbb22.txt").write_text("B\n\nb") - (notes_dir / "0010-20260101-120000-ccc33.txt").write_text("C\n\nc") - store = NoteStore(notes_dir) - store.reorder() - notes = store.list_notes() - assert Note.parse_order(notes[0][0]) == 1 - assert Note.parse_order(notes[1][0]) == 2 - assert Note.parse_order(notes[2][0]) == 3 |
