build_css() now starts from bread_theme::stylesheet(palette) and appends only breadpad/breadman-specific components. This unifies fonts, palette, and generic widgets with the rest of the ecosystem and fixes the colour mapping (overlay is now color7, matching every other app, not color0). Bump bread-theme to v0.2.6. |
||
|---|---|---|
| .forgejo/workflows | ||
| .github/workflows | ||
| breadman | ||
| breadpad | ||
| breadpad-shared | ||
| breadpad-test | ||
| packaging/arch | ||
| .gitignore | ||
| bakery.toml | ||
| breadpad.example.toml | ||
| Cargo.lock | ||
| Cargo.toml | ||
| LICENSE | ||
| README.md | ||
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:
- 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.
- 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.
- 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 SIGHUPreloads 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.
{"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.
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. Setmodel.ort_dylib_pathinbreadpad.toml, or setORT_DYLIB_PATHin your environment. Without a library, Tier 2 is disabled; Tier 1 + 3 still work. - Tier 3 only (optional): Ollama running locally with your chosen model pulled (
ollama pull llama3.2:3b). Tier 3 is silently skipped if Ollama is not running.
Installation
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:
sudo pacman -S gtk4 gtk4-layer-shell
Configuration
On first run, breadpad writes ~/.config/breadpad/breadpad.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)
# 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)
# 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 <id>
# 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-<id>.timer
breadpad-reminder-<id>.service
The service unit runs breadpad fire <id>, 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:
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.
# 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 <path>.
[
{
"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-sensitiveexpected_rrule— matched as substring of the actual RRULE string- Any
nullfield 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
# 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
# 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:
[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
# 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 <note-id>
Event format
Each note is pushed as a VEVENT with:
UID—<note-id>@breadpad(stable and deterministic)SUMMARY— note bodyDTSTART/DTEND— scheduled time (or creation time for recurring notes without a fixed start)RRULE— recurrence rule if setDESCRIPTION—type=<note-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