Config file, pinning, and history-DB hardening #1

Merged
Breadway merged 4 commits from feature/config-file into main 2026-08-31 18:14:16 +08:00
Owner

Cut from main (was ~1450 lines uncommitted on main with no branch).

What

  • User config — optional $XDG_CONFIG_HOME/breadclip/config.toml, every key defaulted + clamped, unparseable file backed up once. Keys: retention.text/images, panel.width, capture.primary. Missing file = defaults, no error. Example in contrib/config.toml.example.
  • Pinning — new pinned column (in-place migration), Ctrl+P in the popup, bread.command.clip.pin verb + bread.clip.pinned/.pin.failed events. Pinned rows are exempt from trim and sort first.
  • Primary selection — opt-in capture.primary adds a second wl-paste --watch --primary loop; those entries get a primary badge.
  • DB hardening — WAL + busy timeout (no more dropped captures on SQLITE_BUSY), HistoryError so image-write failures surface instead of leaving broken rows, 0600-from-first-syscall image writes, 0700 dirs, orphan-image GC, deterministic id DESC tiebreak.
  • Capture rework — keys off wl-paste CLIPBOARD_STATE (sensitive/nil/clear/data); sensitive check and content read happen in one event. Fallback path wrapped in timeout -k 2 5.
  • flock singleton via bread_utils::singleton — no stale pid file.
  • JPEG stored as .jpg/image/jpeg, re-copied as the same type.
  • ignore_rules — conservative best-effort skip of copies that look like secrets (private keys, password: lines, labelled OTPs, Luhn-valid cards, token prefixes). Convenience, not a security boundary.
  • content_kind: single-line prose containing a keyword no longer misclassifies as code.
  • breadclipd.service sets PATH so the user service actually finds wl-paste.
  • check.yml also runs on main; release.yml uses ${GITHUB_REPOSITORY}.

Divergence from the original plan

The original spec framed ignore_rules as user-configurable match rules (MIME/app/regex) documented in EVENTS.md. As built it is a fixed secret-detection heuristic with no config surface — simpler, and documented as such in README + module docs. No [ignore] config section exists.

The popup panel positioning moved to after realisation so it can measure the panel's natural height — behavioural change, flagged here.

Tests

cargo test --workspace — 42 pass (16 core + 26 daemon). cargo clippy --workspace --all-targets -- -D warnings clean. cargo build --workspace clean.

Manual smoke (not yet done)

  • copy text / image / JPEG / password-manager entry; confirm sensitive + ignore-rule matches never land
  • capture.primary = true, middle-click select, confirm primary badge
  • Ctrl+P pin/unpin; restart daemon, confirm pinned survives and config reload from disk
  • bad config file → .bak written, defaults used
Cut from `main` (was ~1450 lines uncommitted on `main` with no branch). ## What - **User config** — optional `$XDG_CONFIG_HOME/breadclip/config.toml`, every key defaulted + clamped, unparseable file backed up once. Keys: `retention.text`/`images`, `panel.width`, `capture.primary`. Missing file = defaults, no error. Example in `contrib/config.toml.example`. - **Pinning** — new `pinned` column (in-place migration), Ctrl+P in the popup, `bread.command.clip.pin` verb + `bread.clip.pinned`/`.pin.failed` events. Pinned rows are exempt from trim and sort first. - **Primary selection** — opt-in `capture.primary` adds a second `wl-paste --watch --primary` loop; those entries get a `primary` badge. - **DB hardening** — WAL + busy timeout (no more dropped captures on SQLITE_BUSY), `HistoryError` so image-write failures surface instead of leaving broken rows, 0600-from-first-syscall image writes, 0700 dirs, orphan-image GC, deterministic `id DESC` tiebreak. - **Capture rework** — keys off wl-paste `CLIPBOARD_STATE` (`sensitive`/`nil`/`clear`/`data`); sensitive check and content read happen in one event. Fallback path wrapped in `timeout -k 2 5`. - **flock singleton** via `bread_utils::singleton` — no stale pid file. - **JPEG** stored as `.jpg`/`image/jpeg`, re-copied as the same type. - **ignore_rules** — conservative best-effort skip of copies that look like secrets (private keys, `password:` lines, labelled OTPs, Luhn-valid cards, token prefixes). Convenience, not a security boundary. - content_kind: single-line prose containing a keyword no longer misclassifies as code. - `breadclipd.service` sets `PATH` so the user service actually finds `wl-paste`. - check.yml also runs on `main`; release.yml uses `${GITHUB_REPOSITORY}`. ## Divergence from the original plan The original spec framed `ignore_rules` as user-configurable match rules (MIME/app/regex) documented in EVENTS.md. As built it is a fixed secret-detection heuristic with no config surface — simpler, and documented as such in README + module docs. No `[ignore]` config section exists. The popup panel positioning moved to *after* realisation so it can measure the panel's natural height — behavioural change, flagged here. ## Tests `cargo test --workspace` — 42 pass (16 core + 26 daemon). `cargo clippy --workspace --all-targets -- -D warnings` clean. `cargo build --workspace` clean. ## Manual smoke (not yet done) - copy text / image / JPEG / password-manager entry; confirm sensitive + ignore-rule matches never land - `capture.primary = true`, middle-click select, confirm primary badge - Ctrl+P pin/unpin; restart daemon, confirm pinned survives and config reload from disk - bad config file → `.bak` written, defaults used
Breadway added 4 commits 2026-08-31 15:08:20 +08:00
Persistence layer changes, all backward-compatible with existing
databases via in-place column migration:

