Compare commits
No commits in common. "main" and "v0.3.0" have entirely different histories.
23 changed files with 446 additions and 1089 deletions
|
|
@ -1,24 +0,0 @@
|
|||
name: check
|
||||
|
||||
# Fast-fail lint/test on short-lived work branches, before it ever reaches
|
||||
# main and triggers a dev-track release build.
|
||||
on:
|
||||
push:
|
||||
branches: ['feature/**', 'fix/**']
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: [self-hosted, hestia]
|
||||
steps:
|
||||
- name: checkout
|
||||
run: |
|
||||
set -euo pipefail
|
||||
rm -rf src && mkdir src
|
||||
git clone --branch "${GITHUB_REF_NAME}" --depth 1 \
|
||||
"https://git.breadway.dev/${GITHUB_REPOSITORY}.git" src
|
||||
|
||||
- name: clippy
|
||||
run: cd src && bash ci/build.sh cargo clippy --workspace --all-targets --locked --features full -- -D warnings
|
||||
|
||||
- name: test
|
||||
run: cd src && bash ci/build.sh cargo test --workspace --locked --features full
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
name: dev release
|
||||
|
||||
# Publishes a dev-track build on every push to `main` (the trunk
|
||||
# branch — there is no separate `dev` branch). See bread-ecosystem's
|
||||
# docs/release-channels.md for the release-track policy this is part of.
|
||||
on:
|
||||
push:
|
||||
branches: ['main']
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: [self-hosted, hestia]
|
||||
steps:
|
||||
- name: checkout
|
||||
run: |
|
||||
set -euo pipefail
|
||||
rm -rf src && mkdir src
|
||||
git clone --branch main --depth 1 \
|
||||
"https://git.breadway.dev/${GITHUB_REPOSITORY}.git" src
|
||||
|
||||
- name: build
|
||||
run: cd src && bash ci/build.sh cargo build --release --locked --workspace --features full
|
||||
|
||||
- name: test
|
||||
run: cd src && bash ci/build.sh cargo test --release --locked --workspace --features full
|
||||
|
||||
- name: compute dev version
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cd src
|
||||
# Base the dev version off the latest published stable tag,
|
||||
# not Cargo.toml — Cargo.toml can go stale relative to the last
|
||||
# real release (seen in practice: breadbox/breadpad/breadcrumbs/
|
||||
# breadpaper), which would make a dev build sort as OLDER than
|
||||
# what's already installed and bakery would correctly refuse it.
|
||||
LATEST_TAG="$(git ls-remote --tags --refs \
|
||||
"https://git.breadway.dev/${GITHUB_REPOSITORY}.git" 'v*' \
|
||||
| awk -F/ '{print $NF}' | sed 's/^v//' | (grep -v -- '-' || true) | sort -V | tail -1)"
|
||||
if [ -n "${LATEST_TAG}" ]; then
|
||||
CUR="${LATEST_TAG}"
|
||||
else
|
||||
CUR="$(grep -m1 '^version' breadsearch/Cargo.toml | sed -E 's/.*"(.*)".*/\1/')"
|
||||
fi
|
||||
IFS='.' read -r MA MI PA <<< "${CUR}"
|
||||
SHA="$(git rev-parse --short HEAD)"
|
||||
TS="$(date -u +%Y%m%d%H%M%S)"
|
||||
echo "VERSION=${MA}.${MI}.$((PA + 1))-dev.${TS}+${SHA}" >> "$GITHUB_ENV"
|
||||
|
||||
- name: prepare artifacts
|
||||
run: |
|
||||
set -euo pipefail
|
||||
PKG_DIR="/srv/breadway-dl/dev/breadsearch/${VERSION}"
|
||||
mkdir -p "${PKG_DIR}"
|
||||
for bin in breadsearch breadmill; do
|
||||
cp "src/target/release/${bin}" "${PKG_DIR}/${bin}-x86_64"
|
||||
strip "${PKG_DIR}/${bin}-x86_64"
|
||||
sha256sum "${PKG_DIR}/${bin}-x86_64" | awk '{print $1}' \
|
||||
> "${PKG_DIR}/${bin}-x86_64.sha256"
|
||||
done
|
||||
cp src/packaging/breadmill.service "${PKG_DIR}/"
|
||||
cp src/config.example.toml "${PKG_DIR}/"
|
||||
cp src/bakery.toml "${PKG_DIR}/bakery.toml"
|
||||
ln -sfn "${VERSION}" "/srv/breadway-dl/dev/breadsearch/latest"
|
||||
|
||||
# No GitHub Release upload — dev, like the other non-stable track,
|
||||
# is only distributed via dl.breadway.dev/dev/.
|
||||
- name: regenerate dev index.json
|
||||
env:
|
||||
MINISIGN_SEC_KEY: ${{ secrets.BAKERY_MINISIGN_SEC_KEY_PATH }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -z "${MINISIGN_SEC_KEY:-}" ]; then
|
||||
echo "::error::BAKERY_MINISIGN_SEC_KEY_PATH secret not set — refusing to regenerate dev index.json unsigned (would leave a stale signature mismatched against fresh content and break bakery for everyone on the dev track)"
|
||||
exit 1
|
||||
fi
|
||||
rm -rf /tmp/bread-ecosystem-ci-* 2>/dev/null || true
|
||||
# mktemp: a fixed clone path races when multiple repos' dev/beta
|
||||
# workflows run close together on the same self-hosted runner.
|
||||
ECOSYSTEM_CI_DIR="$(mktemp -d /tmp/bread-ecosystem-ci-XXXXXX)"
|
||||
git clone --branch main https://git.breadway.dev/Breadway/bread-ecosystem.git "${ECOSYSTEM_CI_DIR}"
|
||||
TRACK=dev bash "${ECOSYSTEM_CI_DIR}/scripts/gen-index.sh"
|
||||
rm -rf "${ECOSYSTEM_CI_DIR}"
|
||||
19
.forgejo/workflows/mirror.yml
Normal file
19
.forgejo/workflows/mirror.yml
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
name: Mirror to GitHub
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ['**']
|
||||
tags: ['**']
|
||||
|
||||
jobs:
|
||||
mirror:
|
||||
runs-on: [self-hosted, hestia]
|
||||
steps:
|
||||
- name: Mirror to GitHub
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git clone --mirror "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" repo.git
|
||||
cd repo.git
|
||||
git push --prune \
|
||||
"https://x-access-token:${{ secrets.MIRROR_TOKEN }}@github.com/Breadway/breadsearch.git" \
|
||||
'+refs/heads/*:refs/heads/*' '+refs/tags/*:refs/tags/*'
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
name: beta (rc) release
|
||||
|
||||
# Publishes a beta-track build for any `vX.Y.Z-rc.N` prerelease tag
|
||||
# pushed to `main` — there is no separate `beta` branch; "freezing" is
|
||||
# just pausing pushes to main while an RC gets tested. See
|
||||
# bread-ecosystem's docs/release-channels.md for the release-track policy.
|
||||
on:
|
||||
push:
|
||||
tags: ['v*']
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: ${{ contains(github.ref_name, '-rc.') }}
|
||||
runs-on: [self-hosted, hestia]
|
||||
steps:
|
||||
- name: checkout
|
||||
run: |
|
||||
set -euo pipefail
|
||||
rm -rf src && mkdir src
|
||||
git clone --branch "${GITHUB_REF_NAME}" --depth 1 \
|
||||
"https://git.breadway.dev/${GITHUB_REPOSITORY}.git" src
|
||||
|
||||
- name: build
|
||||
run: cd src && bash ci/build.sh cargo build --release --locked --workspace --features full
|
||||
|
||||
- name: test
|
||||
run: cd src && bash ci/build.sh cargo test --release --locked --workspace --features full
|
||||
|
||||
- name: prepare artifacts
|
||||
run: |
|
||||
set -euo pipefail
|
||||
VERSION="${GITHUB_REF_NAME#v}"
|
||||
PKG_DIR="/srv/breadway-dl/beta/breadsearch/${VERSION}"
|
||||
mkdir -p "${PKG_DIR}"
|
||||
for bin in breadsearch breadmill; do
|
||||
cp "src/target/release/${bin}" "${PKG_DIR}/${bin}-x86_64"
|
||||
strip "${PKG_DIR}/${bin}-x86_64"
|
||||
sha256sum "${PKG_DIR}/${bin}-x86_64" | awk '{print $1}' \
|
||||
> "${PKG_DIR}/${bin}-x86_64.sha256"
|
||||
done
|
||||
cp src/packaging/breadmill.service "${PKG_DIR}/"
|
||||
cp src/config.example.toml "${PKG_DIR}/"
|
||||
cp src/bakery.toml "${PKG_DIR}/bakery.toml"
|
||||
ln -sfn "${VERSION}" "/srv/breadway-dl/beta/breadsearch/latest"
|
||||
|
||||
# No GitHub Release upload — beta, like dev, is only distributed via
|
||||
# dl.breadway.dev/beta/.
|
||||
- name: regenerate beta index.json
|
||||
env:
|
||||
MINISIGN_SEC_KEY: ${{ secrets.BAKERY_MINISIGN_SEC_KEY_PATH }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -z "${MINISIGN_SEC_KEY:-}" ]; then
|
||||
echo "::error::BAKERY_MINISIGN_SEC_KEY_PATH secret not set — refusing to regenerate beta index.json unsigned (would leave a stale signature mismatched against fresh content and break bakery for everyone on the beta track)"
|
||||
exit 1
|
||||
fi
|
||||
rm -rf /tmp/bread-ecosystem-ci-* 2>/dev/null || true
|
||||
# mktemp: a fixed clone path races when multiple repos' dev/beta
|
||||
# workflows run close together on the same self-hosted runner.
|
||||
ECOSYSTEM_CI_DIR="$(mktemp -d /tmp/bread-ecosystem-ci-XXXXXX)"
|
||||
git clone https://git.breadway.dev/Breadway/bread-ecosystem.git "${ECOSYSTEM_CI_DIR}"
|
||||
TRACK=beta bash "${ECOSYSTEM_CI_DIR}/scripts/gen-index.sh"
|
||||
rm -rf "${ECOSYSTEM_CI_DIR}"
|
||||
|
|
@ -6,7 +6,6 @@ on:
|
|||
|
||||
jobs:
|
||||
build:
|
||||
if: ${{ !contains(github.ref_name, '-rc.') }}
|
||||
runs-on: [self-hosted, hestia]
|
||||
steps:
|
||||
- name: checkout
|
||||
|
|
@ -22,19 +21,10 @@ jobs:
|
|||
# `backend` in config.toml. All three are ort load-dynamic (dlopen)
|
||||
# EPs, so this doesn't require the NPU/ROCm/CUDA toolkits to be
|
||||
# present on the build host — see breadmill/Cargo.toml.
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ ! -f src/ci/build.sh ]; then
|
||||
echo "::error::ci/build.sh is missing — bakery release builds must go through the shared CI wrapper"
|
||||
exit 1
|
||||
fi
|
||||
cd src && bash ci/build.sh cargo build --release --locked --workspace --features full || {
|
||||
echo "::error::cargo build --release --locked failed. If Cargo.lock drifted, update and commit it; do not drop --locked."
|
||||
exit 1
|
||||
}
|
||||
run: cd src && cargo build --release --locked --workspace --features full
|
||||
|
||||
- name: test
|
||||
run: cd src && bash ci/build.sh cargo test --release --locked --workspace --features full
|
||||
run: cd src && cargo test --release --locked --workspace --features full
|
||||
|
||||
- name: prepare artifacts
|
||||
run: |
|
||||
|
|
@ -54,14 +44,8 @@ jobs:
|
|||
ln -sfn "${VERSION}" "/srv/breadway-dl/breadsearch/latest"
|
||||
|
||||
- name: regenerate index.json
|
||||
env:
|
||||
MINISIGN_SEC_KEY: ${{ secrets.BAKERY_MINISIGN_SEC_KEY_PATH }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -z "${MINISIGN_SEC_KEY:-}" ]; then
|
||||
echo "::error::BAKERY_MINISIGN_SEC_KEY_PATH secret not set — refusing to regenerate index.json unsigned (would leave a stale signature mismatched against fresh content and break bakery for everyone)"
|
||||
exit 1
|
||||
fi
|
||||
rm -rf /tmp/bread-ecosystem-ci
|
||||
git clone https://git.breadway.dev/Breadway/bread-ecosystem.git /tmp/bread-ecosystem-ci
|
||||
bash /tmp/bread-ecosystem-ci/scripts/gen-index.sh
|
||||
|
|
|
|||
50
AGENTS.md
50
AGENTS.md
|
|
@ -1,50 +0,0 @@
|
|||
# AGENTS.md — Repo hygiene
|
||||
|
||||
Scope: this file covers *repo hygiene* — branching, remotes, CI — plus a
|
||||
short map of the binaries. It is not user-facing project documentation.
|
||||
|
||||
This repo follows the branch/release workflow documented in `CONTRIBUTING.md`
|
||||
— read and follow it for any git, branch, or release work here (the
|
||||
single-trunk model, `feature/x`/`fix/x` branch naming, how RC tags work,
|
||||
etc). Don't improvise a different workflow. The short version: there is one
|
||||
long-lived branch, `main` — no `dev` or `beta` branch exists. `main`
|
||||
auto-publishes a dev-track build on every push. "Beta" and "stable" are both
|
||||
just tags, not branches: push a `vX.Y.Z-rc.N` tag to publish a beta-track
|
||||
build, push a plain `vX.Y.Z` tag to cut the signed stable release.
|
||||
"Freezing" for stabilization means pausing pushes to `main`, not moving a
|
||||
branch. This replaced an earlier three-branch (`dev`/`beta`/`main`) model
|
||||
after `main` was found to have silently rotted out of sync with `dev`/`beta`
|
||||
across most repos in this ecosystem.
|
||||
|
||||
When starting work on a new feature, create branch `feature/<feature-name>`.
|
||||
When working on a bug or issue, create branch `fix/<issue you are fixing>`.
|
||||
|
||||
## Remotes
|
||||
- `origin` — Forgejo (`git.breadway.dev` via Hestia, SSH) — authoritative.
|
||||
- `github` — GitHub mirror. Push both when publishing.
|
||||
|
||||
## CI
|
||||
- `dev-release.yml` triggers on `push: branches: ['main']`.
|
||||
- `rc-release.yml` triggers on `vX.Y.Z-rc.N` tag pushes (beta track).
|
||||
- `release.yml` triggers on any other `v*` tag push (stable).
|
||||
None of these run on plain commits or PRs beyond what's listed.
|
||||
|
||||
## Architecture
|
||||
|
||||
Two binaries + a shared lib:
|
||||
|
||||
| Crate | Role |
|
||||
|---|---|
|
||||
| `breadsearch-shared` | XDG paths, config, IPC types, Unix-socket client |
|
||||
| `breadmill` | Daemon: walk → extract → chunk → embed → index; serves queries |
|
||||
| `breadsearch` | GTK4 layer-shell overlay: ranked document hits from breadmill |
|
||||
|
||||
breadsearch is document/meaning search. breadbox is the app launcher. They
|
||||
are not the same overlay.
|
||||
|
||||
`--screenshot` (`breadsearch/src/screenshot.rs`) captures the search panel
|
||||
through `bread-screenshots`; do not rewrite it just to retarget the crate pin.
|
||||
|
||||
## Don't
|
||||
- Don't embed credentials in remote URLs — SSH or a credential helper only.
|
||||
- Don't rewrite `screenshot.rs` as part of pin/docs work.
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
# Contributing
|
||||
|
||||
`breadsearch` — Semantic system-wide search for BOS.
|
||||
|
||||
Part of the bread ecosystem; this repo follows the same branch/release
|
||||
workflow as every other ecosystem product.
|
||||
|
||||
## Branches
|
||||
|
||||
There is one long-lived branch: **`main`**. All day-to-day work lands here.
|
||||
Every push to `main` automatically builds and publishes a **dev-track**
|
||||
build (see Tracks below) — a real install you can test before cutting
|
||||
anything more formal.
|
||||
|
||||
New work — features and bug fixes alike — goes on a short-lived branch:
|
||||
|
||||
```
|
||||
feature/<short-name>
|
||||
fix/<issue-number-or-short-name>
|
||||
```
|
||||
|
||||
Branch off `main`, open a PR/push back into `main` when ready. Short-lived
|
||||
branches get deleted on merge — they never accumulate the kind of drift a
|
||||
second long-lived branch does.
|
||||
|
||||
## The release cycle
|
||||
|
||||
There's no separate `beta` or release branch — "stable" and "beta" are both
|
||||
just **tags** on `main`, not branches that need to be kept in sync:
|
||||
|
||||
1. Work accumulates on `main` via `feature/x` / `fix/x` branches. Each push
|
||||
auto-publishes a dev build — install it with `bakery track set dev` and
|
||||
`bakery update --all`, then fix anything broken with another push.
|
||||
2. When you want to stabilize before a real release, tag a release
|
||||
candidate: `git tag vX.Y.Z-rc.1 && git push origin vX.Y.Z-rc.1` (push to
|
||||
both remotes). That tag alone triggers a beta-track build —
|
||||
"freezing" is just pausing pushes to `main` while you test it, not a
|
||||
branch operation. Cut `-rc.2`, `-rc.3`, etc. for further fixes.
|
||||
3. Once an RC has gone without issues, tag the real release:
|
||||
`git tag vX.Y.Z && git push origin vX.Y.Z` — that's what triggers the
|
||||
signed stable release build.
|
||||
|
||||
## Tracks, from a user's perspective
|
||||
|
||||
```
|
||||
bakery track show # what you're currently on (defaults to stable)
|
||||
bakery track set dev # or beta, or stable
|
||||
bakery update --all # pull the latest build on your current track
|
||||
```
|
||||
|
||||
| Track | What it is | Published from |
|
||||
|--------|-----------|-----------------|
|
||||
| `stable` | The last tagged release | a `vX.Y.Z` tag |
|
||||
| `beta` | Latest release candidate | a `vX.Y.Z-rc.N` tag |
|
||||
| `dev` | Bleeding edge | `main`, on every push |
|
||||
|
||||
Dev versions are auto-computed (`X.Y.Z-dev.<timestamp>+<sha>`) from the
|
||||
latest published stable tag, so they always sort as newer than what you
|
||||
have installed — no manual version bumping needed. Beta versions are just
|
||||
the RC tag itself (already valid semver, already sorts below the real
|
||||
release it's a candidate for).
|
||||
|
||||
## Local development
|
||||
|
||||
```sh
|
||||
cargo build --release --workspace --features full
|
||||
cargo test --release --workspace --features full
|
||||
```
|
||||
|
||||
## CI
|
||||
|
||||
- `dev-release.yml` — triggered on push to `main`.
|
||||
- `rc-release.yml` — triggered on any `vX.Y.Z-rc.N` tag push.
|
||||
- `release.yml` — triggered on any other `v*` tag push, cuts the actual
|
||||
stable release.
|
||||
|
||||
All CI runs on a self-hosted runner; nothing runs automatically on plain
|
||||
commits or PRs beyond the track builds above. See
|
||||
[bread-ecosystem's docs/release-channels.md](https://git.breadway.dev/Breadway/bread-ecosystem/src/branch/main/docs/release-channels.md)
|
||||
for the full policy, including how a new product gets wired onto these tracks.
|
||||
|
||||
## Questions
|
||||
|
||||
Open an issue on this repo's Forgejo tracker.
|
||||
660
Cargo.lock
generated
660
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
62
EVENTS.md
62
EVENTS.md
|
|
@ -1,62 +0,0 @@
|
|||
# breadsearch — bread event integration
|
||||
|
||||
breadsearch is a standalone document-search overlay: it works exactly the
|
||||
same with or without `breadd` running. When breadd *is* present, the
|
||||
`breadsearch` GTK overlay publishes events into the shared bread automation
|
||||
fabric. See the parent `bread` repo's `Documentation.md` — specifically its
|
||||
"Namespaces" and "Integrating a bread\* app" sections — for the general
|
||||
convention this follows.
|
||||
|
||||
App id: **`search`**. Transport: `bread-utils`'s `bread_client` module
|
||||
(feature `bread-client`) — the overlay links it directly. Each `emit` is
|
||||
its own short-lived connection (`BreadClient::emit` is fire-and-forget,
|
||||
the same stance as `bread-emit`). Command verbs are only received while
|
||||
`breadsearch listen` is running — that process holds the
|
||||
`bread.command.search.**` subscription open. breadmill, the indexing
|
||||
daemon, does not talk to breadd.
|
||||
|
||||
## Events published (`bread.search.*`)
|
||||
|
||||
| Event | Data | When |
|
||||
|-------|------|------|
|
||||
| `bread.search.opened` | `{}` | The overlay window maps (the search panel is shown). |
|
||||
| `bread.search.opened_result` | `{ "path": "<hit path>" }` | The user opens a hit — Enter / click opens the file, Ctrl+Enter reveals its folder. `path` is the hit's document path, not the parent folder. |
|
||||
| `bread.search.open.done` | `{}` | `bread.command.search.open` was received and `breadsearch` was spawned. This is the command confirmation, not proof the overlay mapped — the spawned process is the same PID-file toggle as a keybind. |
|
||||
| `bread.search.open.failed` | `{ "error": "<message>" }` | `bread.command.search.open` was received but this binary could not be started. |
|
||||
|
||||
## Commands honored (`bread.command.search.*`)
|
||||
|
||||
These are only received while `breadsearch listen` is running. Publishing a
|
||||
command with no subscriber is a silent no-op — that is the documented
|
||||
bread convention, not a breadsearch bug.
|
||||
|
||||
| Verb | Data | Effect |
|
||||
|------|------|--------|
|
||||
| `open` | none | Same as running `breadsearch` (PID-file toggle: show the overlay, or dismiss it if it is already up). Emits `bread.search.open.done` / `.failed`. |
|
||||
|
||||
```lua
|
||||
bread.spawn(function()
|
||||
bread.emit("bread.command.search.open")
|
||||
bread.wait("bread.search.open.done", { timeout = 5000 })
|
||||
end)
|
||||
```
|
||||
|
||||
### Not implemented: extra verbs
|
||||
|
||||
There is no `query` / `close` / `reindex` command verb. breadmill already
|
||||
has its own query socket; inventing a bus query plane would be a new
|
||||
product surface. If/when that exists, add the corresponding
|
||||
`bread.command.search.*` verb at the same time, not stubbed as a no-op
|
||||
ahead of it.
|
||||
|
||||
## Fail-safe behavior
|
||||
|
||||
- If breadd isn't installed or isn't running, `emit` is a silent no-op
|
||||
(`BreadClient::emit` never blocks or errors the caller) and the
|
||||
command subscription simply never receives anything — breadsearch's
|
||||
overlay and breadmill's indexing/query path are entirely unaffected.
|
||||
- If breadd restarts, the command subscription reconnects automatically
|
||||
(`BreadClient::subscribe`'s background thread has its own backoff
|
||||
loop); no restart of `breadsearch listen` is needed.
|
||||
- If `breadsearch listen` is not running, commands are a graceful no-op at
|
||||
the bus (no subscriber). The overlay CLI still works.
|
||||
|
|
@ -2,8 +2,6 @@
|
|||
|
||||
Semantic document search for Bread OS. Type a concept — not a keyword — and get ranked hits from your documents.
|
||||
|
||||
This is **not** [breadbox](https://git.breadway.dev/Breadway/breadbox). breadsearch is document/meaning search over your files; breadbox is the app launcher (fuzzy `.desktop` launch). They are separate overlays with separate binaries, sockets, and keybinds — even though the search panel was originally forked from breadbox's launcher UI.
|
||||
|
||||
Two binaries in one Cargo workspace:
|
||||
|
||||
- **breadmill** — background daemon. Walks configured directories, extracts text, chunks and embeds documents with [nomic-embed-text-v1.5](https://huggingface.co/nomic-ai/nomic-embed-text-v1.5) (768-dim ONNX), stores vectors in an HNSW index (usearch) backed by SQLite metadata, and serves queries over a Unix socket. Watches for filesystem changes and re-indexes incrementally.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "breadmill"
|
||||
version = "0.3.3"
|
||||
version = "0.3.0"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
|
||||
|
|
@ -42,7 +42,7 @@ breadsearch-shared = { path = "../breadsearch-shared" }
|
|||
# needed for the plain CPU path even in the npu build.
|
||||
ort = { version = "2.0.0-rc.12", default-features = false, features = ["std", "tracing", "download-binaries", "tls-native", "copy-dylibs", "api-23"] }
|
||||
tokenizers = "0"
|
||||
bread-onnx = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.2" }
|
||||
bread-onnx = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.3.0" }
|
||||
|
||||
# Surfaces ort's own EP-registration tracing (e.g. a GPU EP silently failing to
|
||||
# register and falling back to CPU) as visible log output instead of nowhere.
|
||||
|
|
|
|||
|
|
@ -81,9 +81,10 @@ fn split_by_chars(chunk: Chunk, max_chars: usize) -> Vec<Chunk> {
|
|||
let text = &chunk.text;
|
||||
let mut result = Vec::new();
|
||||
let mut seg_start = 0usize;
|
||||
let mut count = 0usize;
|
||||
|
||||
for (count, (byte_idx, _)) in text.char_indices().enumerate() {
|
||||
if count > 0 && count.is_multiple_of(max_chars) {
|
||||
for (byte_idx, _) in text.char_indices() {
|
||||
if count > 0 && count % max_chars == 0 {
|
||||
result.push(Chunk {
|
||||
text: text[seg_start..byte_idx].to_string(),
|
||||
start: chunk.start + seg_start,
|
||||
|
|
@ -91,6 +92,7 @@ fn split_by_chars(chunk: Chunk, max_chars: usize) -> Vec<Chunk> {
|
|||
});
|
||||
seg_start = byte_idx;
|
||||
}
|
||||
count += 1;
|
||||
}
|
||||
if seg_start < text.len() {
|
||||
result.push(Chunk {
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ impl Indexer {
|
|||
pub fn full_reindex(&self) {
|
||||
eprintln!("breadmill: full reindex triggered");
|
||||
{
|
||||
let store = self.state.store.lock_recover();
|
||||
let mut store = self.state.store.lock_recover();
|
||||
// Clear all state
|
||||
let _ = store.conn.execute_batch("DELETE FROM chunks; DELETE FROM files;");
|
||||
let _ = store.index.reserve(4096);
|
||||
|
|
@ -416,9 +416,9 @@ fn sha256_str(bytes: &[u8]) -> String {
|
|||
}
|
||||
|
||||
pub fn expand_home(path: &str) -> PathBuf {
|
||||
if let Some(rest) = path.strip_prefix("~/") {
|
||||
if path.starts_with("~/") {
|
||||
let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".into());
|
||||
PathBuf::from(home).join(rest)
|
||||
PathBuf::from(home).join(&path[2..])
|
||||
} else {
|
||||
PathBuf::from(path)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ use usearch::{Index, IndexOptions, MetricKind, ScalarKind, new_index};
|
|||
pub struct Store {
|
||||
pub conn: Connection,
|
||||
pub index: Index,
|
||||
pub dim: usize,
|
||||
}
|
||||
|
||||
// usearch::Index wraps a raw C++ pointer; access is serialized by the Mutex<Store>.
|
||||
|
|
@ -86,7 +87,7 @@ impl Store {
|
|||
index.reserve(4096).map_err(|e| e.to_string())?;
|
||||
}
|
||||
|
||||
Ok(Self { conn, index })
|
||||
Ok(Self { conn, index, dim })
|
||||
}
|
||||
|
||||
// ---- file state ---------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "breadsearch-shared"
|
||||
version = "0.3.3"
|
||||
version = "0.3.0"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ pub fn socket_path() -> PathBuf {
|
|||
|
||||
// ---- Config -----------------------------------------------------------------
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
#[serde(default)]
|
||||
pub index: IndexConfig,
|
||||
|
|
@ -157,6 +157,16 @@ impl Default for ModelConfig {
|
|||
}
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
index: IndexConfig::default(),
|
||||
search: SearchConfig::default(),
|
||||
model: ModelConfig::default(),
|
||||
power: PowerConfig::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PowerConfig {
|
||||
|
|
@ -245,7 +255,8 @@ pub struct StatusInfo {
|
|||
pub fn send_request(req: &Request) -> std::io::Result<Response> {
|
||||
let mut stream = UnixStream::connect(socket_path())?;
|
||||
|
||||
let mut line = serde_json::to_string(req).map_err(std::io::Error::other)?;
|
||||
let mut line = serde_json::to_string(req)
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
|
||||
line.push('\n');
|
||||
stream.write_all(line.as_bytes())?;
|
||||
stream.flush()?;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "breadsearch"
|
||||
version = "0.3.3"
|
||||
version = "0.3.0"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
|
||||
|
|
@ -10,13 +10,7 @@ path = "src/main.rs"
|
|||
|
||||
[dependencies]
|
||||
breadsearch-shared = { path = "../breadsearch-shared" }
|
||||
bread-theme = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.4", features = ["gtk"] }
|
||||
# Bread event fabric client — emit bread.search.* (fail-silent if breadd is down).
|
||||
bread-utils = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.2", features = ["bread-client", "gtk"] }
|
||||
# Capture primitives for `--screenshot` mode — see src/screenshot.rs.
|
||||
bread-screenshots = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.2" }
|
||||
bread-theme = { git = "https://github.com/Breadway/bread-ecosystem", tag = "v0.2.10", features = ["gtk"] }
|
||||
gtk4 = { version = "0.11", features = ["v4_12"] }
|
||||
gtk4-layer-shell = "0.8"
|
||||
serde_json = "1"
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
anyhow = "1"
|
||||
|
|
|
|||
|
|
@ -1,26 +0,0 @@
|
|||
//! `bread.search.*` event integration — optional, non-blocking. See
|
||||
//! `EVENTS.md` at the repo root for the full contract. breadsearch works
|
||||
//! identically with or without breadd running; every call here is
|
||||
//! fire-and-forget (`BreadClient::emit` never blocks or errors this
|
||||
//! process) so a missing or restarting breadd never affects the overlay.
|
||||
|
||||
use bread_utils::bread_client::BreadClient;
|
||||
|
||||
/// This app's id in bread's sibling-app namespace registry
|
||||
/// (`bread_shared::apps::KNOWN_APPS`) — events publish as `bread.search.*`.
|
||||
pub const APP_ID: &str = "search";
|
||||
|
||||
fn client() -> BreadClient {
|
||||
BreadClient::connect(APP_ID)
|
||||
}
|
||||
|
||||
pub fn emit_opened() {
|
||||
client().emit("bread.search.opened", serde_json::json!({}));
|
||||
}
|
||||
|
||||
pub fn emit_opened_result(path: &str) {
|
||||
client().emit(
|
||||
"bread.search.opened_result",
|
||||
serde_json::json!({ "path": path }),
|
||||
);
|
||||
}
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
//! Long-running command subscription for `bread.command.search.*`.
|
||||
//!
|
||||
//! `breadsearch` is still a one-shot toggle overlay by default.
|
||||
//! `breadsearch listen` is the optional persistent process that can honor
|
||||
//! bus commands. See `EVENTS.md`.
|
||||
|
||||
use bread_utils::bread_client::{BreadClient, BreadEvent};
|
||||
|
||||
use crate::bread_events::APP_ID;
|
||||
|
||||
/// Subscribe to `bread.command.search.**` and block until the process is killed.
|
||||
///
|
||||
/// breadd being absent is not an error: [`BreadClient::subscribe`] reconnects
|
||||
/// with backoff, and `on_event` simply isn't called until the daemon is up.
|
||||
pub fn run() {
|
||||
let client = BreadClient::connect(APP_ID);
|
||||
if client.health().is_none() {
|
||||
eprintln!(
|
||||
"breadsearch: breadd unreachable; command subscription will connect when it comes back"
|
||||
);
|
||||
}
|
||||
|
||||
let _commands = client.subscribe("bread.command.search.**", |event| {
|
||||
handle_command(&event);
|
||||
});
|
||||
|
||||
eprintln!("breadsearch: listening for bread.command.search.**");
|
||||
loop {
|
||||
std::thread::park();
|
||||
}
|
||||
}
|
||||
|
||||
/// Reacts to `bread.command.search.*` verbs. Only `open` is honored today —
|
||||
/// other verbs are ignored, not stubbed as no-ops that pretend to succeed.
|
||||
fn handle_command(event: &BreadEvent) {
|
||||
let Some(verb) = command_verb(&event.event) else {
|
||||
return;
|
||||
};
|
||||
match verb {
|
||||
"open" => handle_open(),
|
||||
other => {
|
||||
eprintln!("breadsearch: ignoring unrecognized bread.command.search.{other}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_open() {
|
||||
// Same as running `breadsearch` from a keybind: the PID-file toggle
|
||||
// shows the overlay (or dismisses it if it is already up).
|
||||
let result = spawn_self();
|
||||
let client = BreadClient::connect(APP_ID);
|
||||
match result {
|
||||
Ok(_) => client.emit("bread.search.open.done", serde_json::json!({})),
|
||||
Err(e) => {
|
||||
eprintln!("breadsearch: bread.command.search.open failed: {e}");
|
||||
client.emit(
|
||||
"bread.search.open.failed",
|
||||
serde_json::json!({ "error": e.to_string() }),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_self() -> std::io::Result<std::process::Child> {
|
||||
let exe = std::env::current_exe().unwrap_or_else(|_| std::path::PathBuf::from("breadsearch"));
|
||||
std::process::Command::new(exe).spawn()
|
||||
}
|
||||
|
||||
fn command_verb(event_name: &str) -> Option<&str> {
|
||||
event_name.strip_prefix("bread.command.search.")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn command_verb_strips_search_prefix() {
|
||||
assert_eq!(command_verb("bread.command.search.open"), Some("open"));
|
||||
assert_eq!(command_verb("bread.command.search.query"), Some("query"));
|
||||
assert_eq!(command_verb("bread.command.box.open"), None);
|
||||
assert_eq!(command_verb("bread.search.opened"), None);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +1,22 @@
|
|||
use bread_theme::{hex_to_rgba, ink_on, load_palette, Palette};
|
||||
use breadsearch_shared::{Hit, Request, Response};
|
||||
use std::{cell::RefCell, process::Command, rc::Rc, sync::mpsc};
|
||||
|
||||
use gtk4::{
|
||||
glib, pango::EllipsizeMode, prelude::*, Application, Box as GBox, CssProvider,
|
||||
EventControllerKey, Image, Label, ListBox, Orientation, PolicyType, ScrolledWindow,
|
||||
SearchEntry, SelectionMode,
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
env, fs,
|
||||
path::PathBuf,
|
||||
process::Command,
|
||||
rc::Rc,
|
||||
sync::mpsc,
|
||||
};
|
||||
|
||||
mod bread_events;
|
||||
mod listen;
|
||||
mod screenshot;
|
||||
use gtk4::{
|
||||
glib,
|
||||
pango::EllipsizeMode,
|
||||
prelude::*,
|
||||
Application, ApplicationWindow, Box as GBox, CssProvider, EventControllerKey, Image, Label,
|
||||
ListBox, Orientation, PolicyType, ScrolledWindow, SearchEntry, SelectionMode,
|
||||
};
|
||||
use gtk4_layer_shell::{Edge, KeyboardMode, Layer, LayerShell};
|
||||
|
||||
// ---- Theming ----------------------------------------------------------------
|
||||
|
||||
|
|
@ -41,6 +47,39 @@ fn build_css(p: &Palette) -> String {
|
|||
)
|
||||
}
|
||||
|
||||
// ---- PID file toggle --------------------------------------------------------
|
||||
|
||||
fn pid_file() -> PathBuf {
|
||||
env::var("XDG_RUNTIME_DIR")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|_| PathBuf::from("/tmp"))
|
||||
.join("breadsearch.pid")
|
||||
}
|
||||
|
||||
fn is_breadsearch_pid(pid: u32) -> bool {
|
||||
fs::read_to_string(format!("/proc/{}/comm", pid))
|
||||
.map(|s| s.trim() == "breadsearch")
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn toggle_or_continue() -> bool {
|
||||
let pf = pid_file();
|
||||
if let Ok(content) = fs::read_to_string(&pf) {
|
||||
if let Ok(pid) = content.trim().parse::<u32>() {
|
||||
if is_breadsearch_pid(pid) {
|
||||
let _ = Command::new("kill").arg(pid.to_string()).status();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
let _ = fs::write(&pf, std::process::id().to_string());
|
||||
true
|
||||
}
|
||||
|
||||
fn cleanup_pid() {
|
||||
let _ = fs::remove_file(pid_file());
|
||||
}
|
||||
|
||||
// ---- Row builder ------------------------------------------------------------
|
||||
|
||||
fn make_hit_row(hit: &Hit) -> gtk4::ListBoxRow {
|
||||
|
|
@ -152,7 +191,6 @@ fn open_file(path: &str) {
|
|||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.spawn();
|
||||
bread_events::emit_opened_result(path);
|
||||
}
|
||||
|
||||
fn open_folder(path: &str) {
|
||||
|
|
@ -166,22 +204,14 @@ fn open_folder(path: &str) {
|
|||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.spawn();
|
||||
bread_events::emit_opened_result(path);
|
||||
}
|
||||
|
||||
// ---- UI ---------------------------------------------------------------------
|
||||
|
||||
fn run_ui(screenshot_req: Option<screenshot::ScreenshotRequest>) {
|
||||
let mut builder = Application::builder().application_id("com.breadway.breadsearch");
|
||||
if screenshot_req.is_some() {
|
||||
// GApplication is single-instance by default; this machine typically
|
||||
// already has a real breadsearch instance, so without this a
|
||||
// screenshot run would just message the *existing* instance instead
|
||||
// of starting a fresh one that ever sees `screenshot_req`.
|
||||
builder = builder.flags(gtk4::gio::ApplicationFlags::NON_UNIQUE);
|
||||
}
|
||||
let app = builder.build();
|
||||
let is_screenshot_run = screenshot_req.is_some();
|
||||
fn run_ui() {
|
||||
let app = Application::builder()
|
||||
.application_id("com.breadway.breadsearch")
|
||||
.build();
|
||||
|
||||
let debounce_id: Rc<RefCell<Option<glib::SourceId>>> = Rc::new(RefCell::new(None));
|
||||
|
||||
|
|
@ -195,13 +225,20 @@ fn run_ui(screenshot_req: Option<screenshot::ScreenshotRequest>) {
|
|||
bread_theme::gtk::apply_user_css(&user_css_path, &user_cell);
|
||||
}
|
||||
|
||||
// Full-screen transparent overlay; panel widget is positioned inside it.
|
||||
let window = bread_utils::gtk_popup::new_overlay_window(app, "breadsearch");
|
||||
bread_theme::gtk::bind_window_auto(&window);
|
||||
let window = ApplicationWindow::builder().application(app).build();
|
||||
window.init_layer_shell();
|
||||
window.set_namespace(Some("breadsearch"));
|
||||
window.set_layer(Layer::Overlay);
|
||||
window.set_keyboard_mode(KeyboardMode::Exclusive);
|
||||
for edge in [Edge::Top, Edge::Bottom, Edge::Left, Edge::Right] {
|
||||
window.set_anchor(edge, true);
|
||||
}
|
||||
window.set_exclusive_zone(0);
|
||||
|
||||
let close_all: Rc<dyn Fn()> = Rc::new({
|
||||
let w = window.clone();
|
||||
move || {
|
||||
cleanup_pid();
|
||||
w.close();
|
||||
}
|
||||
});
|
||||
|
|
@ -257,10 +294,7 @@ fn run_ui(screenshot_req: Option<screenshot::ScreenshotRequest>) {
|
|||
let (tx, rx) = mpsc::sync_channel::<std::io::Result<Response>>(1);
|
||||
|
||||
std::thread::spawn(move || {
|
||||
let req = Request::Query {
|
||||
query: q,
|
||||
limit: 10,
|
||||
};
|
||||
let req = Request::Query { query: q, limit: 10 };
|
||||
let _ = tx.send(breadsearch_shared::send_request(&req));
|
||||
});
|
||||
|
||||
|
|
@ -320,11 +354,36 @@ fn run_ui(screenshot_req: Option<screenshot::ScreenshotRequest>) {
|
|||
glib::Propagation::Stop
|
||||
}
|
||||
Key::Down => {
|
||||
bread_utils::gtk_popup::select_next_visible(&list_k);
|
||||
let cur = list_k.selected_row().map(|r| r.index()).unwrap_or(-1);
|
||||
let mut i = cur + 1;
|
||||
loop {
|
||||
match list_k.row_at_index(i) {
|
||||
Some(r) if r.is_selectable() => {
|
||||
list_k.select_row(Some(&r));
|
||||
break;
|
||||
}
|
||||
Some(_) => i += 1,
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
glib::Propagation::Stop
|
||||
}
|
||||
Key::Up => {
|
||||
bread_utils::gtk_popup::select_prev_visible(&list_k);
|
||||
let cur = list_k.selected_row().map(|r| r.index()).unwrap_or(0);
|
||||
let mut i = cur - 1;
|
||||
loop {
|
||||
if i < 0 {
|
||||
break;
|
||||
}
|
||||
match list_k.row_at_index(i) {
|
||||
Some(r) if r.is_selectable() => {
|
||||
list_k.select_row(Some(&r));
|
||||
break;
|
||||
}
|
||||
Some(_) => i -= 1,
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
glib::Propagation::Stop
|
||||
}
|
||||
_ => glib::Propagation::Proceed,
|
||||
|
|
@ -342,65 +401,36 @@ fn run_ui(screenshot_req: Option<screenshot::ScreenshotRequest>) {
|
|||
});
|
||||
|
||||
// Click outside launcher panel → close
|
||||
{
|
||||
let close_outside = Rc::clone(&close_all);
|
||||
bread_utils::gtk_popup::close_on_outside_click(&window, &vbox, move || close_outside());
|
||||
let vbox_ref = vbox.clone();
|
||||
let win_ref = window.clone();
|
||||
let outside_click = gtk4::GestureClick::new();
|
||||
outside_click.connect_pressed(move |_, _, x, y| {
|
||||
if let Some(b) = vbox_ref.compute_bounds(&win_ref) {
|
||||
if x < b.x() as f64
|
||||
|| x > (b.x() + b.width()) as f64
|
||||
|| y < b.y() as f64
|
||||
|| y > (b.y() + b.height()) as f64
|
||||
{
|
||||
close_outside();
|
||||
}
|
||||
|
||||
if let Some(req) = screenshot_req.clone() {
|
||||
screenshot::dispatch(&window, req);
|
||||
}
|
||||
});
|
||||
window.add_controller(outside_click);
|
||||
|
||||
window.connect_map(|_| bread_events::emit_opened());
|
||||
window.connect_destroy(|_| cleanup_pid());
|
||||
window.present();
|
||||
search.grab_focus();
|
||||
});
|
||||
|
||||
if is_screenshot_run {
|
||||
// GLib's own option parser otherwise rejects --screenshot/--output
|
||||
// before clap ever sees them (`Cli::parse()` already ran in `main`,
|
||||
// over the real argv).
|
||||
app.run_with_args(&[] as &[&str]);
|
||||
} else {
|
||||
app.run();
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Main -------------------------------------------------------------------
|
||||
|
||||
fn main() {
|
||||
if std::env::args().nth(1).as_deref() == Some("listen") {
|
||||
listen::run();
|
||||
if !toggle_or_continue() {
|
||||
return;
|
||||
}
|
||||
|
||||
use clap::Parser;
|
||||
let cli = screenshot::Cli::parse();
|
||||
let screenshot_req = cli.screenshot_request();
|
||||
|
||||
// `toggle_or_kill` kills whatever's holding the single-instance lock —
|
||||
// a real, already-running breadsearch included. A screenshot run must
|
||||
// never touch it: it's a separate, disposable instance by design (same
|
||||
// reasoning as breadbar's `allow_multiple_instances`), not a toggle of
|
||||
// the operator's real search panel.
|
||||
//
|
||||
// Kept alive for the rest of `main` — dropping it releases the
|
||||
// single-instance lock and removes the pid file, which happens
|
||||
// naturally once `run_ui` returns (after the window closes).
|
||||
let _singleton_guard = if screenshot_req.is_some() {
|
||||
None
|
||||
} else {
|
||||
match bread_utils::singleton::toggle_or_kill("breadsearch") {
|
||||
Ok(bread_utils::singleton::Toggle::Started(guard)) => Some(guard),
|
||||
Ok(bread_utils::singleton::Toggle::KilledExisting) => return,
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"breadsearch: single-instance lock unavailable ({e}); continuing without it"
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
run_ui(screenshot_req);
|
||||
run_ui();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,93 +0,0 @@
|
|||
//! `--screenshot` CLI mode: render breadsearch's search panel, capture it
|
||||
//! via `bread-screenshots`, then exit — driven by `bread-ecosystem`'s
|
||||
//! `bread-capture` orchestrator, or run standalone for one-off captures.
|
||||
//!
|
||||
//! Same reasoning as breadbox: one view ("search"), full known-size canvas
|
||||
//! capture since the panel isn't its own layer surface. Captures whatever
|
||||
//! the panel shows at settle time — normally the "Type to search…" empty
|
||||
//! state, since there's no query to type in an automated run.
|
||||
|
||||
use bread_utils::screenshot_cli::{validate_pair, DEFAULT_HEIGHT, DEFAULT_WIDTH, SETTLE_DELAY};
|
||||
use clap::Parser;
|
||||
use gtk4::prelude::*;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "breadsearch")]
|
||||
pub struct Cli {
|
||||
/// Render the named view, capture it, then exit instead of running
|
||||
/// normally. Known views: "search".
|
||||
#[arg(long)]
|
||||
pub screenshot: Option<String>,
|
||||
|
||||
/// PNG path to write the capture to. Required together with --screenshot.
|
||||
#[arg(long)]
|
||||
pub output: Option<PathBuf>,
|
||||
|
||||
/// Capture canvas width — matches the isolated compositor's output width
|
||||
/// (`bread-capture --isolate-width`).
|
||||
#[arg(long, default_value_t = DEFAULT_WIDTH)]
|
||||
pub width: u32,
|
||||
|
||||
/// Capture canvas height — see `width`.
|
||||
#[arg(long, default_value_t = DEFAULT_HEIGHT)]
|
||||
pub height: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ScreenshotRequest {
|
||||
pub view: String,
|
||||
pub output: PathBuf,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
}
|
||||
|
||||
impl Cli {
|
||||
/// `None` for a normal run. Exits the process with an error if the
|
||||
/// `--screenshot` / `--output` pair is incomplete, before any GTK setup
|
||||
/// happens.
|
||||
pub fn screenshot_request(&self) -> Option<ScreenshotRequest> {
|
||||
if let Err(e) = validate_pair(self.screenshot.as_deref(), self.output.as_deref()) {
|
||||
eprintln!("breadsearch: {e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
Some(ScreenshotRequest {
|
||||
view: self.screenshot.clone()?,
|
||||
output: self.output.clone()?,
|
||||
width: self.width,
|
||||
height: self.height,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Wire up the given view's screenshot sequence against an already-built,
|
||||
/// not-yet-presented window. Every path here ends by exiting the process —
|
||||
/// it never returns control to the normal search UI.
|
||||
pub fn dispatch(window: >k4::ApplicationWindow, req: ScreenshotRequest) {
|
||||
match req.view.as_str() {
|
||||
"search" => {
|
||||
let output = req.output;
|
||||
let (width, height) = (req.width as i32, req.height as i32);
|
||||
window.connect_map(move |_| {
|
||||
let output = output.clone();
|
||||
gtk4::glib::timeout_add_local_once(SETTLE_DELAY, move || {
|
||||
finish(bread_screenshots::capture_region(0, 0, width, height, &output));
|
||||
});
|
||||
});
|
||||
}
|
||||
other => {
|
||||
eprintln!("breadsearch: unknown screenshot view '{other}' (known: search)");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn finish(result: anyhow::Result<()>) {
|
||||
match result {
|
||||
Ok(()) => std::process::exit(0),
|
||||
Err(e) => {
|
||||
eprintln!("breadsearch: screenshot capture failed: {e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
147cfbbf96ae4b171027defa1130d2caddb934b1
|
||||
21
ci/build.sh
21
ci/build.sh
|
|
@ -1,21 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# Delegates to bread-ecosystem's shared CI build image/script, pinned to
|
||||
# the commit in ci/bread-ecosystem.rev — not `main`. bread-ecosystem's CI
|
||||
# files now affect every product's release pipeline, so bumping the pin
|
||||
# is a deliberate act instead of silent drift (see the bread-theme test
|
||||
# that broke here for exactly that reason, before it was pinned by rev).
|
||||
#
|
||||
# Usage: ci/build.sh cargo build --release --locked
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
REV="$(cat "${ROOT}/ci/bread-ecosystem.rev")"
|
||||
|
||||
CACHE_DIR="/tmp/bread-ecosystem-ci-${REV}"
|
||||
if [ ! -d "$CACHE_DIR" ]; then
|
||||
rm -rf /tmp/bread-ecosystem-ci-*
|
||||
git clone https://git.breadway.dev/Breadway/bread-ecosystem.git "$CACHE_DIR"
|
||||
git -C "$CACHE_DIR" checkout --quiet "$REV"
|
||||
fi
|
||||
|
||||
bash "${CACHE_DIR}/ci/build.sh" breadsearch "$ROOT" "$@"
|
||||
Loading…
Add table
Add a link
Reference in a new issue