breadclip: document pin verb, config file, and ignore rules
All checks were successful
check / check (push) Successful in 2m9s
All checks were successful
check / check (push) Successful in 2m9s
- 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.
This commit is contained in:
parent
d308593efd
commit
6db4a96026
6 changed files with 88 additions and 25 deletions
|
|
@ -1,10 +1,11 @@
|
||||||
name: check
|
name: check
|
||||||
|
|
||||||
# Fast-fail lint/test on short-lived work branches, before it ever reaches
|
# Fast-fail lint/test on short-lived work branches, and on `main` itself —
|
||||||
# main and triggers a dev-track release build.
|
# a push to main immediately triggers a dev-track release build, so it
|
||||||
|
# should be linted/tested first, not shipped unchecked.
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: ['feature/**', 'fix/**']
|
branches: ['feature/**', 'fix/**', 'main']
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check:
|
check:
|
||||||
|
|
|
||||||
|
|
@ -64,9 +64,9 @@ jobs:
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
VERSION="${GITHUB_REF_NAME#v}"
|
VERSION="${GITHUB_REF_NAME#v}"
|
||||||
PKG_DIR="/srv/breadway-dl/breadclip/${VERSION}"
|
PKG_DIR="/srv/breadway-dl/breadclip/${VERSION}"
|
||||||
gh release create "${GITHUB_REF_NAME}" --repo Breadway/breadclip \
|
gh release create "${GITHUB_REF_NAME}" --repo "${GITHUB_REPOSITORY}" \
|
||||||
--title "breadclip v${VERSION}" --generate-notes 2>/dev/null || true
|
--title "breadclip v${VERSION}" --generate-notes 2>/dev/null || true
|
||||||
gh release upload "${GITHUB_REF_NAME}" --repo Breadway/breadclip \
|
gh release upload "${GITHUB_REF_NAME}" --repo "${GITHUB_REPOSITORY}" \
|
||||||
"${PKG_DIR}/breadclip-x86_64" \
|
"${PKG_DIR}/breadclip-x86_64" \
|
||||||
"${PKG_DIR}/breadclipd-x86_64" \
|
"${PKG_DIR}/breadclipd-x86_64" \
|
||||||
"${PKG_DIR}/breadclip-x86_64.sha256" \
|
"${PKG_DIR}/breadclip-x86_64.sha256" \
|
||||||
|
|
|
||||||
14
AGENTS.md
14
AGENTS.md
|
|
@ -51,12 +51,14 @@ Three crates:
|
||||||
through `bread-screenshots`; do not rewrite it just to retarget the crate pin.
|
through `bread-screenshots`; do not rewrite it just to retarget the crate pin.
|
||||||
|
|
||||||
`EVENTS.md` is the bread-event contract. App id `clip`. Implemented:
|
`EVENTS.md` is the bread-event contract. App id `clip`. Implemented:
|
||||||
`bread.clip.copied`, `bread.clip.clear.done`/`.failed`, and command
|
`bread.clip.copied`, `bread.clip.clear.done`/`.failed`,
|
||||||
`bread.command.clip.clear`. There is no pin/select — the history schema has
|
`bread.clip.pinned`/`bread.clip.pin.failed`, and commands
|
||||||
no pinned column and the popup has no pin UI. Do not invent
|
`bread.command.clip.clear` and `bread.command.clip.pin`. Pinning is real:
|
||||||
`bread.command.clip.pin`/`.select` (or matching events) ahead of a real
|
the history schema has a `pinned` column, the popup has a Ctrl+P toggle, and
|
||||||
product feature.
|
trim exempts pinned rows. There is no `select` — the popup is a transient
|
||||||
|
process with no resident service to receive `bread.command.clip.select`;
|
||||||
|
do not invent it (or `bread.clip.selected`) ahead of a real product feature.
|
||||||
|
|
||||||
## Don't
|
## Don't
|
||||||
- Don't embed credentials in remote URLs — SSH or a credential helper only.
|
- Don't embed credentials in remote URLs — SSH or a credential helper only.
|
||||||
- Don't invent pin/select on the event bus. See `EVENTS.md`.
|
- Don't invent `select` on the event bus. See `EVENTS.md`.
|
||||||
|
|
|
||||||
32
EVENTS.md
32
EVENTS.md
|
|
@ -21,6 +21,8 @@ per-clipboard-change process invocation, not inside a persistent loop).
|
||||||
| `bread.clip.copied` | `{ "kind": "url" \| "error" \| "code" \| "path" \| "plain", "len": <bytes or chars> }` | Every successful clipboard capture (text or image — images always get `kind: "image"`). `kind` is a heuristic classification (see `breadclipd/src/content_kind.rs`), not a guarantee — don't build a security decision on it. |
|
| `bread.clip.copied` | `{ "kind": "url" \| "error" \| "code" \| "path" \| "plain", "len": <bytes or chars> }` | Every successful clipboard capture (text or image — images always get `kind: "image"`). `kind` is a heuristic classification (see `breadclipd/src/content_kind.rs`), not a guarantee — don't build a security decision on it. |
|
||||||
| `bread.clip.clear.done` | `{}` | `bread.command.clip.clear` was received and history was successfully cleared. |
|
| `bread.clip.clear.done` | `{}` | `bread.command.clip.clear` was received and history was successfully cleared. |
|
||||||
| `bread.clip.clear.failed` | `{ "error": "<message>" }` | `bread.command.clip.clear` was received but clearing failed (e.g. DB error). |
|
| `bread.clip.clear.failed` | `{ "error": "<message>" }` | `bread.command.clip.clear` was received but clearing failed (e.g. DB error). |
|
||||||
|
| `bread.clip.pinned` | `{ "id": <entry id>, "pinned": true \| false }` | An entry was pinned or unpinned — either via `bread.command.clip.pin`, or locally from the popup's Ctrl+P toggle (which updates the DB directly; see below). |
|
||||||
|
| `bread.clip.pin.failed` | `{ "error": "<message>" }` | `bread.command.clip.pin` was received but pinning failed (e.g. DB error, or the command was missing its `id`). |
|
||||||
|
|
||||||
Content is never included in the payload — only its detected kind and length. History (including the actual copied content) stays local to breadclip's own SQLite database; the event bus is for *notifications about* clipboard activity, not a channel for clipboard content itself.
|
Content is never included in the payload — only its detected kind and length. History (including the actual copied content) stays local to breadclip's own SQLite database; the event bus is for *notifications about* clipboard activity, not a channel for clipboard content itself.
|
||||||
|
|
||||||
|
|
@ -28,19 +30,27 @@ Content is never included in the payload — only its detected kind and length.
|
||||||
|
|
||||||
| Verb | Effect |
|
| Verb | Effect |
|
||||||
|------|--------|
|
|------|--------|
|
||||||
| `clear` | Deletes all clipboard history (text entries and stored image files). Emits `bread.clip.clear.done`/`.failed`. |
|
| `clear` | Deletes all clipboard history (text entries, stored image files, pinned entries — it's a hard reset). Emits `bread.clip.clear.done`/`.failed`. |
|
||||||
|
| `pin` | Payload `{ "id": <entry id>, "pin": true \| false }`. Pins or unpins a history entry by id; `pin` defaults to `true` if omitted. Emits `bread.clip.pinned`/`bread.clip.pin.failed`. |
|
||||||
|
|
||||||
### Not implemented: `pin` / `select`
|
Pinned entries are exempt from the daemon's trimming and sort to the top of
|
||||||
|
the history list. The popup's Ctrl+P toggle writes the same `pinned` column
|
||||||
|
directly (it doesn't round-trip through the bus), and also emits
|
||||||
|
`bread.clip.pinned`, so automation can observe either path. Entry `id`s come
|
||||||
|
from `list_entries` — there is currently no read API on the bus for the
|
||||||
|
history contents.
|
||||||
|
|
||||||
An earlier draft of this integration planned `pin`/`select` verbs, but
|
### Not implemented: `select`
|
||||||
breadclip's history schema has no "pinned" concept at all today — there's no
|
|
||||||
column for it, and the GTK popup UI has no corresponding affordance. Adding
|
`select` (remote-activate a row from the bus, e.g. "paste entry 42 now")
|
||||||
real pin/select support is a product decision for breadclip itself (does it
|
is still deliberately not implemented: the popup is a short-lived process
|
||||||
want pinning, and what should the UI look like?), not something to fabricate
|
with no resident service to receive that command, and the daemon has no
|
||||||
as a side effect of wiring up the event bus. If/when breadclip grows that
|
reason to talk to a transient popup. That's a real product decision for
|
||||||
feature, the corresponding `bread.command.clip.pin`/`.select` verbs (and
|
breadclip itself — what should "select from the bus" even mean when the
|
||||||
matching `bread.clip.pinned`/`.selected` events) should be added at the same
|
popup isn't open? — not something to fabricate as a side effect of wiring
|
||||||
time, not stubbed out ahead of it.
|
up the event bus. When breadclip grows that feature, the matching
|
||||||
|
`bread.command.clip.select` verb and `bread.clip.selected` event should be
|
||||||
|
added at the same time.
|
||||||
|
|
||||||
## Fail-safe behavior
|
## Fail-safe behavior
|
||||||
|
|
||||||
|
|
|
||||||
32
README.md
32
README.md
|
|
@ -77,6 +77,7 @@ Running `breadclip` a second time while it is open closes it (toggle behaviour).
|
||||||
| `Up` / `Down` | Move selection |
|
| `Up` / `Down` | Move selection |
|
||||||
| `Enter` | Copy selected entry to clipboard and close |
|
| `Enter` | Copy selected entry to clipboard and close |
|
||||||
| `Delete` | Remove selected entry from history |
|
| `Delete` | Remove selected entry from history |
|
||||||
|
| `Ctrl+P` | Pin/unpin selected entry (pinned entries survive trimming and sort to the top) |
|
||||||
| `Escape` | Close without copying |
|
| `Escape` | Close without copying |
|
||||||
|
|
||||||
Clicking an entry copies it and closes the popup. Clicking outside the panel closes it.
|
Clicking an entry copies it and closes the popup. Clicking outside the panel closes it.
|
||||||
|
|
@ -85,6 +86,25 @@ Clicking an entry copies it and closes the popup. Clicking outside the panel clo
|
||||||
|
|
||||||
The popup has three filter chips — **All**, **Text**, **Images** — and a search box. The search box filters text entries by content; image entries only appear under the **Images** filter.
|
The popup has three filter chips — **All**, **Text**, **Images** — and a search box. The search box filters text entries by content; image entries only appear under the **Images** filter.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Optional TOML config at `$XDG_CONFIG_HOME/breadclip/config.toml` (typically
|
||||||
|
`~/.config/breadclip/config.toml`). Every key has a sensible default, so the
|
||||||
|
file can be omitted entirely — a copy of the full example lives in
|
||||||
|
`contrib/config.toml.example`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[retention]
|
||||||
|
text = 200 # max non-pinned text entries (0 = keep none)
|
||||||
|
images = 50 # max non-pinned image entries (0 = keep none)
|
||||||
|
|
||||||
|
[panel]
|
||||||
|
width = 520 # popup panel width, px
|
||||||
|
|
||||||
|
[capture]
|
||||||
|
primary = false # also watch the middle-click primary selection
|
||||||
|
```
|
||||||
|
|
||||||
## Data storage
|
## Data storage
|
||||||
|
|
||||||
History is stored under `$XDG_DATA_HOME/breadclip/` (typically `~/.local/share/breadclip/`):
|
History is stored under `$XDG_DATA_HOME/breadclip/` (typically `~/.local/share/breadclip/`):
|
||||||
|
|
@ -92,14 +112,20 @@ History is stored under `$XDG_DATA_HOME/breadclip/` (typically `~/.local/share/b
|
||||||
| Path | Contents |
|
| Path | Contents |
|
||||||
|------|----------|
|
|------|----------|
|
||||||
| `history.db` | SQLite database of all entries |
|
| `history.db` | SQLite database of all entries |
|
||||||
| `images/` | PNG files for image entries |
|
| `images/` | PNG/JPEG files for image entries |
|
||||||
|
|
||||||
The daemon keeps at most 200 text entries and 50 image entries, trimming oldest entries automatically.
|
The daemon trims the oldest non-pinned entries automatically, keeping at
|
||||||
|
most `retention.text` text entries and `retention.images` image entries
|
||||||
|
(defaults 200 and 50; configurable). **Pinned entries are exempt from
|
||||||
|
trimming** and sort to the top of the popup. Entries captured from the
|
||||||
|
primary (middle-click) selection — when `capture.primary = true` — are
|
||||||
|
stored alongside regular clipboard entries with a `primary` badge.
|
||||||
|
|
||||||
### Privacy
|
### Privacy
|
||||||
|
|
||||||
- `history.db` and every file under `images/` are created with `0600` permissions (owner read/write only), regardless of your umask.
|
- `history.db` and every file under `images/` are created with `0600` permissions (owner read/write only), regardless of your umask.
|
||||||
- breadclipd **never persists clipboard content flagged as sensitive by a password manager**. If a clipboard offer advertises the `x-kde-passwordManagerHint` MIME type — the convention used by KeePassXC, Bitwarden, and other password managers to mark content they own — that copy is skipped entirely and never reaches the database.
|
- breadclipd **never persists clipboard content flagged as sensitive**. `wl-paste --watch` reports copies made with `wl-copy --sensitive` — which also covers offers advertising the `x-kde-passwordManagerHint` MIME type, the convention used by KeePassXC, Bitwarden, and other password managers to mark content they own — via `CLIPBOARD_STATE=sensitive`, and those copies are skipped entirely and never reach the database.
|
||||||
|
- On top of that, breadclipd runs **best-effort ignore rules** that skip copies that *look* like secrets even when the app didn't flag them: one-time codes, Luhn-valid credit card numbers, private key blocks, `password:`-style credential lines, and well-known API token prefixes (see `breadclipd/src/ignore_rules.rs`). These are deliberately conservative and are a convenience, not a security boundary — the 0600/0700 permissions are the real protection.
|
||||||
- That said, this is still a plaintext SQLite database of everything else you copy. Anything copied by an app that doesn't set the hint (e.g. copying a password from a terminal or a non-integrated app) will be stored like any other text entry. Treat `history.db` as sensitive, and don't rely on it as your only safeguard.
|
- That said, this is still a plaintext SQLite database of everything else you copy. Anything copied by an app that doesn't set the hint (e.g. copying a password from a terminal or a non-integrated app) will be stored like any other text entry. Treat `history.db` as sensitive, and don't rely on it as your only safeguard.
|
||||||
|
|
||||||
## Theming
|
## Theming
|
||||||
|
|
|
||||||
24
contrib/config.toml.example
Normal file
24
contrib/config.toml.example
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
# breadclip configuration — copy to
|
||||||
|
# $XDG_CONFIG_HOME/breadclip/config.toml (usually ~/.config/breadclip/config.toml)
|
||||||
|
#
|
||||||
|
# Every key is optional; a missing file, missing key, or out-of-range value
|
||||||
|
# falls back to the default shown here. A config file that fails to parse is
|
||||||
|
# backed up to config.toml.bak once before defaults are used.
|
||||||
|
|
||||||
|
[retention]
|
||||||
|
# Max non-pinned entries kept per kind; the oldest are trimmed automatically.
|
||||||
|
# 0 means "keep no (unpinned) entries of this kind". Pinned entries are never
|
||||||
|
# trimmed, regardless of these caps.
|
||||||
|
text = 200
|
||||||
|
images = 50
|
||||||
|
|
||||||
|
[panel]
|
||||||
|
# Popup panel width in pixels.
|
||||||
|
width = 520
|
||||||
|
|
||||||
|
[capture]
|
||||||
|
# Also watch the middle-click "primary" selection (wl-paste --watch --primary)
|
||||||
|
# and store those entries alongside regular clipboard entries with a
|
||||||
|
# "primary" badge. Off by default because primary selections tend to be
|
||||||
|
# transient and noisy.
|
||||||
|
primary = false
|
||||||
Loading…
Add table
Add a link
Reference in a new issue