- WAL journal mode + a 5s busy timeout so overlapping `--capture-once`
  writers and the popup reader stop dropping captures on SQLITE_BUSY.
- `HistoryError` replaces bare `rusqlite::Error` so a filesystem failure
  while writing an image file surfaces instead of leaving a row that
  points at a file that was never written.
- Image files are created 0600 from the first syscall (O_CREAT|O_EXCL,
  mode 0600) — no world-readable window before a chmod. Data and images
  dirs are forced to 0700 on every open. `gc_orphaned_images` sweeps
  image files no row references (older than 1h, to spare in-flight
  writes).
- `pinned` and `is_primary` columns. Pinned rows are exempt from trim
  and sort first; `list_entries` breaks timestamp ties by `id DESC` so
  ordering (and which rows trim keeps) is deterministic within a second.
- `Retention` caps are now a field on `HistoryDb` (`open_with`), and
  `0` is a legal value ("keep no unpinned entries of this kind").

New `config` module: optional TOML at
`$XDG_CONFIG_HOME/breadclip/config.toml`, every key defaulted and
clamped, unparseable file backed up once (bread-utils tomlcfg
discipline). Keys: retention.text/images, panel.width, capture.primary.
- Capture now keys off wl-paste's `CLIPBOARD_STATE`: `sensitive`
  (password manager via `wl-copy --sensitive`, covers the old
  x-kde-passwordManagerHint case) and `nil`/`clear` persist nothing;
  `data` reads the content off stdin in the same event, so the
  sensitive check and the read can't straddle a clipboard change.
  A manual `--capture-once` with no CLIPBOARD_STATE falls back to
  querying wl-paste directly, now wrapped in `timeout -k 2 5` so a
  stalled offer can't block a capture process forever.
- Single-instance guard moves to `bread_utils::singleton` (flock) —
  kernel-atomic, auto-released on death, no stale pid file.
- `ignore_rules::is_sensitive`: conservative best-effort heuristics
  that skip copies which look like secrets even when unflagged —
  PEM/OpenSSH private key blocks, `password:`-style lines, labelled
  one-time codes, Luhn-valid card numbers, well-known token prefixes.
  A convenience, not a security boundary.
- `bread.command.clip.pin` verb (payload `{id, pin?}`, pin defaults
  true) → `set_pinned`, emits `bread.clip.pinned` / `.pin.failed`.
- Two independent `wl-paste --watch` loops (regular clipboard always
  on; primary selection when `capture.primary`), each restarting with
  2s→30s capped backoff and state-change-only journal logging.
- Image type is sniffed from magic bytes (PNG/JPEG) on the stdin path
  and requested by actual offered type on the fallback path — a JPEG
  is stored as `.jpg` / `image/jpeg`, not relabelled PNG.
- content_kind: a single line of prose that merely contains a keyword
  ("class is a concept") no longer classifies as code — it must look
  like a statement.
- Ctrl+P pins/unpins the selected row, re-reads history (pinned rows
  sort first), rebuilds the list and re-selects the toggled row.
- ★ badge on pinned rows, "primary" badge on primary-selection rows.
- Panel width comes from `config.panel_width`; positioning now runs
  after the panel is realised and measures its natural height instead
  of always assuming the worst-case estimate, so a short history gets
  a correctly-anchored short panel.
- Delete in the search box edits the query again instead of deleting
  the selected history row.
- Re-copy offers an image row as its stored MIME type (JPEG stays
  JPEG). History fetch is a flat cap since pinned rows can exceed
  retention.
breadclip: document pin verb, config file, and ignore rules
All checks were successful
check / check (push) Successful in 2m9s
6db4a96026
- EVENTS.md: `bread.command.clip.pin` and `bread.clip.pinned` /
  `.pin.failed` are now implemented; `select` stays explicitly not
  implemented with the reason. AGENTS.md follows.
- README: Configuration section, Ctrl+P bind, updated privacy notes
  (CLIPBOARD_STATE + ignore rules), JPEG image entries, primary badge.
- check.yml also runs on pushes to `main` — a push to main triggers a
  dev-track release build, so it should be linted/tested first.
- release.yml uses `${GITHUB_REPOSITORY}` instead of a hard-coded
  `Breadway/breadclip` for the GitHub mirror release upload.
Breadway merged commit 6d9551ed65 into main 2026-08-31 18:14:16 +08:00
Breadway deleted branch feature/config-file 2026-08-31 18:14:17 +08:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: Breadway/breadclip#1
No description provided.