# breadpad / breadman A quick-capture scratchpad and structured note viewer for Hyprland / Wayland, with AI-powered classification, reminders, recurrence, and snooze. Two entry points, one binary, one shared workspace: | Binary | Purpose | |--------|---------| | `breadpad` | Layer-shell capture popup — type a note, press Enter, done | | `breadman` | Full note viewer and manager | --- ## Workspace layout ``` breadpad-shared shared types, storage, classification, scheduler breadpad GTK4 layer-shell capture popup breadman GTK4 note viewer / manager ``` --- ## Features ### Capture (`breadpad`) - Layer-shell popup, centered, keyboard-exclusive — appears instantly on your keybind - Single text field; press **Enter** or click **✓** to save, **Escape** to dismiss - Optional manual type override before saving (defaults to AI classification) - Timestamp and active Hyprland workspace recorded automatically ### Classification Every note passes through a three-tier pipeline at capture time: 1. **Rule-based parser** — always runs first; handles time extraction ("at 7pm", "in 30 minutes", "tomorrow morning", "next Friday"), recurrence ("every Sunday at 9pm", "every weekday morning"), and strong type signals ("?" → question, "idea:" prefix → idea, action verbs → todo). High-confidence results skip the remaining tiers entirely. 2. **Small local ONNX model** — runs when Tier 1 can't confidently assign a type. Responsible for type classification only; Tier 1's extracted time, recurrence rule, and cleaned body are always preserved. 3. **Large local model via Ollama** — runs only when Tier 2 confidence falls below a configurable threshold. Communicates with a locally running Ollama instance over HTTP. If Ollama is unreachable, the Tier 2 result is used. No cloud APIs are involved. Manual override always available — the AI-assigned type is shown as a chip you can tap to change before saving. ### Note types (built-in) | Type | Example | |------|---------| | `todo` | "buy milk on the way home" | | `reminder` | "pack calculator in bag at 7pm" | | `idea` | "what if breadman had a calendar view" | | `note` | "meeting went well, follow up Friday" | | `question` | "why does nmcli drop on suspend?" | User-defined tags can be added freely on top of the built-in types. ### Reminders, recurrence, and snooze - **One-off reminders** — natural language time ("at 7pm", "in 30 minutes", "tomorrow morning") parsed at classification time; scheduled via a systemd user timer - **Recurring reminders** — "every Sunday at 9pm", "every weekday morning" — stored as an iCal-compatible RRULE and re-scheduled on each trigger - **Snooze** — notification popup includes snooze actions: 15 min / 1 hour / tomorrow morning / custom; snoozing reschedules the timer without touching the original note - **Missed reminders** — if the system was off or suspended at the scheduled time, the reminder fires on next login ### Viewer (`breadman`) - Sidebar with one entry per type + "All" and "Upcoming" - Each note card shows: body, type chip, timestamp, workspace tag, recurrence badge if set - **Upcoming** view: chronological list of all pending reminders and todos with times - Inline editing — click any card to edit body, type, time, or recurrence - Mark todo/reminder as done; done items move to an archive accessible via a toggle - Search across all notes (full-text, instant) - Sort: newest first (default) ### Theming - Reads `~/.cache/wal/colors.json` (pywal) on startup — matches the rest of the bread ecosystem - Falls back to Catppuccin Mocha - CSS override: `~/.config/breadpad/style.css` - `SIGHUP` reloads theme at runtime --- ## Storage Notes are stored as JSONL at `~/.local/share/breadpad/notes.jsonl` — one JSON object per line, human-readable, easy to back up or script against. ```jsonl {"id":"a1b2c3","body":"Pack calculator in bag","type":"reminder","time":"2026-05-25T19:00:00Z","rrule":null,"done":false,"workspace":"1","created":"2026-05-25T18:45:00Z","snoozed_until":null,"completed":null,"tags":[],"caldav_uid":null} {"id":"d4e5f6","body":"Look into relm4 reactive patterns","type":"idea","time":null,"rrule":null,"done":false,"workspace":"2","created":"2026-05-25T14:10:00Z","snoozed_until":null,"completed":null,"tags":[],"caldav_uid":null} ``` Completed notes are never deleted — they gain `"done": true` and a `"completed"` timestamp. A separate `~/.local/share/breadpad/archive.jsonl` is written periodically for notes older than 30 days. --- ## AI classification ### Three-tier pipeline #### Tier 1 — Rule-based parser Always runs. Handles: - **Time extraction**: "at 7pm", "in 30 minutes", "tomorrow morning", "next Friday at 9am" - **Recurrence**: "every Sunday at 9pm", "every weekday morning" → stored as RRULE - **Type signals**: leading "?" or "why/how/what" → `question`; "idea:" prefix or "what if" → `idea`; action verbs → `todo`; time present → `reminder` Returns a calibrated confidence. If ≥ 0.82, Tiers 2 and 3 are skipped. #### Tier 2 — Small local ONNX model Runs when Tier 1 confidence is below threshold. Responsible for **type classification only** — Tier 1's extracted time, recurrence rule, and cleaned body are always preserved. Invoked via `ort` (ONNX Runtime Rust bindings, `load-dynamic`) on the CPU. Requires an external `libonnxruntime.so`; set `model.ort_dylib_path` in `breadpad.toml` or let breadpad auto-discover it via `ORT_DYLIB_PATH`. #### Tier 3 — Large local model via Ollama Runs only when Tier 2 confidence falls below `model.ollama.confidence_threshold` (default 0.6). Sends a structured prompt to a locally running Ollama instance over HTTP and parses the JSON response for `type`, `body`, and `confidence`. The Ollama model runs on the iGPU via Ollama's own backend — breadpad does not manage GPU allocation for this tier. If Ollama is unreachable or returns an invalid response, breadpad logs a warning and uses the Tier 2 result. No cloud APIs are used anywhere. ### Model location (Tier 2) ``` ~/.local/share/breadpad/model/classifier.onnx ~/.local/share/breadpad/model/tokenizer.json ``` breadpad ships without a bundled model. Drop a compatible ONNX classifier and `tokenizer.json` at those paths, then configure `model.ort_dylib_path` to point at your ONNX Runtime library. ```bash breadpad model-info # shows active EP and model path ``` --- ## Requirements - Linux with a running Hyprland compositor - GTK4 (≥ 4.12) + `gtk4-layer-shell` - D-Bus session bus (for notifications) - systemd user session (for timer-backed reminders) - Rust 1.80+ - **Tier 2 (ONNX classifier):** An external `libonnxruntime.so`. Set `model.ort_dylib_path` in `breadpad.toml`, or set `ORT_DYLIB_PATH` in your environment. Without a library, Tier 2 is disabled; Tier 1 + 3 still work. - **Tier 3 only (optional):** [Ollama](https://ollama.com) running locally with your chosen model pulled (`ollama pull llama3.2:3b`). Tier 3 is silently skipped if Ollama is not running. --- ## Installation ```bash git clone https://github.com/breadway/breadpad cd breadpad cargo build --release cp target/release/breadpad ~/.local/bin/ cp target/release/breadman ~/.local/bin/ # Place your ONNX classifier and tokenizer in the model directory mkdir -p ~/.local/share/breadpad/model # Then set model.ort_dylib_path in breadpad.toml to your libonnxruntime.so ``` On Arch Linux, install GTK4 dependencies first: ```bash sudo pacman -S gtk4 gtk4-layer-shell ``` --- ## Configuration On first run, breadpad writes `~/.config/breadpad/breadpad.toml`: ```toml [settings] default_type = "note" # fallback type if classification is skipped workspace_tag = true # tag notes with active Hyprland workspace snooze_options = ["15m", "1h", "tomorrow_morning"] # shown in notification actions archive_after_days = 30 [model] path = "~/.local/share/breadpad/model/classifier.onnx" tokenizer = "~/.local/share/breadpad/model/tokenizer.json" ort_dylib_path = "" # optional: explicit path to libonnxruntime.so; auto-discovered when empty [model.ollama] endpoint = "http://localhost:11434" model = "llama3.2:3b" # any model you have pulled in Ollama confidence_threshold = 0.6 # Tier 2 scores below this trigger Tier 3 enabled = true # set false to never call Ollama [reminders] default_morning = "08:00" # what "tomorrow morning" resolves to missed_grace_minutes = 60 # how long after boot to still fire a missed reminder ``` --- ## Usage ### breadpad (capture) ```bash # Open the capture popup (bind this to a key in hyprland.conf) breadpad # Open with a pre-selected type breadpad --type todo # Skip AI classification (save as plain note) breadpad --no-classify # Show model and storage status breadpad --status ``` Hyprland keybind: ``` bind = $mainMod, N, exec, breadpad ``` ### breadman (viewer) ```bash # Open the note viewer breadman # Open directly to a specific type view breadman --view todo breadman --view upcoming # Mark a note done by ID (scriptable) breadman done # List upcoming reminders in the terminal breadman upcoming --plain ``` --- ## Scheduler breadpad manages reminders via systemd user timers. Each scheduled note gets a transient timer unit: ``` breadpad-reminder-.timer breadpad-reminder-.service ``` The service unit runs `breadpad fire `, which sends a `notify-send` notification with snooze actions. Snoozing writes the new time back to the note and creates a replacement timer. Recurring notes create the next timer immediately on fire. You can inspect pending timers: ```bash systemctl --user list-timers 'breadpad-*' ``` --- ## Testing `breadpad-test` is a CLI test harness for the classification pipeline. It runs a JSON corpus of labelled inputs through any tier of the pipeline and reports pass/fail. ```bash # Run Tier 1 (rule-based only) — fast, no model needed breadpad-test run # See only failing cases breadpad-test run --format failures # Run Tier 2 (+ ONNX model) breadpad-test run --tier 2 # Run full pipeline including Ollama breadpad-test run --tier all # Machine-readable output breadpad-test run --format json ``` ### Corpus format Default path: `breadpad-test/corpus.json`. Override with `--corpus `. ```json [ { "input": "pack my calculator in my bag tonight", "expected_type": "todo", "expected_time": null, "expected_body": "pack my calculator in my bag", "expected_rrule": null, "notes": "no time specified, should not infer one" } ] ``` - `expected_time` — `HH:MM`; date component is ignored so tests are never date-sensitive - `expected_rrule` — matched as substring of the actual RRULE string - Any `null` field is skipped — only non-null fields are asserted ### Tier modes | `--tier` | What runs | |----------|-----------| | `1` (default) | Tier 1 rule-based parser only — no model required | | `2` | Tiers 1 + 2 (ONNX classifier) | | `3` / `all` | Full pipeline including Tier 3 Ollama | ### Corpus management ```bash # Interactively add an entry breadpad-test add # Show entry #5 and the pipeline's actual output breadpad-test show 5 # Open corpus file in $EDITOR at entry #5 breadpad-test edit 5 ``` ### Typical tuning workflow ```bash # 1. See what Tier 1 gets wrong breadpad-test run --tier 1 --format failures # 2. Edit parser.rs, then rerun cargo build -p breadpad-shared && breadpad-test run --tier 1 --format failures # 3. Once Tier 1 is stable, audit Tier 2 regressions breadpad-test run --tier 2 --format failures ``` --- ## Nextcloud Calendar integration breadpad can push scheduled notes and recurring reminders to a CalDAV calendar (Nextcloud or any RFC 4791-compliant server). No cloud APIs are used — everything goes directly to your own server over HTTPS. ### What gets pushed Notes with a scheduled time (`time` field) or recurrence rule (`rrule`) are pushed as VEVENT entries when saved. Notes without a time are not pushed. Deleting a note also deletes the corresponding calendar event. ### Configuration Add a `[calendar]` section to `~/.config/breadpad/breadpad.toml`: ```toml [calendar] enabled = true url = "https://nextcloud.example.com/remote.php/dav/calendars/you/breadpad/" username = "you" password = "app-password-here" # use a Nextcloud app password, not your login password ``` The calendar must already exist on the server. Create it in the Nextcloud Calendar app before enabling this integration. ### CLI commands ```bash # Verify the CalDAV connection and credentials breadpad calendar test # List CalDAV UIDs for all scheduled notes (queries the server if enabled, local store if not) breadpad calendar list-uid # Show the CalDAV UID for a specific note by its local ID breadpad calendar list-uid ``` ### Event format Each note is pushed as a VEVENT with: - `UID` — `@breadpad` (stable and deterministic) - `SUMMARY` — note body - `DTSTART` / `DTEND` — scheduled time (or creation time for recurring notes without a fixed start) - `RRULE` — recurrence rule if set - `DESCRIPTION` — `type=` ### Security note Store your CalDAV password using a Nextcloud app password rather than your account password. App passwords can be revoked individually from the Nextcloud security settings. --- ## Module layout | Crate / module | Responsibility | |----------------|---------------| | `breadpad-shared/src/types.rs` | `Note`, `NoteType`, `RecurrenceRule`, `SnoozeState` | | `breadpad-shared/src/store.rs` | JSONL read/write, atomic saves, archive rotation | | `breadpad-shared/src/classifier.rs` | Three-tier pipeline orchestration (Tier 1 → 2 → 3) | | `breadpad-shared/src/parser.rs` | Tier 1: rule-based time/recurrence/type parsing | | `breadpad-shared/src/ai.rs` | Tier 3: Ollama HTTP client, prompt construction, response parsing | | `breadpad-shared/src/calendar.rs` | CalDAV client: push, delete, list events; iCal VEVENT builder | | `breadpad-shared/src/scheduler.rs` | systemd timer creation, snooze, recurrence next-occurrence | | `breadpad/src/main.rs` | GTK4 layer-shell popup, text field, type chip selector | | `breadman/src/main.rs` | GTK4 app entry, sidebar, note list, search | | `breadman/src/views/` | `upcoming.rs`, `archive.rs`, per-type list views | | `breadman/src/editor.rs` | Inline note editor popover | --- ## License MIT