diff --git a/.forgejo/workflows/check.yml b/.forgejo/workflows/check.yml deleted file mode 100644 index b547c34..0000000 --- a/.forgejo/workflows/check.yml +++ /dev/null @@ -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 -- -D warnings - - - name: test - run: cd src && bash ci/build.sh cargo test --workspace --locked diff --git a/.forgejo/workflows/dev-release.yml b/.forgejo/workflows/dev-release.yml deleted file mode 100644 index 11f75d2..0000000 --- a/.forgejo/workflows/dev-release.yml +++ /dev/null @@ -1,67 +0,0 @@ -name: dev release - -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 - - - name: compute dev version - run: | - set -euo pipefail - cd src - 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' 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/breadpad/${VERSION}" - mkdir -p "${PKG_DIR}" - for bin in breadpad breadman; 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/breadpad.example.toml "${PKG_DIR}/" - cp src/LICENSE "${PKG_DIR}/" - cp src/bakery.toml "${PKG_DIR}/bakery.toml" - ln -sfn "${VERSION}" "/srv/breadway-dl/dev/breadpad/latest" - - - 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 - 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}" diff --git a/.forgejo/workflows/mirror.yml b/.forgejo/workflows/mirror.yml new file mode 100644 index 0000000..01b9bef --- /dev/null +++ b/.forgejo/workflows/mirror.yml @@ -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/breadpad.git" \ + '+refs/heads/*:refs/heads/*' '+refs/tags/*:refs/tags/*' diff --git a/.forgejo/workflows/package.yml b/.forgejo/workflows/package.yml new file mode 100644 index 0000000..fdfdef9 --- /dev/null +++ b/.forgejo/workflows/package.yml @@ -0,0 +1,40 @@ +name: Build and publish package + +on: + push: + tags: ['v*'] + +jobs: + package: + runs-on: [self-hosted, hestia] + container: + image: archlinux:latest + steps: + # Note: no actions/checkout — the archlinux image has no Node, which JS + # actions require. Everything runs as shell steps and clones manually. + - name: Build and publish + env: + PUBLISH_TOKEN: ${{ secrets.REGISTRY_TOKEN }} + run: | + set -euo pipefail + VERSION="${GITHUB_REF_NAME#v}" + pacman -Syu --noconfirm base-devel git rust cargo gtk4 gtk4-layer-shell + useradd -m builder + git config --global --add safe.directory '*' + git clone --branch "${GITHUB_REF_NAME}" --depth 1 \ + "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" /home/builder/src + cd /home/builder/src + git archive --format=tar.gz --prefix="breadpad-${VERSION}/" HEAD \ + > packaging/arch/breadpad-${VERSION}.tar.gz + SHA=$(sha256sum packaging/arch/breadpad-${VERSION}.tar.gz | awk '{print $1}') + sed -i "s/^pkgver=.*/pkgver=${VERSION}/" packaging/arch/PKGBUILD + sed -i "s/^sha256sums=.*/sha256sums=('${SHA}')/" packaging/arch/PKGBUILD + chown -R builder:builder /home/builder/src + # --nocheck: packaging builds the artifact; tests belong in a CI job. + su builder -c "cd /home/builder/src/packaging/arch && makepkg -f --noconfirm --nocheck" + PKG=$(find /home/builder/src/packaging/arch -name '*.pkg.tar.zst' | head -1) + curl -fsS -X PUT \ + -H "Authorization: token ${PUBLISH_TOKEN}" \ + -H "Content-Type: application/octet-stream" \ + --data-binary "@${PKG}" \ + "https://git.breadway.dev/api/packages/Breadway/arch/os" diff --git a/.forgejo/workflows/rc-release.yml b/.forgejo/workflows/rc-release.yml deleted file mode 100644 index aad2f82..0000000 --- a/.forgejo/workflows/rc-release.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: beta (rc) release - -on: - push: - tags: ['v*-rc.*'] - -jobs: - build: - 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 - - - name: prepare artifacts - run: | - set -euo pipefail - VERSION="${GITHUB_REF_NAME#v}" - PKG_DIR="/srv/breadway-dl/beta/breadpad/${VERSION}" - mkdir -p "${PKG_DIR}" - for bin in breadpad breadman; 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/breadpad.example.toml "${PKG_DIR}/" - cp src/LICENSE "${PKG_DIR}/" - cp src/bakery.toml "${PKG_DIR}/bakery.toml" - ln -sfn "${VERSION}" "/srv/breadway-dl/beta/breadpad/latest" - - - 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 - 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}" diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index bd7569b..c878cf4 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -6,7 +6,6 @@ on: jobs: build: - if: ${{ !contains(github.ref_name, '-rc.') }} runs-on: [self-hosted, hestia] steps: - name: checkout @@ -17,16 +16,7 @@ jobs: "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" src - name: build - 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 || { - 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 - name: prepare artifacts run: | @@ -41,19 +31,12 @@ jobs: > "${PKG_DIR}/${bin}-x86_64.sha256" done cp src/breadpad.example.toml "${PKG_DIR}/" - cp src/LICENSE "${PKG_DIR}/" cp src/bakery.toml "${PKG_DIR}/bakery.toml" ln -sfn "${VERSION}" "/srv/breadway-dl/breadpad/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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 8df4368..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,66 +0,0 @@ -name: release - -on: - push: - tags: ["v*"] - -permissions: - contents: write - -env: - DL_DIR: /srv/breadway-dl - ECOSYSTEM_DIR: /home/breadway/Projects/bread-ecosystem - -jobs: - build: - runs-on: [self-hosted, hestia] - steps: - - uses: actions/checkout@v4 - - - name: install build deps - run: sudo apt-get install -y libgtk-4-dev libdbus-1-dev pkg-config 2>/dev/null || true - - - name: build - run: cargo build --release --locked - - - name: prepare artifacts - run: | - VERSION="${GITHUB_REF_NAME#v}" - PKG_DIR="${DL_DIR}/breadpad/${VERSION}" - mkdir -p "${PKG_DIR}" - for bin in breadpad breadman; do - cp "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 breadpad.example.toml "${PKG_DIR}/" - cp bakery.toml "${PKG_DIR}/bakery.toml" - ln -sfn "${VERSION}" "${DL_DIR}/breadpad/latest" - - - name: ensure bread-ecosystem - run: | - if [[ -d "${ECOSYSTEM_DIR}/.git" ]]; then - git -C "${ECOSYSTEM_DIR}" pull --ff-only - else - mkdir -p "$(dirname "${ECOSYSTEM_DIR}")" - git clone https://github.com/Breadway/bread-ecosystem.git "${ECOSYSTEM_DIR}" - fi - - - name: regenerate index.json - run: bash "${ECOSYSTEM_DIR}/scripts/gen-index.sh" - - - name: upload to GitHub Release - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - VERSION="${GITHUB_REF_NAME#v}" - PKG_DIR="${DL_DIR}/breadpad/${VERSION}" - gh release create "${GITHUB_REF_NAME}" \ - --title "breadpad v${VERSION}" --generate-notes 2>/dev/null || true - gh release upload "${GITHUB_REF_NAME}" \ - "${PKG_DIR}/breadpad-x86_64" \ - "${PKG_DIR}/breadman-x86_64" \ - "${PKG_DIR}/breadpad-x86_64.sha256" \ - "${PKG_DIR}/breadman-x86_64.sha256" \ - --clobber diff --git a/.gitignore b/.gitignore index 3b0b041..9168541 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,3 @@ svgs.txt # Rust/Cargo Cargo.lock dist/ - -# Local knowledge-graph cache -graphify-out/ diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index dd2f2fd..0000000 --- a/AGENTS.md +++ /dev/null @@ -1,37 +0,0 @@ -# AGENTS.md — Repo hygiene - -Scope: this file covers *repo hygiene* — branching, remotes, CI, cleanup. It is not 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. - -## Remotes -- `origin` — Forgejo (`git.breadway.dev` via Hestia, SSH) — authoritative. -- `github` — GitHub mirror. Push both when publishing. - -## CI -- `check.yml` — clippy + test, triggers on push to `feature/**`/`fix/**`. -- `dev-release.yml` — triggers on push to `main`. -- `rc-release.yml` — triggers on `vX.Y.Z-rc.N` tag push. -- `release.yml` — triggers on any other `v*` tag push. - -All four run on a self-hosted runner (`hestia`) inside a pinned Arch -container — not the host's native environment. The Containerfile/build -script are shared across bread-ecosystem products and live in -`bread-ecosystem/ci/`; this repo's `ci/build.sh` clones that repo at the -sha in `ci/bread-ecosystem.rev` (deliberately pinned, not `main`) and -delegates to it. Nothing runs automatically on plain commits or PRs -beyond what's listed. - -## Don't -- Don't embed credentials in remote URLs — SSH or a credential helper only. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index ac30b30..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,97 +0,0 @@ -# Contributing - -`breadpad` — Quick-capture scratchpad and note viewer (breadman) with AI classification. - -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/ -fix/ -``` - -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.+`) 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 -cargo test --release --workspace -``` - -## CI - -- `check.yml` — clippy + test, triggered on push to `feature/**`/`fix/**`. - Fast-fail before anything reaches `main`. -- `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 of these build inside a pinned Arch Linux container on a self-hosted -runner — not the runner host's native environment. Arch's repos carry -current `gtk4`/`libadwaita`/`gtk4-layer-shell` as prebuilt packages, so -there's no from-source library build to go stale. The Containerfile and -build script are shared across bread-ecosystem GTK4 products, living in -`bread-ecosystem/ci/`; `ci/build.sh` here is a thin wrapper that clones -that repo at the commit pinned in `ci/bread-ecosystem.rev` (not `main` — -an unrelated change there shouldn't silently affect this repo's release -builds) and delegates to it. Bump the pin deliberately when you want the -shared image or build logic updated. A `ci/deps.txt` here (currently -absent — breadpad needs nothing beyond the shared base) would layer on -extra pacman packages if that ever changes. Nothing runs automatically on -plain commits or PRs beyond the jobs listed 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. diff --git a/Cargo.lock b/Cargo.lock index 4d85626..4936958 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -24,18 +24,18 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.5" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] [[package]] name = "android_system_properties" -version = "0.1.6" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" dependencies = [ "libc", ] @@ -92,9 +92,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.104" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "async-broadcast" @@ -175,7 +175,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn", ] [[package]] @@ -215,7 +215,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn", ] [[package]] @@ -226,13 +226,13 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.92" +version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn", ] [[package]] @@ -276,9 +276,9 @@ checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" [[package]] name = "bitflags" -version = "2.13.1" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" [[package]] name = "block-buffer" @@ -302,79 +302,27 @@ dependencies = [ "piper", ] -[[package]] -name = "bread-onnx" -version = "0.7.2" -source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.2#30517f161724132cdeb658c04cf5e490be07ee73" -dependencies = [ - "anyhow", - "bread-utils", - "hex", - "ort", - "sha2", - "tokenizers", - "tracing", - "ureq", -] - -[[package]] -name = "bread-screenshots" -version = "0.7.2" -source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.2#30517f161724132cdeb658c04cf5e490be07ee73" -dependencies = [ - "anyhow", - "bread-utils", - "tracing", -] - -[[package]] -name = "bread-shared" -version = "0.7.0" -source = "git+https://git.breadway.dev/Breadway/bread?tag=v0.7.0#22e34e2cf2202305d7960759dfccb54dc79f948b" -dependencies = [ - "dirs 5.0.1", - "serde", - "serde_json", - "toml 0.8.23", -] - [[package]] name = "bread-theme" -version = "0.7.4" -source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.4#fcba3760387e2523edb71350f8efea3bc851b21e" +version = "0.2.3" +source = "git+https://github.com/Breadway/bread-ecosystem?tag=v0.2.8#77417d552130281ff787e07d52541eb25e9d533b" dependencies = [ "dirs 5.0.1", "gtk4", - "libadwaita", - "serde", - "serde_json", -] - -[[package]] -name = "bread-utils" -version = "0.7.2" -source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.2#30517f161724132cdeb658c04cf5e490be07ee73" -dependencies = [ - "bread-shared", - "dirs 5.0.1", "serde", "serde_json", ] [[package]] name = "breadman" -version = "0.5.3" +version = "0.4.0" dependencies = [ "anyhow", - "bread-screenshots", - "bread-theme", - "bread-utils", "breadpad-shared", "chrono", "dirs 5.0.1", "futures-channel", "gtk4", - "libadwaita", "serde", "serde_json", "tokio", @@ -384,11 +332,9 @@ dependencies = [ [[package]] name = "breadpad" -version = "0.5.3" +version = "0.4.0" dependencies = [ "anyhow", - "bread-screenshots", - "bread-utils", "breadpad-shared", "chrono", "dirs 5.0.1", @@ -405,15 +351,14 @@ dependencies = [ [[package]] name = "breadpad-shared" -version = "0.5.3" +version = "0.4.0" dependencies = [ "anyhow", - "bread-onnx", "bread-theme", "chrono", "dirs 5.0.1", - "gtk4", "ical", + "ndarray 0.16.1", "ort", "regex", "reqwest", @@ -432,7 +377,7 @@ dependencies = [ [[package]] name = "breadpad-test" -version = "0.5.3" +version = "0.4.0" dependencies = [ "anyhow", "breadpad-shared", @@ -460,9 +405,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.12.1" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cairo-rs" @@ -484,7 +429,7 @@ checksum = "f8b4985713047f5faee02b8db6a6ef32bbb50269ff53c1aee716d1d195b76d54" dependencies = [ "glib-sys", "libc", - "system-deps 7.0.8", + "system-deps", ] [[package]] @@ -498,9 +443,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.4.3" +version = "1.2.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "509591b7bcd67f4ef775afad7662703b4935daaa6ec0e5605cfb1090b32a2b6d" +checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" dependencies = [ "find-msvc-tools", "shlex", @@ -524,20 +469,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" -version = "0.2.2" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" - -[[package]] -name = "chacha20" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" -dependencies = [ - "cfg-if", - "cpufeatures 0.3.0", - "rand_core 0.10.1", -] +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" @@ -577,9 +511,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.6.6" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", "clap_derive", @@ -587,9 +521,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.6.6" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -599,14 +533,14 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.6.4" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" dependencies = [ "heck", "proc-macro2", "quote", - "syn 3.0.3", + "syn", ] [[package]] @@ -679,18 +613,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "console" -version = "0.16.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fe5f465a4f6fee88fad41b85d990f84c835335e85b5d9e6e63e0d06d28cba7c" -dependencies = [ - "encode_unicode", - "libc", - "unicode-width", - "windows-sys 0.61.2", -] - [[package]] name = "convert_case" version = "0.10.0" @@ -715,15 +637,6 @@ dependencies = [ "libc", ] -[[package]] -name = "cpufeatures" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" -dependencies = [ - "libc", -] - [[package]] name = "crc32fast" version = "1.5.0" @@ -735,9 +648,9 @@ dependencies = [ [[package]] name = "crossbeam-deque" -version = "0.8.7" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -745,18 +658,18 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.20" +version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.22" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crossterm" @@ -791,12 +704,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "daachorse" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f55d7153ba3b507595872a3874803f07a8a81d1e888abed8e5db7da0597d6e2" - [[package]] name = "darling" version = "0.20.11" @@ -818,7 +725,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.119", + "syn", ] [[package]] @@ -829,7 +736,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", - "syn 2.0.119", + "syn", ] [[package]] @@ -859,7 +766,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.119", + "syn", ] [[package]] @@ -869,7 +776,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ "derive_builder_core", - "syn 2.0.119", + "syn", ] [[package]] @@ -891,7 +798,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.119", + "syn", "unicode-xid", ] @@ -949,13 +856,13 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.7" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn", ] [[package]] @@ -969,9 +876,9 @@ dependencies = [ [[package]] name = "either" -version = "1.17.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" [[package]] name = "encode_unicode" @@ -1003,7 +910,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn", ] [[package]] @@ -1027,16 +934,14 @@ name = "esaxx-rs" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d817e038c30374a4bcb22f94d0a8a0e216958d4c3dcde369b1439fec4bdda6e6" -dependencies = [ - "cc", -] [[package]] name = "event-listener" -version = "5.4.2" +version = "5.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" dependencies = [ + "concurrent-queue", "parking", "pin-project-lite", ] @@ -1053,9 +958,9 @@ dependencies = [ [[package]] name = "fancy-regex" -version = "0.17.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72cf461f865c862bb7dc573f643dd6a2b6842f7c30b07882b56bd148cc2761b8" +checksum = "6e24cb5a94bcae1e5408b0effca5cd7172ea3c5755049c5f3af4cd283a165298" dependencies = [ "bit-set", "regex-automata", @@ -1064,9 +969,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.5.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "field-offset" @@ -1080,9 +985,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.11" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "flate2" @@ -1100,6 +1005,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -1111,24 +1022,24 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.34" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", ] [[package]] name = "futures-core" -version = "0.3.34" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.34" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -1137,9 +1048,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.34" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-lite" @@ -1156,32 +1067,32 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.34" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn", ] [[package]] name = "futures-sink" -version = "0.3.34" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.34" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.34" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-core", "futures-macro", @@ -1213,14 +1124,14 @@ dependencies = [ "glib-sys", "gobject-sys", "libc", - "system-deps 7.0.8", + "system-deps", ] [[package]] name = "gdk4" -version = "0.11.4" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d81e2a6c6ecba2aab60633a98df1868b03fa0bfdce8105edc27c1bccf71f0e39" +checksum = "fd42fdbbf48612c6e8f47c65fb92d2e8f39c25aecd6af047e83897c1a22d2a4e" dependencies = [ "cairo-rs", "gdk-pixbuf", @@ -1234,9 +1145,9 @@ dependencies = [ [[package]] name = "gdk4-sys" -version = "0.11.4" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d8f608d8d7d229975c4d0d026f5d3071598c4ddab3c5262b0a31840fec78d13" +checksum = "9d974ac4f15e67472c3a9728daf612590b4a5762a4b33f0edd298df0b80d043c" dependencies = [ "cairo-sys-rs", "gdk-pixbuf-sys", @@ -1246,7 +1157,7 @@ dependencies = [ "libc", "pango-sys", "pkg-config", - "system-deps 7.0.8", + "system-deps", ] [[package]] @@ -1279,30 +1190,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi 5.3.0", "wasip2", -] - -[[package]] -name = "getrandom" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "r-efi 6.0.0", - "rand_core 0.10.1", "wasm-bindgen", ] [[package]] -name = "gio" -version = "0.22.8" +name = "getrandom" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b3e1f669909c326b9413bde5a742097b8c90a7d78f45326db13668984769ded" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "gio" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3848bcba3a35cc0a71df8ba8ecfd799d6bfb862342a53a4a915fb62213aa4e6" dependencies = [ "futures-channel", "futures-core", @@ -1317,14 +1229,14 @@ dependencies = [ [[package]] name = "gio-sys" -version = "0.22.8" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "353fdc7da7cd16da916104b1e0e4e7de380ec9c8aaa20d4d742d66310ab4b0d5" +checksum = "64729ba2772c080448f9f966dba8f4456beeb100d8c28a865ef8a0f2ef4987e1" dependencies = [ "glib-sys", "gobject-sys", "libc", - "system-deps 7.0.8", + "system-deps", "windows-sys 0.61.2", ] @@ -1350,9 +1262,9 @@ dependencies = [ [[package]] name = "glib" -version = "0.22.8" +version = "0.22.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddbcf514bd1881fc1b960e4e52b4e82873f4da3bceddbd58d42827b508888100" +checksum = "c207e04e51605dcf7b2924c41591b3a10e1438eaac5bcf448fb91f325381104a" dependencies = [ "bitflags", "futures-channel", @@ -1378,17 +1290,17 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.119", + "syn", ] [[package]] name = "glib-sys" -version = "0.22.8" +version = "0.22.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "030967459f9f676851872c6304adea7825c6d462ec9b72554c733cf0c5952233" +checksum = "5f7fbac234ed5bc2a28359b7bde8e1b9cdf1441cc2d7f068e4824672d7db9445" dependencies = [ "libc", - "system-deps 7.0.8", + "system-deps", ] [[package]] @@ -1399,35 +1311,37 @@ checksum = "22a861859b887a79cf461359c192c97a57d8fb0229dd291232e57aa11f6fa72c" dependencies = [ "glib-sys", "libc", - "system-deps 7.0.8", + "system-deps", ] [[package]] name = "graphene-rs" -version = "0.22.8" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb856b9c558971c3f13ab692358926da710b046932a4e087aedcc35b040d7dff" +checksum = "c7d1b7881f96869f49808b6adfe906a93a57a34204952253444d68c3208d71f1" dependencies = [ "glib", "graphene-sys", + "libc", ] [[package]] name = "graphene-sys" -version = "0.22.8" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c7ffdfde88f3570d3705e0d8a2433e036d387a1f2930bbf47eafcb5f569fd04" +checksum = "517f062f3fd6b7fd3e57a3f038a74b3c23ca32f51199ff028aa704609943f79c" dependencies = [ "glib-sys", "libc", - "system-deps 7.0.8", + "pkg-config", + "system-deps", ] [[package]] name = "gsk4" -version = "0.11.4" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b867be1c5f14dcb8f552c0eff6e9a9b1da5f8b43943e8efc3a63c889d84952ff" +checksum = "53c912dfcbd28acace5fc99c40bb9f25e1dcb73efb1f2608327f66a99acdcb62" dependencies = [ "cairo-rs", "gdk4", @@ -1440,9 +1354,9 @@ dependencies = [ [[package]] name = "gsk4-sys" -version = "0.11.4" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b7c7eb2e681ee896646cfb8872b431f24d09f53ba9283289d9b10caa6707088" +checksum = "d7d54bbc7a9d8b6ffe4f0c95eede15ccfb365c8bf521275abe6bcfb57b18fb8a" dependencies = [ "cairo-sys-rs", "gdk4-sys", @@ -1451,14 +1365,14 @@ dependencies = [ "graphene-sys", "libc", "pango-sys", - "system-deps 7.0.8", + "system-deps", ] [[package]] name = "gtk4" -version = "0.11.4" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98a0a0466484f64b07b5b8184d43fa46be78eb0b8e04ae4e179af31d770b76d9" +checksum = "7181b837f04cbe93f79441475f7a00560a92cba7a72e38cc1a68b6f8b78eaae2" dependencies = [ "cairo-rs", "field-offset", @@ -1477,9 +1391,9 @@ dependencies = [ [[package]] name = "gtk4-layer-shell" -version = "0.8.1" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17c28ea0f4676fdaaae7ff2413a24d0d35c8657424f84856c1103c73454c9da4" +checksum = "a4069987ff4793699511a251028cc336b438e46565b463f111250148d574752a" dependencies = [ "bitflags", "gdk4", @@ -1492,34 +1406,34 @@ dependencies = [ [[package]] name = "gtk4-layer-shell-sys" -version = "0.6.1" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcf19bb884ef0ef55b9e6b2b369c39b4fcc0c41e3a0c1cbc8c267720338b690b" +checksum = "8f566a5ec5bcc454e7fcf2ab76930887ced5365afce12c1e5201bb296b95f1b9" dependencies = [ "gdk4-sys", "glib-sys", "gtk4-sys", "libc", - "system-deps 8.0.0", + "system-deps", ] [[package]] name = "gtk4-macros" -version = "0.11.4" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ac7179400a36a04de039c24206bb841c5596992b907b43b23ee8d5bdc40d00e" +checksum = "3581b242ba62fdff122ebb626ea641582ec326031622bd19d60f85029c804a87" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.119", + "syn", ] [[package]] name = "gtk4-sys" -version = "0.11.4" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f954786af0b1984425c4446b77f5ff6594346181316be3f850caab1c6f01" +checksum = "20ba8e695e2640455561274e65e45f0a151619e450746007667f4b23ceae4e1b" dependencies = [ "cairo-sys-rs", "gdk-pixbuf-sys", @@ -1531,7 +1445,16 @@ dependencies = [ "gsk4-sys", "libc", "pango-sys", - "system-deps 7.0.8", + "system-deps", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", ] [[package]] @@ -1566,22 +1489,22 @@ checksum = "629d8f3bbeda9d148036d6b0de0a3ab947abd08ce90626327fc3547a49d59d97" dependencies = [ "dirs 6.0.0", "http", - "indicatif 0.17.11", + "indicatif", "libc", "log", - "rand 0.9.5", + "rand 0.9.4", "serde", "serde_json", - "thiserror 2.0.20", + "thiserror 2.0.18", "ureq", "windows-sys 0.60.2", ] [[package]] name = "http" -version = "1.5.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +checksum = "8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0" dependencies = [ "bytes", "itoa", @@ -1589,9 +1512,9 @@ dependencies = [ [[package]] name = "http-body" -version = "1.1.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", "http", @@ -1599,9 +1522,9 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.5" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", @@ -1618,9 +1541,9 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "hyper" -version = "1.11.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" dependencies = [ "atomic-waker", "bytes", @@ -1649,7 +1572,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots 1.0.9", + "webpki-roots 1.0.7", ] [[package]] @@ -1686,7 +1609,7 @@ dependencies = [ "either", "futures-lite", "hyprland-macros", - "pastey 0.1.1", + "pastey", "serde", "serde_json", "serde_repr", @@ -1701,7 +1624,7 @@ checksum = "31157e6ccefbad4b0cd7e549db6696691a70c11b108f26bf6bf76eef26af8c10" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn", ] [[package]] @@ -1739,9 +1662,9 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.3.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ "displaydoc", "potential_utf", @@ -1753,9 +1676,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.3.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", @@ -1766,9 +1689,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.3.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -1780,17 +1703,16 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.3.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" [[package]] name = "icu_properties" -version = "2.3.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" dependencies = [ - "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", @@ -1801,15 +1723,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.3.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] name = "icu_provider" -version = "2.3.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92a7ed671a6aad807a8651a2e1782a6598fda9ce5185dd8158549e95a91c6428" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", @@ -1820,6 +1742,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "ident_case" version = "1.0.1" @@ -1854,7 +1782,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.17.1", + "serde", + "serde_core", ] [[package]] @@ -1863,31 +1793,18 @@ version = "0.17.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235" dependencies = [ - "console 0.15.11", + "console", "number_prefix", "portable-atomic", "unicode-width", "web-time", ] -[[package]] -name = "indicatif" -version = "0.18.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9433806cd6b4ec1aba79c021c7e4c58fb4c3b9977c085062e611ac929998fb0c" -dependencies = [ - "console 0.16.4", - "portable-atomic", - "unicode-width", - "unit-prefix", - "web-time", -] - [[package]] name = "ipnet" -version = "2.12.1" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "is_terminal_polyfill" @@ -1912,12 +1829,13 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "js-sys" -version = "0.3.104" +version = "0.3.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" +checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" dependencies = [ "cfg-if", "futures-util", + "once_cell", "wasm-bindgen", ] @@ -1934,40 +1852,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] -name = "libadwaita" -version = "0.9.2" +name = "leb128fmt" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85b9900e67182a4b5b1f157b448d94f0715c8b9770cce21cf000801917f53bfa" -dependencies = [ - "gdk4", - "gio", - "glib", - "gtk4", - "libadwaita-sys", - "pango", -] - -[[package]] -name = "libadwaita-sys" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3c27642b389852aa99341bd4a4c19ec6f8a2b63ebdd7f5ba1952198079ccd" -dependencies = [ - "gdk4-sys", - "gio-sys", - "glib-sys", - "gobject-sys", - "gtk4-sys", - "libc", - "pango-sys", - "system-deps 7.0.8", -] +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.189" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libloading" @@ -1981,9 +1875,9 @@ dependencies = [ [[package]] name = "libredox" -version = "0.1.20" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d0a00925a9f930d679b6789b721e3a7f9ed110f41b86d2497caa780c3a070a" +checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" dependencies = [ "libc", ] @@ -1996,9 +1890,9 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.3" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "litrs" @@ -2017,9 +1911,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.33" +version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" [[package]] name = "lru-slab" @@ -2029,19 +1923,19 @@ checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" [[package]] name = "macro_rules_attribute" -version = "0.2.3" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3ae8f6d608c795738406608304d30a2dfbdc8e58e44f7ba43236da5208ded3c" +checksum = "65049d7923698040cd0b1ddcced9b0eb14dd22c5f86ae59c3740eab64a676520" dependencies = [ "macro_rules_attribute-proc_macro", - "pastey 0.2.3", + "paste", ] [[package]] name = "macro_rules_attribute-proc_macro" -version = "0.2.3" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc04a4c58212d57930a24bf47d3fa87485264a3a054e9c10e042eb373573ad3c" +checksum = "670fdfda89751bc4a84ac13eaa63e205cf0fd22b4c9a5fbfa085b63c1f1d3a30" [[package]] name = "matchers" @@ -2054,9 +1948,9 @@ dependencies = [ [[package]] name = "matrixmultiply" -version = "0.3.11" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f607c237553f086e7043417a51df26b2eb899d3caff94e6a67592ff992fedc7" +checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08" dependencies = [ "autocfg", "rawpointer", @@ -2064,9 +1958,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.8.3" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" [[package]] name = "memoffset" @@ -2095,9 +1989,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.2.2" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" dependencies = [ "libc", "wasi", @@ -2123,7 +2017,22 @@ checksum = "e4db6d5580af57bf992f59068d4ea26fd518574ff48d7639b255a36f9de6e7e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn", +] + +[[package]] +name = "ndarray" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "882ed72dce9365842bf196bdeedf5055305f11fc8c03dee7bb0194a6cad34841" +dependencies = [ + "matrixmultiply", + "num-complex", + "num-integer", + "num-traits", + "portable-atomic", + "portable-atomic-util", + "rawpointer", ] [[package]] @@ -2184,9 +2093,9 @@ dependencies = [ [[package]] name = "num-integer" -version = "0.1.47" +version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" dependencies = [ "num-traits", ] @@ -2218,28 +2127,6 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" -[[package]] -name = "onig" -version = "6.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc3cbf698f9438986c11a880c90a6d04b9de27575afd28bbf45b154b6c709e2" -dependencies = [ - "bitflags", - "libc", - "once_cell", - "onig_sys", -] - -[[package]] -name = "onig_sys" -version = "69.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e68317604e77e53b85896388e1a803c1d21b74c899ec9e5e1112db90735edd7" -dependencies = [ - "cc", - "pkg-config", -] - [[package]] name = "option-ext" version = "0.2.0" @@ -2258,12 +2145,12 @@ dependencies = [ [[package]] name = "ort" -version = "2.0.0-rc.13" +version = "2.0.0-rc.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4336a1e2b38848325241c72889086886004e589b7c74f335e60a8e8db5138a0b" +checksum = "d7de3af33d24a745ffb8fab904b13478438d1cd52868e6f17735ef6e1f8bf133" dependencies = [ "libloading", - "ndarray", + "ndarray 0.17.2", "ort-sys", "smallvec", "tracing", @@ -2271,18 +2158,19 @@ dependencies = [ [[package]] name = "ort-sys" -version = "2.0.0-rc.13" +version = "2.0.0-rc.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf211e3776eea6aec988552fa118dd746d70e1b1e5e244058d1c98015f3e5872" +checksum = "d7b497d21a8b6fbb4b5a544f8fadb77e801a09ae0add9e411d31c6f89e3c1e90" [[package]] name = "pango" -version = "0.22.8" +version = "0.22.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d800d8d0de2ad5d0fb046f5344dbaba14a003cf3dd27cc21d85893d35ea316c" +checksum = "251bdc6e6487b811be0e406a21e301e07e45c0aa8fa39e00c0c8e12a91752438" dependencies = [ "gio", "glib", + "libc", "pango-sys", ] @@ -2295,7 +2183,7 @@ dependencies = [ "glib-sys", "gobject-sys", "libc", - "system-deps 7.0.8", + "system-deps", ] [[package]] @@ -2348,12 +2236,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" -[[package]] -name = "pastey" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" - [[package]] name = "percent-encoding" version = "2.3.2" @@ -2386,7 +2268,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared", - "rand 0.8.7", + "rand 0.8.6", ] [[package]] @@ -2417,9 +2299,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.34" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "polling" @@ -2437,9 +2319,9 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.15.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-atomic-util" @@ -2452,9 +2334,9 @@ dependencies = [ [[package]] name = "potential_utf" -version = "0.1.6" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" dependencies = [ "zerovec", ] @@ -2468,29 +2350,39 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "proc-macro-crate" version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ - "toml_edit 0.25.13+spec-1.1.0", + "toml_edit 0.25.12+spec-1.1.0", ] [[package]] name = "proc-macro2" -version = "1.0.107" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "quinn" -version = "0.11.11" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" dependencies = [ "bytes", "cfg_aliases", @@ -2500,7 +2392,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2", - "thiserror 2.0.20", + "thiserror 2.0.18", "tokio", "tracing", "web-time", @@ -2508,21 +2400,20 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.16" +version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" dependencies = [ "bytes", - "getrandom 0.4.3", + "getrandom 0.3.4", "lru-slab", - "rand 0.10.2", - "rand_pcg", + "rand 0.9.4", "ring", "rustc-hash", "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.20", + "thiserror 2.0.18", "tinyvec", "tracing", "web-time", @@ -2530,23 +2421,23 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.15" +version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" dependencies = [ "cfg_aliases", "libc", "once_cell", "socket2", "tracing", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] name = "quote" -version = "1.0.47" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -2565,9 +2456,9 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.8.7" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", "rand_chacha 0.3.1", @@ -2576,25 +2467,14 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.5" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.5", ] -[[package]] -name = "rand" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" -dependencies = [ - "chacha20", - "getrandom 0.4.3", - "rand_core 0.10.1", -] - [[package]] name = "rand_chacha" version = "0.3.1" @@ -2633,21 +2513,6 @@ dependencies = [ "getrandom 0.3.4", ] -[[package]] -name = "rand_core" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" - -[[package]] -name = "rand_pcg" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" -dependencies = [ - "rand_core 0.10.1", -] - [[package]] name = "rawpointer" version = "0.2.1" @@ -2713,14 +2578,14 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.17", "libredox", - "thiserror 2.0.20", + "thiserror 2.0.18", ] [[package]] name = "regex" -version = "1.13.1" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -2730,9 +2595,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.18" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -2741,9 +2606,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.11" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "reqwest" @@ -2780,7 +2645,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 1.0.9", + "webpki-roots 1.0.7", ] [[package]] @@ -2813,9 +2678,9 @@ dependencies = [ [[package]] name = "rustc-hash" -version = "2.1.3" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" [[package]] name = "rustc_version" @@ -2841,9 +2706,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.43" +version = "0.23.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ "log", "once_cell", @@ -2856,9 +2721,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.15.1" +version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" dependencies = [ "web-time", "zeroize", @@ -2866,9 +2731,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.14" +version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0527518605e68109d875e248ea259b6758801cf165e4b2c2733ae3b51f12535a" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ "ring", "rustls-pki-types", @@ -2877,9 +2742,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.23" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" @@ -2901,9 +2766,9 @@ checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" -version = "1.0.229" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", "serde_derive", @@ -2911,29 +2776,29 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.229" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.229" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn", ] [[package]] name = "serde_json" -version = "1.0.151" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "itoa", "memchr", @@ -2944,13 +2809,13 @@ dependencies = [ [[package]] name = "serde_repr" -version = "0.1.21" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn", ] [[package]] @@ -2985,23 +2850,12 @@ dependencies = [ [[package]] name = "sha1" -version = "0.10.7" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures 0.2.17", - "digest", -] - -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", + "cpufeatures", "digest", ] @@ -3032,9 +2886,9 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.10" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" [[package]] name = "siphasher" @@ -3050,15 +2904,15 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.2" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.6.5" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", "windows-sys 0.61.2", @@ -3113,20 +2967,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.119" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "3.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -3150,7 +2993,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn", ] [[package]] @@ -3162,20 +3005,7 @@ dependencies = [ "cfg-expr", "heck", "pkg-config", - "toml 1.1.4+spec-1.1.0", - "version-compare", -] - -[[package]] -name = "system-deps" -version = "8.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83779a5c956bcb6ba627a4ecf0a9d7625db47d7537e0892d97f712ac995648a3" -dependencies = [ - "cfg-expr", - "heck", - "pkg-config", - "toml 1.1.4+spec-1.1.0", + "toml 1.1.2+spec-1.1.0", "version-compare", ] @@ -3192,7 +3022,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.3", + "getrandom 0.4.2", "once_cell", "rustix", "windows-sys 0.61.2", @@ -3209,11 +3039,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.20" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.20", + "thiserror-impl 2.0.18", ] [[package]] @@ -3224,34 +3054,34 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn", ] [[package]] name = "thiserror-impl" -version = "2.0.20" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn", ] [[package]] name = "thread_local" -version = "1.1.10" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" dependencies = [ "cfg-if", ] [[package]] name = "tinystr" -version = "0.8.4" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", "zerovec", @@ -3259,9 +3089,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.12.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" dependencies = [ "tinyvec_macros", ] @@ -3274,27 +3104,25 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokenizers" -version = "0.23.1" +version = "0.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44e5bea67576e04b6ff8564c5d9e09c2ef0cf476502245f2f120e497769d3112" +checksum = "a620b996116a59e184c2fa2dfd8251ea34a36d0a514758c6f966386bd2e03476" dependencies = [ "ahash", + "aho-corasick", "compact_str", - "daachorse", "dary_heap", "derive_builder", "esaxx-rs", "fancy-regex", "getrandom 0.3.4", "hf-hub", - "indicatif 0.18.6", "itertools", "log", "macro_rules_attribute", "monostate", - "onig", "paste", - "rand 0.9.5", + "rand 0.9.4", "rayon", "rayon-cond", "regex", @@ -3302,7 +3130,7 @@ dependencies = [ "serde", "serde_json", "spm_precompiled", - "thiserror 2.0.20", + "thiserror 2.0.18", "unicode-normalization-alignments", "unicode-segmentation", "unicode_categories", @@ -3310,9 +3138,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.53.1" +version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ "bytes", "libc", @@ -3328,13 +3156,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.7.2" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn", ] [[package]] @@ -3361,9 +3189,9 @@ dependencies = [ [[package]] name = "toml" -version = "1.1.4+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" dependencies = [ "indexmap", "serde_core", @@ -3371,7 +3199,7 @@ dependencies = [ "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", "toml_writer", - "winnow 1.0.4", + "winnow 1.0.3", ] [[package]] @@ -3408,23 +3236,23 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.25.13+spec-1.1.0" +version = "0.25.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" dependencies = [ "indexmap", "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", - "winnow 1.0.4", + "winnow 1.0.3", ] [[package]] name = "toml_parser" -version = "1.1.3+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "winnow 1.0.4", + "winnow 1.0.3", ] [[package]] @@ -3435,9 +3263,9 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] name = "toml_writer" -version = "1.1.2+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" [[package]] name = "tower" @@ -3503,7 +3331,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn", ] [[package]] @@ -3607,12 +3435,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" -[[package]] -name = "unit-prefix" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3" - [[package]] name = "untrusted" version = "0.9.0" @@ -3664,11 +3486,11 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.24.1" +version = "1.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cefc03fd367c0c6d4305de1b312cf00248c4114f4a0418ce6a6af769e3b0bd9" +checksum = "d258b83ceec21034727ecee8c382cfa6c3e133699b0742c64571814fb420c9f7" dependencies = [ - "getrandom 0.4.3", + "getrandom 0.4.2", "js-sys", "wasm-bindgen", ] @@ -3708,18 +3530,27 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.4+wasi-0.2.12" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", ] [[package]] name = "wasm-bindgen" -version = "0.2.127" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" +checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" dependencies = [ "cfg-if", "once_cell", @@ -3730,9 +3561,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.77" +version = "0.4.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b7777d5cc23d0e91404e53ce2d5e8ec7acae3026b16233dba62cd3246457950" +checksum = "9473dbd2991ae90b6291c3c32c30c6187ac49aa32f9905d1cce280ec1e110b0f" dependencies = [ "js-sys", "wasm-bindgen", @@ -3740,9 +3571,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.127" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" +checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3750,31 +3581,65 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.127" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" +checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.119", + "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.127" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" +checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" dependencies = [ "unicode-ident", ] [[package]] -name = "web-sys" -version = "0.3.104" +name = "wasm-encoder" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436" dependencies = [ "js-sys", "wasm-bindgen", @@ -3796,14 +3661,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.9", + "webpki-roots 1.0.7", ] [[package]] name = "webpki-roots" -version = "1.0.9" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" dependencies = [ "rustls-pki-types", ] @@ -3851,7 +3716,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn", ] [[package]] @@ -3862,7 +3727,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn", ] [[package]] @@ -4131,13 +3996,22 @@ dependencies = [ [[package]] name = "winnow" -version = "1.0.4" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" dependencies = [ "memchr", ] +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + [[package]] name = "wit-bindgen" version = "0.57.1" @@ -4145,10 +4019,89 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" [[package]] -name = "writeable" -version = "0.6.4" +name = "wit-bindgen-core" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "xdg-home" @@ -4162,9 +4115,9 @@ dependencies = [ [[package]] name = "xml-rs" -version = "0.8.29" +version = "0.8.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7" +checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" [[package]] name = "yoke" @@ -4185,7 +4138,7 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn", "synstructure", ] @@ -4207,7 +4160,7 @@ dependencies = [ "hex", "nix", "ordered-stream", - "rand 0.8.7", + "rand 0.8.6", "serde", "serde_repr", "sha1", @@ -4231,7 +4184,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.119", + "syn", "zvariant_utils", ] @@ -4248,22 +4201,22 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.56" +version = "0.8.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" +checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.56" +version = "0.8.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" +checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn", ] [[package]] @@ -4283,21 +4236,21 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn", "synstructure", ] [[package]] name = "zeroize" -version = "1.9.0" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" [[package]] name = "zerotrie" -version = "0.2.5" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", "yoke", @@ -4306,9 +4259,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.7" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94b5c6b5976d66c1d703c4fd17d3f5e43c8cedaacf604961b171adc7130896d8" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ "yoke", "zerofrom", @@ -4317,20 +4270,20 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.4" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47402523226a02bfe5230160dc3ccc089aa6f6f19e7fcbb4e6f824bbb1b4aa62" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn", ] [[package]] name = "zmij" -version = "1.0.23" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" [[package]] name = "zvariant" @@ -4354,7 +4307,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.119", + "syn", "zvariant_utils", ] @@ -4366,5 +4319,5 @@ checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn", ] diff --git a/Cargo.toml b/Cargo.toml index b9508be..1de5996 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ members = [ resolver = "2" [workspace.package] -version = "0.5.3" +version = "0.4.0" edition = "2021" license = "MIT" authors = ["Breadway"] @@ -24,10 +24,9 @@ chrono = { version = "0.4", features = ["serde"] } rrule = "0.12" tokio = { version = "1", features = ["full"] } zbus = { version = "4", default-features = false, features = ["tokio"] } -# WHY: bread-onnx's Provider enum references every EP type, so those ort -# features must be on in the consumer even if breadpad only requests MIGraphX. -ort = { version = "2.0.0-rc.12", default-features = false, features = ["std", "tracing", "api-24", "migraphx", "cuda", "openvino", "vitis", "load-dynamic"] } -tokenizers = { version = "0.23", default-features = false, features = ["http", "fancy-regex"] } +ort = { version = "2.0.0-rc.12", default-features = false, features = ["std", "ndarray", "tracing", "api-24", "rocm", "load-dynamic"] } +ndarray = "0.16" +tokenizers = { version = "0.21", default-features = false, features = ["http", "fancy-regex"] } gtk4 = { version = "0.11", features = ["v4_12"] } gtk4-layer-shell = "0.8" hyprland = "0.4.0-beta.3" diff --git a/EVENTS.md b/EVENTS.md deleted file mode 100644 index 428599c..0000000 --- a/EVENTS.md +++ /dev/null @@ -1,70 +0,0 @@ -# breadpad — bread event integration - -breadpad is a standalone capture popup: it works exactly the same with or -without `breadd` running. When breadd *is* present, the `breadpad` binary -publishes events into the shared bread automation fabric after actions that -already happened. 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: **`pad`**. Transport: `bread-utils`'s `bread_client` module -(feature `bread-client`) — the capture popup links it directly. One-shot -popup / `fire ` invocations each `emit` on their own short-lived -connection. Command verbs are only received while `breadpad listen` is -running — that process holds the `bread.command.pad.**` subscription -open. - -`breadman` (the viewer) does not emit or subscribe. Notes created or edited -there are not quick-capture, and it is not on the reminder-fire path. - -## Events published (`bread.pad.*`) - -| Event | Data | When | -|-------|------|------| -| `bread.pad.captured` | `{ "id": "" }` | The capture popup saved a note successfully (`Store::save_note` returned `Ok`). Not emitted when the field is empty, the window is dismissed, classification-only preview happens, or the write fails. | -| `bread.pad.reminder.due` | `{ "id": "" }` | `breadpad fire ` decided the reminder is due (`Scheduler::fire` returned true) and is about to show the reminder window. This is the existing in-process systemd-timer hook (`breadpad-reminder-.timer` → `breadpad fire `), not a new daemon. Not emitted when the note is missing, the fire is outside the missed-grace window, or the reminder window is opened as a `--screenshot` sample. | -| `bread.pad.capture.done` | `{}` | `bread.command.pad.capture` was received and `breadpad` was spawned. This is the command confirmation, not proof the popup mapped — the spawned process is the same no-args invocation as the capture keybind. | -| `bread.pad.capture.failed` | `{ "error": "" }` | `bread.command.pad.capture` was received but this binary could not be started. | - -Note bodies are never included in the payload — only the local note id. -Notes stay in `~/.local/share/breadpad/notes.jsonl`; the event bus is for -*notifications about* capture and due reminders, not a channel for note -content. - -## Commands honored (`bread.command.pad.*`) - -These are only received while `breadpad listen` is running. Publishing a -command with no subscriber is a silent no-op — that is the documented -bread convention, not a breadpad bug. - -| Verb | Data | Effect | -|------|------|--------| -| `capture` | none | Same as running `breadpad` with no args: open the capture popup. Emits `bread.pad.capture.done` / `.failed`. | - -```lua -bread.spawn(function() - bread.emit("bread.command.pad.capture") - bread.wait("bread.pad.capture.done", { timeout = 5000 }) -end) -``` - -### Not implemented: extra verbs - -There is no `snooze` / `done` / `fire` command verb. Reminder fire -already exists as `breadpad fire ` (systemd user timer), and -viewing/editing lives in `breadman`. If/when a bus verb maps to real -extra behavior, add it then — do not stub one 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. Capture, save, - systemd timers, and the reminder window are entirely unaffected. -- If breadd restarts, the command subscription reconnects automatically - (`BreadClient::subscribe`'s background thread has its own backoff - loop); no restart of `breadpad listen` is needed. -- If `breadpad listen` is not running, commands are a graceful no-op at - the bus (no subscriber). One-shot capture / `fire` still emit - `bread.pad.captured` / `bread.pad.reminder.due` on their own - short-lived connection. diff --git a/README.md b/README.md index e9f4498..6d41ee8 100644 --- a/README.md +++ b/README.md @@ -32,11 +32,11 @@ breadman GTK4 note viewer / manager ### Classification -Every note passes through a three-tier pipeline at capture time. **Tier 1 is the only tier that ships with breadpad** — the ONNX classifier model is not bundled, and Ollama is optional. Capture works without either. +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. This is the default path; no model files required. -2. **Small local ONNX model (optional, not shipped)** — runs when Tier 1 can't confidently assign a type *and* you have dropped in your own classifier. Responsible for type classification only; Tier 1's extracted time, recurrence rule, and cleaned body are always preserved. -3. **Large local model via Ollama (optional)** — 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 previous tier's result is used. No cloud APIs are involved. +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. @@ -56,7 +56,7 @@ User-defined tags can be added freely on top of the built-in types. - **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 +- **Snooze** — notification popup includes snooze actions drawn from `snooze_options` (default: 15 min / 1 hour / tomorrow morning); 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`) @@ -104,11 +104,11 @@ Always runs. Handles: Returns a calibrated confidence. If ≥ 0.82, Tiers 2 and 3 are skipped. -#### Tier 2 — Small local ONNX model (optional, not shipped) +#### Tier 2 — Small local ONNX model -Runs when Tier 1 confidence is below threshold **and** a compatible `classifier.onnx` + `tokenizer.json` are present. Responsible for **type classification only** — Tier 1's extracted time, recurrence rule, and cleaned body are always preserved. +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`. Without a model file or runtime library, Tier 2 is skipped and Tier 1 (plus optional Tier 3) still works. +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 @@ -123,7 +123,7 @@ If Ollama is unreachable or returns an invalid response, breadpad logs a warning ~/.local/share/breadpad/model/tokenizer.json ``` -**breadpad does not ship a classifier model.** Tier 1 rules work with no extra files. If you want Tier 2, drop a compatible ONNX classifier and `tokenizer.json` at those paths and point `model.ort_dylib_path` at your ONNX Runtime library. +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 @@ -138,27 +138,23 @@ breadpad model-info # shows active EP and model path - D-Bus session bus (for notifications) - systemd user session (for timer-backed reminders) - Rust 1.80+ -- **Tier 2 (ONNX classifier, optional):** A model you supply yourself (`classifier.onnx` + `tokenizer.json`) and an external `libonnxruntime.so`. Set `model.ort_dylib_path` in `breadpad.toml`, or set `ORT_DYLIB_PATH` in your environment. Neither the model nor the runtime is shipped. Without them, Tier 2 is disabled; Tier 1 (and Tier 3, if Ollama is running) 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. +- **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 (e.g. `ollama pull fastflowlm`). Tier 3 is silently skipped if Ollama is not running. --- ## Installation ```bash -git clone https://git.breadway.dev/Breadway/breadpad +git clone https://github.com/breadway/breadpad cd breadpad cargo build --release cp target/release/breadpad ~/.local/bin/ cp target/release/breadman ~/.local/bin/ -``` -That's enough for capture + viewing. Tier 1 classification works out of the box. The ONNX classifier is **not** included — only add it if you want Tier 2: - -```bash +# Place your ONNX classifier and tokenizer in the model directory mkdir -p ~/.local/share/breadpad/model -# Drop your own classifier.onnx + tokenizer.json in that directory, then -# set model.ort_dylib_path in breadpad.toml to your libonnxruntime.so +# Then set model.ort_dylib_path in breadpad.toml to your libonnxruntime.so ``` On Arch Linux, install GTK4 dependencies first: @@ -187,7 +183,7 @@ ort_dylib_path = "" # optional: explicit path to libonnxruntime.so; [model.ollama] endpoint = "http://localhost:11434" -model = "llama3.2:3b" # any model you have pulled in Ollama +model = "fastflowlm" # 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 @@ -247,12 +243,15 @@ breadpad --no-classify # Show model and storage status breadpad --status + +# Print expected model paths (does not download automatically) +breadpad download-model ``` -Hyprland keybind (BOS default is Super+U; bind whatever you want): +Hyprland keybind: ``` -bind = $mainMod, U, exec, breadpad +bind = $mainMod, N, exec, breadpad ``` ### breadman (viewer) diff --git a/bakery.toml b/bakery.toml index f7a339e..25e9033 100644 --- a/bakery.toml +++ b/bakery.toml @@ -4,7 +4,6 @@ binaries = ["breadpad", "breadman"] system_deps = ["gtk4", "gtk4-layer-shell"] optional_system_deps = ["rocm-hip-runtime", "ollama", "hyprland"] bread_deps = [] -license_file = "LICENSE" [config] dir = "~/.config/breadpad" diff --git a/breadman/Cargo.toml b/breadman/Cargo.toml index d47a586..577f684 100644 --- a/breadman/Cargo.toml +++ b/breadman/Cargo.toml @@ -11,13 +11,6 @@ path = "src/main.rs" [dependencies] breadpad-shared = { path = "../breadpad-shared" } -# Capture primitives for `--screenshot` mode — see src/screenshot.rs. -bread-screenshots = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.2" } -# Shared `--screenshot` pair validation + settle delay (`screenshot_cli`). -bread-utils = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.2" } -# `adw` implies `gtk` (`chip`, `set_chip_active`, `adw::init`). -bread-theme = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.4", features = ["adw"] } -libadwaita = { version = "0.9", features = ["v1_7"] } anyhow.workspace = true tracing.workspace = true tracing-subscriber.workspace = true diff --git a/breadman/src/editor.rs b/breadman/src/editor.rs index e39b11c..a01dfa5 100644 --- a/breadman/src/editor.rs +++ b/breadman/src/editor.rs @@ -1,8 +1,3 @@ -//! Note editor, presented as an AdwDialog (was a bare GtkPopover with no -//! scrim, no title, anchored wherever the triggering button happened to be - -//! flagged in design review as the weakest surface in the app). AdwDialog -//! gives us the scrim, the title, and correct modal anchoring for free. - use breadpad_shared::{ parser::parse_rule_based, scheduler::Scheduler, @@ -11,73 +6,48 @@ use breadpad_shared::{ }; use chrono::{Local, TimeZone, Utc}; use gtk4::{glib, prelude::*}; -use libadwaita::prelude::*; use std::cell::RefCell; use std::rc::Rc; use std::sync::Arc; -/// Same wording used by `main::show_add_note_window`'s New Note dialog - the -/// two surfaces used to teach the user two different input languages for -/// the same fields. -pub const TIME_PLACEHOLDER: &str = "tomorrow 9am / at 7pm / 2026-08-01 09:00"; -pub const RRULE_PLACEHOLDER: &str = "RRULE:FREQ=WEEKLY;BYDAY=MO"; - -/// Builds the dialog but does not present it - callers that need to hook -/// its `map` signal (screenshot mode) must connect before presenting or -/// they miss the signal entirely; interactive callers present immediately -/// with `dialog.present(Some(parent))`. -pub fn open_editor( +pub fn build_editor_popover( note: &Note, store: Arc, morning: String, on_save: Rc, on_delete: Rc, on_error: Rc, -) -> libadwaita::Dialog { - let dialog = libadwaita::Dialog::builder() - .title("Edit Note") - .content_width(480) - .content_height(520) - .build(); +) -> gtk4::Popover { + let popover = gtk4::Popover::new(); + popover.set_has_arrow(false); - let header = libadwaita::HeaderBar::new(); - let toolbar_view = libadwaita::ToolbarView::new(); - toolbar_view.add_top_bar(&header); - - let content = gtk4::Box::builder() + let vbox = gtk4::Box::builder() .orientation(gtk4::Orientation::Vertical) - .spacing(16) - .margin_top(16) - .margin_bottom(16) - .margin_start(16) - .margin_end(16) + .spacing(8) + .margin_top(12) + .margin_bottom(12) + .margin_start(12) + .margin_end(12) + .width_request(420) .build(); - let group = libadwaita::PreferencesGroup::builder().title("Details").build(); + vbox.append(>k4::Label::builder().label("Body").xalign(0.0).build()); + let body_entry = gtk4::Entry::builder() + .text(¬e.body) + .hexpand(true) + .build(); + vbox.append(&body_entry); - let body_row = libadwaita::EntryRow::builder().title("Body").build(); - body_row.set_text(¬e.body); - group.add(&body_row); - - let type_row = libadwaita::ActionRow::builder().title("Type").build(); - let type_pill_box = gtk4::Box::builder().orientation(gtk4::Orientation::Horizontal).spacing(4).valign(gtk4::Align::Center).build(); - let selected_type: Rc> = Rc::new(RefCell::new(note.note_type.as_str().to_string())); - let type_pills: Vec<(gtk4::Button, &'static str)> = NoteType::all_builtin().iter().map(|&name| (bread_theme::gtk::chip(name), name)).collect(); - for (btn, name) in &type_pills { - bread_theme::gtk::set_chip_active(btn, *name == selected_type.borrow().as_str()); - let sel = selected_type.clone(); - let name = *name; - let all_btns: Vec = type_pills.iter().map(|(b, _)| b.clone()).collect(); - btn.connect_clicked(move |clicked| { - *sel.borrow_mut() = name.to_string(); - for b in &all_btns { bread_theme::gtk::set_chip_active(b, false); } - bread_theme::gtk::set_chip_active(clicked, true); - }); - type_pill_box.append(btn); - } - type_row.add_suffix(&type_pill_box); - group.add(&type_row); + vbox.append(>k4::Label::builder().label("Type").xalign(0.0).build()); + let type_combo = gtk4::DropDown::from_strings(NoteType::all_builtin()); + let current_idx = NoteType::all_builtin() + .iter() + .position(|&s| s == note.note_type.as_str()) + .unwrap_or(3) as u32; + type_combo.set_selected(current_idx); + vbox.append(&type_combo); + vbox.append(>k4::Label::builder().label("Time").xalign(0.0).build()); let time_text = note .time .map(|t| { @@ -85,44 +55,38 @@ pub fn open_editor( local.format("%Y-%m-%d %H:%M").to_string() }) .unwrap_or_default(); - let time_row = libadwaita::EntryRow::builder().title("Time").build(); - time_row.set_text(&time_text); - // EntryRow has no placeholder-text property of its own (unlike GtkEntry) - - // the title already communicates the field, so the example format goes in - // the group description instead of a placeholder that would otherwise - // vanish behind the title when empty. - group.add(&time_row); - - let rrule_row = libadwaita::EntryRow::builder().title("Recurrence").build(); - rrule_row.set_text(note.rrule.as_ref().map(|r| r.as_str()).unwrap_or("")); - group.add(&rrule_row); - - content.append(&group); - - let hint = gtk4::Label::builder() - .label(format!("Time: {TIME_PLACEHOLDER}\nRecurrence: {RRULE_PLACEHOLDER}")) - .css_classes(["dim-label"]) - .xalign(0.0) - .wrap(true) + let time_entry = gtk4::Entry::builder() + .text(&time_text) + .placeholder_text("YYYY-MM-DD HH:MM or tomorrow 9am (blank = no time)") + .hexpand(true) .build(); - content.append(&hint); + vbox.append(&time_entry); - let btn_row = gtk4::Box::builder().orientation(gtk4::Orientation::Horizontal).spacing(8).build(); - let delete_btn = gtk4::Button::builder().label("Delete").css_classes(["destructive-action"]).build(); - let save_btn = gtk4::Button::builder().label("Save").css_classes(["confirm-button"]).hexpand(true).build(); + vbox.append(>k4::Label::builder().label("Recurrence").xalign(0.0).build()); + let rrule_entry = gtk4::Entry::builder() + .text(note.rrule.as_ref().map(|r| r.as_str()).unwrap_or("")) + .placeholder_text("RRULE:FREQ=WEEKLY;BYDAY=MO (blank = none)") + .build(); + vbox.append(&rrule_entry); + + // Button row: [Delete] [Save] + let btn_row = gtk4::Box::builder() + .orientation(gtk4::Orientation::Horizontal) + .spacing(8) + .build(); + + let delete_btn = gtk4::Button::builder() + .label("🗑 Delete") + .css_classes(["danger-btn"]) + .build(); + let save_btn = gtk4::Button::builder() + .label("Save") + .css_classes(["confirm-button"]) + .hexpand(true) + .build(); btn_row.append(&delete_btn); btn_row.append(&save_btn); - content.append(&btn_row); - - let scroll = gtk4::ScrolledWindow::builder() - .hscrollbar_policy(gtk4::PolicyType::Never) - .vscrollbar_policy(gtk4::PolicyType::Automatic) - .vexpand(true) - .min_content_height(400) - .build(); - scroll.set_child(Some(&content)); - toolbar_view.set_content(Some(&scroll)); - dialog.set_child(Some(&toolbar_view)); + vbox.append(&btn_row); // Delete: two-click confirm let confirming = Rc::new(RefCell::new(false)); @@ -131,7 +95,7 @@ pub fn open_editor( let delete_btn_label = delete_btn.clone(); let note_id = note.id.clone(); let store_del = store.clone(); - let dialog_del = dialog.clone(); + let popover_del = popover.clone(); let on_delete = Rc::clone(&on_delete); let on_error = Rc::clone(&on_error); @@ -141,7 +105,7 @@ pub fn open_editor( let id = note_id.clone(); let on_delete = Rc::clone(&on_delete); let on_error = Rc::clone(&on_error); - let dialog = dialog_del.clone(); + let popover = popover_del.clone(); spawn_bg( move || -> anyhow::Result<()> { store.delete_note(&id)?; @@ -155,7 +119,7 @@ pub fn open_editor( Ok(()) => on_delete(), Err(e) => on_error(format!("delete failed: {}", e)), } - dialog.close(); + popover.popdown(); }, ); } else { @@ -168,20 +132,33 @@ pub fn open_editor( // Save { let note_clone = note.clone(); - let dialog_save = dialog.clone(); + let popover_save = popover.clone(); let on_error = Rc::clone(&on_error); - let selected_type = selected_type.clone(); save_btn.connect_clicked(move |_| { + // Read all field values on the main thread before handing off. let mut updated = note_clone.clone(); - updated.body = body_row.text().to_string(); - updated.note_type = NoteType::from_str(&selected_type.borrow()); - let time_str = time_row.text().to_string(); - updated.time = if time_str.trim().is_empty() { None } else { parse_time_field(&time_str, &morning) }; - let rrule_text = rrule_row.text().to_string(); - updated.rrule = if rrule_text.trim().is_empty() { None } else { Some(RecurrenceRule::new(rrule_text)) }; + updated.body = body_entry.text().to_string(); + updated.note_type = NoteType::from_str( + NoteType::all_builtin() + .get(type_combo.selected() as usize) + .copied() + .unwrap_or("note"), + ); + let time_str = time_entry.text().to_string(); + updated.time = if time_str.trim().is_empty() { + None + } else { + parse_time_field(&time_str, &morning) + }; + let rrule_text = rrule_entry.text().to_string(); + updated.rrule = if rrule_text.trim().is_empty() { + None + } else { + Some(RecurrenceRule::new(rrule_text)) + }; - dialog_save.close(); + popover_save.popdown(); let store_bg = store.clone(); let on_save = Rc::clone(&on_save); @@ -205,7 +182,8 @@ pub fn open_editor( }); } - dialog + popover.set_child(Some(&vbox)); + popover } fn spawn_bg(work: F, then: C) diff --git a/breadman/src/main.rs b/breadman/src/main.rs index efd5b3f..e132a80 100644 --- a/breadman/src/main.rs +++ b/breadman/src/main.rs @@ -6,52 +6,23 @@ use breadpad_shared::{ store::Store, types::{Note, NoteType, RecurrenceRule}, }; -use chrono::Local; +use chrono::{DateTime, Local, Utc}; use gtk4::{glib, prelude::*}; use std::cell::RefCell; use std::rc::Rc; use std::sync::Arc; mod editor; -mod screenshot; mod views; // ── Args ───────────────────────────────────────────────────────────────────── mod args { - use bread_utils::screenshot_cli::{validate_pair, DEFAULT_HEIGHT, DEFAULT_WIDTH}; - use std::path::Path; - #[derive(Debug)] pub struct Args { pub view: Option, pub done_id: Option, pub upcoming_plain: bool, - pub screenshot: Option, - pub output: Option, - pub width: u32, - pub height: u32, - } - - impl Args { - /// `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 { - if let Err(e) = validate_pair( - self.screenshot.as_deref(), - self.output.as_deref().map(Path::new), - ) { - eprintln!("breadman: {e}"); - std::process::exit(1); - } - Some(crate::screenshot::ScreenshotRequest { - view: self.screenshot.clone()?, - output: self.output.clone()?.into(), - width: self.width, - height: self.height, - }) - } } pub fn parse() -> Args { @@ -59,10 +30,6 @@ mod args { view: None, done_id: None, upcoming_plain: false, - screenshot: None, - output: None, - width: DEFAULT_WIDTH, - height: DEFAULT_HEIGHT, }; let raw: Vec = std::env::args().skip(1).collect(); let mut i = 0; @@ -83,26 +50,6 @@ mod args { } args.view = Some("upcoming".into()); } - "--screenshot" => { - i += 1; - args.screenshot = raw.get(i).cloned(); - } - "--output" => { - i += 1; - args.output = raw.get(i).cloned(); - } - "--width" => { - i += 1; - if let Some(v) = raw.get(i).and_then(|s| s.parse().ok()) { - args.width = v; - } - } - "--height" => { - i += 1; - if let Some(v) = raw.get(i).and_then(|s| s.parse().ok()) { - args.height = v; - } - } _ => {} } i += 1; @@ -113,22 +60,30 @@ mod args { // ── AppState ────────────────────────────────────────────────────────────────── -type ErrorLog = Rc, String)>>>; - /// Shared UI state, cheap to clone (all fields are Rc/Arc). #[derive(Clone)] struct AppState { store: Arc, notes: Rc>>, cfg: Rc>, - errors: ErrorLog, + errors: Rc, String)>>>, active_view: Rc>, stack: gtk4::Stack, - window: gtk4::ApplicationWindow, + /// Sidebar row id ("all", "upcoming", "archive", or a note type name) -> + /// its count `Label`, so counts can be refreshed in place after every + /// `rebuild_stack` without rebuilding the sidebar itself. + sidebar_counts: Rc>>, + status_label: gtk4::Label, } impl AppState { - fn new(store: Arc, notes: Vec, cfg: Config, stack: gtk4::Stack, window: gtk4::ApplicationWindow) -> Self { + fn new( + store: Arc, + notes: Vec, + cfg: Config, + stack: gtk4::Stack, + status_label: gtk4::Label, + ) -> Self { AppState { store, notes: Rc::new(RefCell::new(notes)), @@ -136,7 +91,8 @@ impl AppState { errors: Rc::new(RefCell::new(Vec::new())), active_view: Rc::new(RefCell::new("all".to_string())), stack, - window, + sidebar_counts: Rc::new(RefCell::new(Vec::new())), + status_label, } } @@ -202,7 +158,7 @@ fn rebuild_all_view(notes: &[Note], state: &AppState) { if let Some(child) = state.stack.child_by_name("all") { state.stack.remove(&child); } - let scroll = build_note_list(notes, state.clone(), true, "No notes yet — jot something down to get started.", Some(NoteType::Note)); + let scroll = build_note_list(notes, state.clone()); state.stack.add_named(&scroll, Some("all")); } @@ -216,22 +172,14 @@ fn rebuild_stack(state: &AppState) { let errors: Vec<_> = state.errors.borrow().clone(); // All - let all_scroll = build_note_list(¬es, state.clone(), true, "No notes yet — jot something down to get started.", Some(NoteType::Note)); + let all_scroll = build_note_list(¬es, state.clone()); state.stack.add_named(&all_scroll, Some("all")); // Upcoming - let upcoming = views::upcoming::build(¬es, state.clone()); + let upcoming = views::upcoming::build(¬es); state.stack.add_named(&upcoming, Some("upcoming")); // Per-type - let empty_text = |type_name: &str| match type_name { - "todo" => "No todos yet.", - "reminder" => "No reminders yet.", - "idea" => "No ideas captured yet.", - "note" => "No notes yet.", - "question" => "No open questions yet.", - _ => "Nothing here yet.", - }; for type_name in NoteType::all_builtin() { let nt = NoteType::from_str(type_name); let filtered: Vec = notes @@ -239,7 +187,7 @@ fn rebuild_stack(state: &AppState) { .filter(|n| n.note_type == nt && !n.done) .cloned() .collect(); - let scroll = build_note_list(&filtered, state.clone(), false, empty_text(type_name), Some(nt)); + let scroll = build_note_list(&filtered, state.clone()); state.stack.add_named(&scroll, Some(type_name)); } @@ -257,6 +205,40 @@ fn rebuild_stack(state: &AppState) { // Errors let errors_view = views::errors::build(&errors); state.stack.add_named(&errors_view, Some("errors")); + + update_counts_and_status(state); +} + +/// Refreshes the sidebar's per-row counts and the content pane's footer +/// note count from the current `state.notes`. Cheap enough to call on every +/// rebuild — five type counts plus all/upcoming/archive over a note list +/// that in practice stays small. +fn update_counts_and_status(state: &AppState) { + let notes = state.notes.borrow(); + let total = notes.iter().filter(|n| !n.done).count(); + state + .status_label + .set_label(&format!("{total} note{}", if total == 1 { "" } else { "s" })); + + for (key, label) in state.sidebar_counts.borrow().iter() { + let n = match key.as_str() { + "all" => total, + "upcoming" => notes + .iter() + .filter(|n| { + !n.done + && matches!(n.note_type, NoteType::Reminder | NoteType::Todo) + && n.effective_time().is_some() + }) + .count(), + "archive" => notes.iter().filter(|n| n.done).count(), + other => { + let nt = NoteType::from_str(other); + notes.iter().filter(|n| !n.done && n.note_type == nt).count() + } + }; + label.set_label(&n.to_string()); + } } // ── main ───────────────────────────────────────────────────────────────────── @@ -279,9 +261,7 @@ fn main() -> Result<()> { return cmd_upcoming_plain(); } - let screenshot_req = args.screenshot_request(); - let view = screenshot_req.as_ref().map(|r| r.view.clone()).or(args.view); - run_app(view, cfg, screenshot_req) + run_app(args.view, cfg) } fn cmd_done(id: &str) -> Result<()> { @@ -317,20 +297,10 @@ fn cmd_upcoming_plain() -> Result<()> { Ok(()) } -fn run_app( - initial_view: Option, - cfg: Config, - screenshot_req: Option, -) -> Result<()> { - let mut builder = gtk4::Application::builder().application_id("com.breadway.breadman"); - if screenshot_req.is_some() { - // GApplication is single-instance by default; this machine typically - // already has a real breadman instance, so without this a - // screenshot run would activate 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(); +fn run_app(initial_view: Option, cfg: Config) -> Result<()> { + let app = gtk4::Application::builder() + .application_id("com.breadway.breadman") + .build(); let cfg = Arc::new(cfg); let initial_view = Arc::new(initial_view); @@ -338,7 +308,7 @@ fn run_app( app.connect_activate(move |app| { let cfg = cfg.as_ref().clone(); let initial_view = initial_view.as_deref().map(|s| s.to_string()); - if let Err(e) = build_app_window(app, cfg, initial_view, screenshot_req.clone()) { + if let Err(e) = build_app_window(app, cfg, initial_view) { tracing::error!("failed to build window: {}", e); } }); @@ -356,13 +326,8 @@ fn build_app_window( app: >k4::Application, cfg: Config, initial_view: Option, - screenshot_req: Option, ) -> Result<()> { apply_css(&cfg); - // Needed once before constructing any adw:: widget (see views::settings) — - // also forces dark mode, since bread-theme's palette is a fixed dark base - // regardless of the system GTK preference. - bread_theme::adw::init(); let store = Arc::new(Store::new()?); let notes = store.load_all()?; @@ -373,7 +338,6 @@ fn build_app_window( .default_width(960) .default_height(640) .build(); - bread_theme::gtk::bind_window_auto(&window); let hbox = gtk4::Box::builder() .orientation(gtk4::Orientation::Horizontal) @@ -414,11 +378,12 @@ fn build_app_window( )); row }; - // One icon language throughout (was 8 full-colour emoji + 2 thin - // monochrome glyphs for Settings/Errors + a third style on row action - // buttons) — every sidebar entry and row action now uses a real GTK - // symbolic icon. - let make_item = |id: &str, icon_name: &str, label: &str| { + // Returns the row plus its count `Label` when `counted` is set — callers + // collect these so counts can be kept live from `AppState.sidebar_counts` + // (monochrome geometric icons + a colored dot per type instead of the + // old full-color emoji, which always render in fixed colors no matter + // what the pywal palette says). + let make_item = |id: &str, icon: &str, icon_class: Option<&str>, label: &str, counted: bool| { let row = gtk4::ListBoxRow::builder() .css_classes(["sidebar-row"]) .build(); @@ -427,7 +392,15 @@ fn build_app_window( .orientation(gtk4::Orientation::Horizontal) .spacing(10) .build(); - hbox.append(>k4::Image::builder().icon_name(icon_name).pixel_size(16).build()); + let icon_label = gtk4::Label::builder() + .label(icon) + .width_chars(2) + .xalign(0.5) + .build(); + if let Some(class) = icon_class { + icon_label.add_css_class(class); + } + hbox.append(&icon_label); hbox.append( >k4::Label::builder() .label(label) @@ -435,23 +408,62 @@ fn build_app_window( .hexpand(true) .build(), ); + let count_label = if counted { + let l = gtk4::Label::builder() + .label("") + .css_classes(["sidebar-count"]) + .build(); + hbox.append(&l); + Some(l) + } else { + None + }; row.set_child(Some(&hbox)); - row + (row, count_label) }; + let mut sidebar_counts: Vec<(String, gtk4::Label)> = Vec::new(); + sidebar_list.append(&make_section("VIEWS")); - sidebar_list.append(&make_item("all", "view-list-symbolic", "All")); - sidebar_list.append(&make_item("upcoming", "x-office-calendar-symbolic", "Upcoming")); + { + let (row, count) = make_item("all", "▦", None, "All", true); + sidebar_counts.push(("all".into(), count.unwrap())); + sidebar_list.append(&row); + } + { + let (row, count) = make_item("upcoming", "◷", None, "Upcoming", true); + sidebar_counts.push(("upcoming".into(), count.unwrap())); + sidebar_list.append(&row); + } sidebar_list.append(&make_section("TYPES")); - sidebar_list.append(&make_item("todo", "task-due-symbolic", "Todo")); - sidebar_list.append(&make_item("reminder", "appointment-soon-symbolic", "Reminder")); - sidebar_list.append(&make_item("idea", "emblem-important-symbolic", "Idea")); - sidebar_list.append(&make_item("note", "text-x-generic-symbolic", "Note")); - sidebar_list.append(&make_item("question", "dialog-question-symbolic", "Question")); + for (id, icon_class, label) in [ + ("todo", "icon-todo", "Todo"), + ("reminder", "icon-reminder", "Reminder"), + ("idea", "icon-idea", "Idea"), + ("note", "icon-note", "Note"), + ("question", "icon-question", "Question"), + ] { + let (row, count) = make_item(id, "●", Some(icon_class), label, true); + sidebar_counts.push((id.to_string(), count.unwrap())); + sidebar_list.append(&row); + } sidebar_list.append(&make_section("MORE")); - sidebar_list.append(&make_item("archive", "folder-symbolic", "Archive")); - sidebar_list.append(&make_item("settings", "preferences-system-symbolic", "Settings")); - sidebar_list.append(&make_item("errors", "dialog-warning-symbolic", "Errors")); + { + let (row, count) = make_item("archive", "▢", None, "Archive", true); + sidebar_counts.push(("archive".into(), count.unwrap())); + sidebar_list.append(&row); + } + { + let (row, _) = make_item("settings", "⚙", None, "Settings", false); + sidebar_list.append(&row); + } + { + // De-emphasized: session-only debug info, not part of the daily + // triage flow the rest of the sidebar serves. + let (row, _) = make_item("errors", "⚠", None, "Errors", false); + row.add_css_class("sidebar-row-minor"); + sidebar_list.append(&row); + } sidebar_vbox.append(&sidebar_list); // ── Content area ────────────────────────────────────────────── @@ -471,8 +483,19 @@ fn build_app_window( let stack = gtk4::Stack::builder().hexpand(true).vexpand(true).build(); + let status_label = gtk4::Label::builder() + .label("0 notes") + .css_classes(["dim-label"]) + .xalign(0.0) + .margin_start(12) + .margin_end(12) + .margin_top(6) + .margin_bottom(8) + .build(); + content_vbox.append(&search_entry); content_vbox.append(&stack); + content_vbox.append(&status_label); hbox.append(&sidebar_vbox); hbox.append(>k4::Separator::builder() @@ -482,7 +505,8 @@ fn build_app_window( window.set_child(Some(&hbox)); // ── AppState ────────────────────────────────────────────────── - let state = AppState::new(store, notes, cfg, stack.clone(), window.clone()); + let state = AppState::new(store, notes, cfg, stack.clone(), status_label.clone()); + state.sidebar_counts.replace(sidebar_counts); // Initial build rebuild_stack(&state); @@ -490,15 +514,11 @@ fn build_app_window( // ── Sidebar selection ───────────────────────────────────────── { let state_c = state.clone(); - let search_entry_c = search_entry.clone(); sidebar_list.connect_row_selected(move |_, row| { if let Some(row) = row { let view = row.widget_name().to_string(); if view.is_empty() { return; } *state_c.active_view.borrow_mut() = view.clone(); - // The search bar only means anything on note-list views — - // it used to render (uselessly) on Settings and Errors too. - search_entry_c.set_visible(!matches!(view.as_str(), "settings" | "errors")); refresh(&state_c); } }); @@ -530,14 +550,13 @@ fn build_app_window( let state_c = state.clone(); let window_c = window.clone(); new_note_btn.connect_clicked(move |_| { - show_add_note_window(&window_c, state_c.clone(), NoteType::Note, |_| {}); + show_add_note_window(&window_c, state_c.clone()); }); } // ── Select initial view ─────────────────────────────────────── let initial = initial_view.as_deref().unwrap_or("all"); *state.active_view.borrow_mut() = initial.to_string(); - search_entry.set_visible(!matches!(initial, "settings" | "errors")); for row in sidebar_list .observe_children() .snapshot() @@ -551,55 +570,59 @@ fn build_app_window( } stack.set_visible_child_name(initial); - if let Some(req) = screenshot_req { - screenshot::dispatch(&window, req, state.clone()); - } - window.present(); Ok(()) } -// ── Note list ───────────────────────────────────────────────────────────────── -// Row rendering itself lives in views::row (shared with Upcoming/Archive) — -// design review found the old two-line card here wasted enormous horizontal -// space (title and actions separated by ~800-1000px of dead middle) compared -// to Archive's tighter single-line layout, so all list views now share one -// row template. +// ── Note list & cards ───────────────────────────────────────────────────────── -fn build_note_list( - notes: &[Note], - state: AppState, - show_type_badge: bool, - empty_text: &str, - empty_new_type: Option, -) -> gtk4::ScrolledWindow { +fn build_note_list(notes: &[Note], state: AppState) -> gtk4::ScrolledWindow { let scroll = gtk4::ScrolledWindow::builder() .hscrollbar_policy(gtk4::PolicyType::Never) .vscrollbar_policy(gtk4::PolicyType::Automatic) .vexpand(true) .build(); + // Capped and centered so cards don't stretch edge-to-edge on a wide + // window — full-width rows left the type chip and action buttons + // hundreds of pixels from the title they belong to. let list = gtk4::Box::builder() .orientation(gtk4::Orientation::Vertical) - .spacing(4) - .margin_top(8) - .margin_bottom(8) + .spacing(8) + .margin_top(12) + .margin_bottom(12) + .margin_start(12) + .margin_end(12) + .width_request(700) + .halign(gtk4::Align::Center) .build(); let mut sorted: Vec = notes.iter().filter(|n| !n.done).cloned().collect(); - sorted.sort_by_key(|n| std::cmp::Reverse(n.created)); + sorted.sort_by(|a, b| b.created.cmp(&a.created)); if sorted.is_empty() { - let action = empty_new_type.map(|nt| views::row::new_note_action(nt, state.window.clone(), state.clone())); - list.append(&views::row::build_empty_state("view-list-symbolic", empty_text, action)); + let empty = gtk4::Box::builder() + .orientation(gtk4::Orientation::Vertical) + .spacing(6) + .halign(gtk4::Align::Center) + .margin_top(64) + .build(); + empty.append( + >k4::Label::builder() + .label("No notes here yet") + .css_classes(["note-title"]) + .build(), + ); + empty.append( + >k4::Label::builder() + .label("Capture something with breadpad and it'll show up here.") + .css_classes(["dim-label"]) + .build(), + ); + list.append(&empty); } else { for note in &sorted { - let created_str = { - let local: chrono::DateTime = note.created.into(); - local.format("%b %d %H:%M").to_string() - }; - let spec = views::row::RowSpec { date_label: created_str, note, show_type_badge, show_done: true }; - list.append(&views::row::build(spec, state.clone())); + list.append(&build_note_card(note, state.clone())); } } @@ -607,16 +630,260 @@ fn build_note_list( scroll } +/// Short relative form for a recency-ordered list ("2h ago"); the exact +/// absolute timestamp is still available via tooltip. Falls back to a plain +/// date once a note is more than a week old, where "Nd ago" stops being +/// useful at a glance. +fn humanize_relative(dt: DateTime) -> String { + let secs = Utc::now().signed_duration_since(dt).num_seconds().max(0); + if secs < 60 { + "just now".to_string() + } else if secs < 3600 { + format!("{}m ago", secs / 60) + } else if secs < 86_400 { + format!("{}h ago", secs / 3600) + } else if secs < 86_400 * 7 { + format!("{}d ago", secs / 86_400) + } else { + let local: DateTime = dt.into(); + local.format("%b %d").to_string() + } +} + +fn build_note_card(note: &Note, state: AppState) -> gtk4::Box { + let card = gtk4::Box::builder() + .orientation(gtk4::Orientation::Vertical) + .spacing(8) + .margin_start(0) + .margin_end(0) + .margin_top(0) + .margin_bottom(0) + .css_classes(["note-card"]) + .build(); + card.add_css_class(&format!("note-card-{}", note.note_type.as_str())); + + // Top row: body + type chip + let top_row = gtk4::Box::builder() + .orientation(gtk4::Orientation::Horizontal) + .spacing(8) + .build(); + + let body_label = gtk4::Label::builder() + .label(¬e.body) + .hexpand(true) + .xalign(0.0) + .wrap(true) + .css_classes(["note-title"]) + .build(); + + let type_chip = gtk4::Label::builder() + .label(note.note_type.as_str()) + .css_classes(["type-chip"]) + .build(); + + top_row.append(&body_label); + top_row.append(&type_chip); + + // Bottom row: metadata + action buttons + let bottom_row = gtk4::Box::builder() + .orientation(gtk4::Orientation::Horizontal) + .spacing(8) + .build(); + + let created_abs = { + let local: chrono::DateTime = note.created.into(); + local.format("%b %d %H:%M").to_string() + }; + let meta_label = gtk4::Label::builder() + .label(&humanize_relative(note.created)) + .css_classes(["dim-label"]) + .xalign(0.0) + .tooltip_text(&created_abs) + .build(); + + // Date first, then chips + bottom_row.append(&meta_label); + if let Some(ws) = ¬e.workspace { + bottom_row.append( + >k4::Label::builder() + .label(&format!("ws {}", ws)) + .css_classes(["type-chip"]) + .tooltip_text(&format!("Workspace {}", ws)) + .build(), + ); + } + if let Some(t) = note.time { + let local: chrono::DateTime = t.into(); + bottom_row.append( + >k4::Label::builder() + .label(&local.format("⏰ %b %d %H:%M").to_string()) + .css_classes(["dim-label"]) + .build(), + ); + } + if note.rrule.is_some() { + bottom_row.append( + >k4::Label::builder() + .label("↻") + .css_classes(["type-chip"]) + .build(), + ); + } + + bottom_row.append(>k4::Box::builder().hexpand(true).build()); + + // ✓ Done button + let done_btn = gtk4::Button::builder() + .label("✓") + .css_classes(["action-btn", "done-btn"]) + .tooltip_text("Mark done") + .build(); + { + let note_id = note.id.clone(); + let card_c = card.clone(); + let state_c = state.clone(); + done_btn.connect_clicked(move |_| { + card_c.set_visible(false); // optimistic hide + let store = state_c.write_store(); + let id = note_id.clone(); + let state = state_c.clone(); + spawn_bg( + move || -> anyhow::Result> { + if let Some(mut n) = store.get_by_id(&id)? { + n.mark_done(); + store.update_note(&n)?; + } + store.load_all() + }, + move |result| { + match result { + Ok(fresh) => { + *state.notes.borrow_mut() = fresh; + rebuild_stack(&state); + let active = state.active_view.borrow().clone(); + state.stack.set_visible_child_name(&active); + } + Err(e) => state.log_error(format!("mark done failed: {}", e)), + } + }, + ); + }); + } + bottom_row.append(&done_btn); + + // ✎ Edit button + let edit_btn = gtk4::Button::builder() + .label("✎") + .css_classes(["action-btn", "edit-btn"]) + .tooltip_text("Edit") + .build(); + { + let note_c = note.clone(); + let state_c = state.clone(); + let body_label_c = body_label.clone(); + let card_c = card.clone(); + + edit_btn.connect_clicked(move |btn| { + let morning = state_c.cfg.borrow().reminders.default_morning.clone(); + let store = Arc::new(state_c.write_store()); + + let state_save = state_c.clone(); + let body_label_save = body_label_c.clone(); + let state_del = state_c.clone(); + let card_del = card_c.clone(); + let state_err = state_c.clone(); + + let popover = editor::build_editor_popover( + ¬e_c, + store, + morning, + Rc::new(move |updated: Note| { + body_label_save.set_label(&updated.body); + state_save.reload_notes(); + rebuild_stack(&state_save); + let active = state_save.active_view.borrow().clone(); + state_save.stack.set_visible_child_name(&active); + }), + Rc::new(move || { + card_del.set_visible(false); + state_del.reload_notes(); + rebuild_stack(&state_del); + let active = state_del.active_view.borrow().clone(); + state_del.stack.set_visible_child_name(&active); + }), + Rc::new(move |e: String| { + state_err.log_error(e); + }), + ); + popover.set_parent(btn); + popover.popup(); + }); + } + bottom_row.append(&edit_btn); + + // 🗑 Delete button — two-click confirm: first click → "Sure?", second → delete + let delete_btn = gtk4::Button::builder() + .label("🗑") + .css_classes(["action-btn", "danger-btn"]) + .tooltip_text("Delete") + .build(); + { + use std::cell::RefCell; + use std::rc::Rc; + let confirming = Rc::new(RefCell::new(false)); + let note_id = note.id.clone(); + let card_c = card.clone(); + let state_c = state.clone(); + let btn_c = delete_btn.clone(); + + delete_btn.connect_clicked(move |_| { + if *confirming.borrow() { + card_c.set_visible(false); // optimistic hide + let store = state_c.write_store(); + let id = note_id.clone(); + let state = state_c.clone(); + spawn_bg( + move || -> anyhow::Result> { + store.delete_note(&id)?; + if let Err(e) = Scheduler::cancel(&id) { + tracing::warn!("failed to cancel timer for {}: {}", id, e); + } + store.load_all() + }, + move |result| { + match result { + Ok(fresh) => { + *state.notes.borrow_mut() = fresh; + rebuild_stack(&state); + let active = state.active_view.borrow().clone(); + state.stack.set_visible_child_name(&active); + } + Err(e) => state.log_error(format!("delete failed: {}", e)), + } + }, + ); + } else { + *confirming.borrow_mut() = true; + btn_c.set_label("Sure?"); + } + }); + } + bottom_row.append(&delete_btn); + + card.append(&top_row); + card.append(&bottom_row); + card +} + // ── Add note window ─────────────────────────────────────────────────────────── -fn show_add_note_window(parent: >k4::ApplicationWindow, state: AppState, preselect: NoteType, on_build: impl FnOnce(>k4::Window)) { +fn show_add_note_window(parent: >k4::ApplicationWindow, state: AppState) { let win = gtk4::Window::builder() .title("New Note") .transient_for(parent) .modal(true) .default_width(500) .build(); - bread_theme::gtk::bind_window_auto(&win); let vbox = gtk4::Box::builder() .orientation(gtk4::Orientation::Vertical) @@ -634,42 +901,48 @@ fn show_add_note_window(parent: >k4::ApplicationWindow, state: AppState, prese .build(); vbox.append(&body_entry); - // Type pills — same chip widget the editor dialog and settings screen - // use, instead of three different type-picker widgets across the app. - vbox.append(>k4::Label::builder().label("Type").xalign(0.0).build()); + // Type chips let chip_box = gtk4::Box::builder() .orientation(gtk4::Orientation::Horizontal) .spacing(4) .build(); - let selected_type: Rc> = Rc::new(RefCell::new(preselect.clone())); + let selected_type: Rc> = Rc::new(RefCell::new(NoteType::Note)); let chips: Vec<(gtk4::Button, NoteType)> = NoteType::all_builtin() .iter() - .map(|&name| (bread_theme::gtk::chip(name), NoteType::from_str(name))) + .map(|&name| { + let btn = gtk4::Button::builder() + .label(name) + .css_classes(["type-chip"]) + .build(); + (btn, NoteType::from_str(name)) + }) .collect(); for (btn, nt) in &chips { - bread_theme::gtk::set_chip_active(btn, *nt == preselect); let sel = selected_type.clone(); let nt_c = nt.clone(); let all_btns: Vec = chips.iter().map(|(b, _)| b.clone()).collect(); btn.connect_clicked(move |clicked| { *sel.borrow_mut() = nt_c.clone(); - for b in &all_btns { bread_theme::gtk::set_chip_active(b, false); } - bread_theme::gtk::set_chip_active(clicked, true); + for b in &all_btns { b.remove_css_class("active"); } + clicked.add_css_class("active"); }); chip_box.append(btn); } + if let Some((btn, _)) = chips.iter().find(|(_, nt)| *nt == NoteType::Note) { + btn.add_css_class("active"); + } vbox.append(&chip_box); vbox.append(>k4::Label::builder().label("Time (optional)").xalign(0.0).build()); let time_entry = gtk4::Entry::builder() - .placeholder_text(editor::TIME_PLACEHOLDER) + .placeholder_text("tomorrow 9am / at 7pm / in 30 minutes") .hexpand(true) .build(); vbox.append(&time_entry); vbox.append(>k4::Label::builder().label("Recurrence (optional)").xalign(0.0).build()); let rrule_entry = gtk4::Entry::builder() - .placeholder_text(editor::RRULE_PLACEHOLDER) + .placeholder_text("RRULE:FREQ=WEEKLY;BYDAY=MO") .hexpand(true) .build(); vbox.append(&rrule_entry); @@ -789,7 +1062,6 @@ fn show_add_note_window(parent: >k4::ApplicationWindow, state: AppState, prese }); } - on_build(&win); win.present(); body_entry.grab_focus(); } diff --git a/breadman/src/screenshot.rs b/breadman/src/screenshot.rs deleted file mode 100644 index a335369..0000000 --- a/breadman/src/screenshot.rs +++ /dev/null @@ -1,132 +0,0 @@ -//! `--screenshot` CLI mode: render the named view, capture it via -//! `bread-screenshots`, then exit — driven by `bread-ecosystem`'s -//! `bread-capture` orchestrator, or run standalone for one-off captures. -//! -//! No clap here, same reasoning as breadpad: extends breadman's own -//! hand-rolled `mod args` instead of bolting on a second parser that would -//! reject its real flags (`--view`, `done`, `upcoming --plain`). -//! -//! `--screenshot ` doubles as the view selector for every named stack -//! page ("all", "upcoming", "todo", ...) — it's passed through as -//! `initial_view` (the same field `--view` already sets) rather than -//! needing a separate mechanism, since breadman already supports opening -//! directly to a named stack page. -//! -//! One view isn't a stack page at all: "editor" opens the per-note editor -//! dialog (`editor::open_editor`), normally only reachable by clicking a -//! real note row's edit button. Screenshot mode calls the same builder -//! function directly against the first real note in the store (bypassing -//! the button/click-handler entirely), with no-op save/delete/error -//! callbacks since nothing here should actually persist a change. - -use bread_utils::screenshot_cli::SETTLE_DELAY; -use gtk4::prelude::*; -use libadwaita::prelude::*; -use std::path::PathBuf; -use std::rc::Rc; -use std::sync::Arc; -use std::time::Duration; - -/// Delay before popping the editor popover open — same reasoning as every -/// other app's PRE_POPUP_DELAY: the parent window's own layout needs a beat -/// to settle first. -const PRE_POPUP_DELAY: Duration = SETTLE_DELAY; - -#[derive(Clone)] -pub struct ScreenshotRequest { - pub view: String, - pub output: PathBuf, - pub width: u32, - pub height: u32, -} - -/// 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 note-manager UI. -/// -/// Unlike the other apps' `dispatch`, this doesn't validate `req.view` -/// against a known-views list for the stack-page case — an invalid name -/// just falls through to breadman's own `unwrap_or("all")` default (see -/// `build_app_window`), same as `--view` already behaves for a normal run. -pub fn dispatch( - window: >k4::ApplicationWindow, - req: ScreenshotRequest, - state: crate::AppState, -) { - let output = req.output; - let (width, height) = (req.width as i32, req.height as i32); - - if req.view == "new-note" { - window.connect_map(move |root| { - let output = output.clone(); - let root = root.clone(); - let state = state.clone(); - gtk4::glib::timeout_add_local_once(PRE_POPUP_DELAY, move || { - crate::show_add_note_window(&root, state, breadpad_shared::types::NoteType::Note, move |dialog| { - let output = output.clone(); - dialog.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)); - }); - }); - }); - }); - }); - return; - } - - if req.view == "editor" { - window.connect_map(move |root| { - let output = output.clone(); - let state = state.clone(); - let root = root.clone(); - gtk4::glib::timeout_add_local_once(PRE_POPUP_DELAY, move || { - let Some(note) = state.notes.borrow().first().cloned() else { - eprintln!("breadman: no notes in the store to build the editor view from"); - std::process::exit(1); - }; - let morning = state.cfg.borrow().reminders.default_morning.clone(); - let store = Arc::new(state.write_store()); - // AdwDialog handles its own presentation/centering - no more - // manual popover anchor/position/autohide juggling. Must - // connect `map` BEFORE presenting, or the signal (which can - // fire synchronously inside `present`) is missed entirely. - let dialog = crate::editor::open_editor( - ¬e, - store, - morning, - Rc::new(|_| {}), - Rc::new(|| {}), - Rc::new(|_| {}), - ); - let output = output.clone(); - dialog.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)); - }); - }); - dialog.present(Some(root.upcast_ref::())); - }); - }); - return; - } - - 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)); - }); - }); -} - -fn finish(result: anyhow::Result<()>) { - match result { - Ok(()) => std::process::exit(0), - Err(e) => { - eprintln!("breadman: screenshot capture failed: {e}"); - std::process::exit(1); - } - } -} diff --git a/breadman/src/views/archive.rs b/breadman/src/views/archive.rs index baa4fb1..b4cd75f 100644 --- a/breadman/src/views/archive.rs +++ b/breadman/src/views/archive.rs @@ -1,6 +1,7 @@ -use super::row::{build_empty_state, RowSpec}; use breadpad_shared::types::Note; use gtk4::prelude::*; +use std::cell::RefCell; +use std::rc::Rc; pub fn build(notes: &[Note], state: crate::AppState) -> gtk4::ScrolledWindow { let scroll = gtk4::ScrolledWindow::builder() @@ -17,28 +18,92 @@ pub fn build(notes: &[Note], state: crate::AppState) -> gtk4::ScrolledWindow { .build(); let mut archived: Vec<&Note> = notes.iter().filter(|n| n.done).collect(); - // Sort by completion time, not creation time - the previous sort used - // `created` while the row displayed `completed` ("done {date}"), which - // is why the last row could appear out of order against the visible - // dates. - archived.sort_by_key(|n| std::cmp::Reverse(n.completed.unwrap_or(n.created))); + archived.sort_by(|a, b| b.created.cmp(&a.created)); if archived.is_empty() { - list.append(&build_empty_state("folder-symbolic", "Nothing archived yet.", None)); + list.append( + >k4::Label::builder() + .label("Archive is empty.") + .margin_top(32) + .build(), + ); } else { for note in archived { - let completed_str = note - .completed - .map(|t| { - let local: chrono::DateTime = t.into(); - format!("done {}", local.format("%b %d")) - }) - .unwrap_or_else(|| "done".into()); - let spec = RowSpec { date_label: completed_str, note, show_type_badge: true, show_done: false }; - list.append(&super::row::build(spec, state.clone())); + list.append(&build_archive_card(note, state.clone())); } } scroll.set_child(Some(&list)); scroll } + +fn build_archive_card(note: &Note, state: crate::AppState) -> gtk4::Box { + let row = gtk4::Box::builder() + .orientation(gtk4::Orientation::Horizontal) + .spacing(8) + .margin_start(8) + .margin_end(8) + .margin_top(2) + .margin_bottom(2) + .css_classes(["note-card"]) + .build(); + + let completed_str = note + .completed + .map(|t| { + let local: chrono::DateTime = t.into(); + format!("done {}", local.format("%b %d")) + }) + .unwrap_or_else(|| "done".into()); + + let done_label = gtk4::Label::builder() + .label(&completed_str) + .width_chars(12) + .xalign(0.0) + .build(); + + let body_label = gtk4::Label::builder() + .label(¬e.body) + .hexpand(true) + .xalign(0.0) + .ellipsize(gtk4::pango::EllipsizeMode::End) + .build(); + + let type_label = gtk4::Label::builder() + .label(note.note_type.as_str()) + .css_classes(["type-chip"]) + .build(); + + // 🗑 Delete — two-click confirm + let delete_btn = gtk4::Button::builder() + .label("🗑") + .css_classes(["action-btn", "danger-btn"]) + .tooltip_text("Delete permanently") + .build(); + { + let confirming = Rc::new(RefCell::new(false)); + let note_id = note.id.clone(); + let row_c = row.clone(); + let btn_c = delete_btn.clone(); + + delete_btn.connect_clicked(move |_| { + if *confirming.borrow() { + let store = state.write_store(); + if let Err(e) = store.delete_note(¬e_id) { + state.log_error(format!("delete failed: {}", e)); + } + row_c.set_visible(false); + state.reload_notes(); + } else { + *confirming.borrow_mut() = true; + btn_c.set_label("Sure?"); + } + }); + } + + row.append(&done_label); + row.append(&body_label); + row.append(&type_label); + row.append(&delete_btn); + row +} diff --git a/breadman/src/views/errors.rs b/breadman/src/views/errors.rs index cedb9ff..1be64e0 100644 --- a/breadman/src/views/errors.rs +++ b/breadman/src/views/errors.rs @@ -35,7 +35,7 @@ pub fn build(entries: &[(DateTime, String)]) -> gtk4::ScrolledWin .build(); let time_label = gtk4::Label::builder() - .label(ts.format("%H:%M:%S").to_string()) + .label(&ts.format("%H:%M:%S").to_string()) .width_chars(10) .xalign(0.0) .css_classes(["dim-label"]) diff --git a/breadman/src/views/mod.rs b/breadman/src/views/mod.rs index 43a6788..d4cde84 100644 --- a/breadman/src/views/mod.rs +++ b/breadman/src/views/mod.rs @@ -1,5 +1,4 @@ pub mod archive; pub mod errors; -pub mod row; pub mod settings; pub mod upcoming; diff --git a/breadman/src/views/row.rs b/breadman/src/views/row.rs deleted file mode 100644 index 501b8a0..0000000 --- a/breadman/src/views/row.rs +++ /dev/null @@ -1,268 +0,0 @@ -//! Shared single-line note row, used by every list view (All/Upcoming/ -//! per-type/Archive) instead of each view hand-rolling its own card. Design -//! review found the two-line card (title/badge top, huge dead gap, actions -//! bottom-right) used by the active views wasted enormous horizontal space -//! compared to Archive's tighter aligned-column layout - this ports that -//! layout everywhere and unifies the row template (including the edit -//! affordance, previously pencil-in-active / click-row-in-archive). - -use breadpad_shared::types::{Note, NoteType}; -use gtk4::prelude::*; -use libadwaita::prelude::*; -use std::rc::Rc; - -pub struct RowSpec<'a> { - pub date_label: String, - pub note: &'a Note, - pub show_type_badge: bool, - pub show_done: bool, -} - -/// Type-tinted badge class matching the `note-card-{type}` accent-bar colors -/// already established in breadpad-shared's theme (todo=green, -/// reminder=yellow, idea=pink, question=teal, note=blue). -fn type_chip_class(note_type: &NoteType) -> &'static str { - match note_type { - NoteType::Todo => "type-chip-todo", - NoteType::Reminder => "type-chip-reminder", - NoteType::Idea => "type-chip-idea", - NoteType::Note => "type-chip-note", - NoteType::Question => "type-chip-question", - NoteType::Tag(_) => "type-chip", - } -} - -pub fn build(spec: RowSpec, state: crate::AppState) -> gtk4::Box { - let note = spec.note; - let row = gtk4::Box::builder() - .orientation(gtk4::Orientation::Horizontal) - .spacing(8) - .margin_start(8) - .margin_end(8) - .margin_top(2) - .margin_bottom(2) - .css_classes(["note-card"]) - .build(); - row.add_css_class(&format!("note-card-{}", note.note_type.as_str())); - - let date_label = gtk4::Label::builder() - .label(&spec.date_label) - .width_chars(16) - .xalign(0.0) - .css_classes(["dim-label"]) - .build(); - row.append(&date_label); - - let body_label = gtk4::Label::builder() - .label(¬e.body) - .hexpand(true) - .xalign(0.0) - .ellipsize(gtk4::pango::EllipsizeMode::End) - .build(); - row.append(&body_label); - - if let Some(ws) = ¬e.workspace { - row.append( - >k4::Label::builder() - .label(format!("ws:{}", ws)) - .css_classes(["type-chip"]) - .build(), - ); - } - if note.rrule.is_some() { - row.append(>k4::Label::builder().label("\u{21bb}").css_classes(["dim-label"]).build()); - } - - if spec.show_type_badge { - row.append( - >k4::Label::builder() - .label(note.note_type.as_str()) - .css_classes(["type-chip", type_chip_class(¬e.note_type)]) - .build(), - ); - } - - if spec.show_done { - let done_btn = gtk4::Button::builder() - .icon_name("object-select-symbolic") - .css_classes(["action-btn", "done-btn"]) - .tooltip_text("Mark done") - .build(); - { - let note_id = note.id.clone(); - let row_c = row.clone(); - let state_c = state.clone(); - done_btn.connect_clicked(move |_| { - row_c.set_visible(false); // optimistic hide - let store = state_c.write_store(); - let id = note_id.clone(); - let state = state_c.clone(); - crate::spawn_bg( - move || -> anyhow::Result> { - if let Some(mut n) = store.get_by_id(&id)? { - n.mark_done(); - store.update_note(&n)?; - } - store.load_all() - }, - move |result| match result { - Ok(fresh) => { - *state.notes.borrow_mut() = fresh; - crate::rebuild_stack(&state); - let active = state.active_view.borrow().clone(); - state.stack.set_visible_child_name(&active); - } - Err(e) => state.log_error(format!("mark done failed: {}", e)), - }, - ); - }); - } - row.append(&done_btn); - } - - let edit_btn = gtk4::Button::builder() - .icon_name("document-edit-symbolic") - .css_classes(["action-btn", "edit-btn"]) - .tooltip_text("Edit") - .build(); - { - let note_c = note.clone(); - let state_c = state.clone(); - let body_label_c = body_label.clone(); - let row_c = row.clone(); - - edit_btn.connect_clicked(move |btn| { - let morning = state_c.cfg.borrow().reminders.default_morning.clone(); - let store = std::sync::Arc::new(state_c.write_store()); - - let state_save = state_c.clone(); - let body_label_save = body_label_c.clone(); - let state_del = state_c.clone(); - let row_del = row_c.clone(); - let state_err = state_c.clone(); - - let dialog = crate::editor::open_editor( - ¬e_c, - store, - morning, - std::rc::Rc::new(move |updated: Note| { - body_label_save.set_label(&updated.body); - state_save.reload_notes(); - crate::rebuild_stack(&state_save); - let active = state_save.active_view.borrow().clone(); - state_save.stack.set_visible_child_name(&active); - }), - std::rc::Rc::new(move || { - row_del.set_visible(false); - state_del.reload_notes(); - crate::rebuild_stack(&state_del); - let active = state_del.active_view.borrow().clone(); - state_del.stack.set_visible_child_name(&active); - }), - std::rc::Rc::new(move |e: String| { - state_err.log_error(e); - }), - ); - dialog.present(Some(btn.upcast_ref::())); - }); - } - row.append(&edit_btn); - - let delete_btn = gtk4::Button::builder() - .icon_name("user-trash-symbolic") - .css_classes(["action-btn", "danger-btn"]) - .tooltip_text("Delete") - .build(); - { - use std::cell::RefCell; - use std::rc::Rc; - let confirming = Rc::new(RefCell::new(false)); - let note_id = note.id.clone(); - let row_c = row.clone(); - let state_c = state.clone(); - let btn_c = delete_btn.clone(); - - delete_btn.connect_clicked(move |_| { - if *confirming.borrow() { - row_c.set_visible(false); // optimistic hide - let store = state_c.write_store(); - let id = note_id.clone(); - let state = state_c.clone(); - crate::spawn_bg( - move || -> anyhow::Result> { - store.delete_note(&id)?; - if let Err(e) = breadpad_shared::scheduler::Scheduler::cancel(&id) { - tracing::warn!("failed to cancel timer for {}: {}", id, e); - } - store.load_all() - }, - move |result| match result { - Ok(fresh) => { - *state.notes.borrow_mut() = fresh; - crate::rebuild_stack(&state); - let active = state.active_view.borrow().clone(); - state.stack.set_visible_child_name(&active); - } - Err(e) => state.log_error(format!("delete failed: {}", e)), - }, - ); - } else { - *confirming.borrow_mut() = true; - btn_c.set_icon_name("edit-delete-symbolic"); - btn_c.set_tooltip_text(Some("Click again to delete permanently")); - } - }); - } - row.append(&delete_btn); - - row -} - -/// Centered "nothing here" state with type-specific copy and (optionally) a -/// direct affordance to act on - the empty states were all top-anchored -/// generic text with no icon or action. -pub fn build_empty_state(icon_name: &str, text: &str, action: Option<(String, Rc)>) -> gtk4::Widget { - let outer = gtk4::Box::builder() - .orientation(gtk4::Orientation::Vertical) - .spacing(12) - .valign(gtk4::Align::Center) - .halign(gtk4::Align::Center) - .vexpand(true) - .build(); - - let icon = gtk4::Image::builder() - .icon_name(icon_name) - .pixel_size(32) - .css_classes(["dim-label"]) - .build(); - outer.append(&icon); - - let label = gtk4::Label::builder() - .label(text) - .css_classes(["dim-label"]) - .justify(gtk4::Justification::Center) - .build(); - outer.append(&label); - - if let Some((label_text, on_click)) = action { - let btn = gtk4::Button::builder() - .label(&label_text) - .css_classes(["confirm-button"]) - .halign(gtk4::Align::Center) - .build(); - btn.connect_clicked(move |_| on_click()); - outer.append(&btn); - } - - outer.upcast() -} - -/// Convenience wrapper for the note-list views: a "+ New {type}" button that -/// opens the New Note window preselected to `note_type`. -pub fn new_note_action(note_type: NoteType, window: gtk4::ApplicationWindow, state: crate::AppState) -> (String, Rc) { - let label = format!("+ New {}", note_type.as_str()); - let action: Rc = Rc::new(move || { - crate::show_add_note_window(&window, state.clone(), note_type.clone(), |_| {}); - }); - (label, action) -} diff --git a/breadman/src/views/settings.rs b/breadman/src/views/settings.rs index 6090274..8cf49c8 100644 --- a/breadman/src/views/settings.rs +++ b/breadman/src/views/settings.rs @@ -1,80 +1,7 @@ -//! Settings screen. Deliberately plain GTK4, not libadwaita's AdwActionRow/ -//! AdwSpinRow/AdwEntryRow family — those ran noticeably taller than the rest -//! of the app and don't expose a way to constrain the internal spin -//! button's width from the outside (it stretches to fill whatever room the -//! row has, leaving the digits and +/- buttons stranded behind a huge empty -//! bordered box once the row is wider than libadwaita's usual ~400-600px -//! home turf). Instead this mirrors bos-settings' own Row.svelte / -//! NumberField.svelte / TextField.svelte design exactly — same tokens -//! (12/16px row padding, ch-width inputs, transparent-at-rest border) — so -//! the two settings screens in the ecosystem actually agree with each other. - use breadpad_shared::config::{ CalendarConfig, Config, ModelConfig, OllamaConfig, RemindersConfig, Settings, }; -use breadpad_shared::types::NoteType; -use gtk4::{glib, prelude::*}; -use std::cell::RefCell; -use std::rc::Rc; - -/// A titled group: heading, optional description, then a `.boxed-list` of -/// `field_row`s (native GTK4 rounded-corner-run + divider styling). -fn field_group(title: &str, description: Option<&str>) -> (gtk4::Box, gtk4::ListBox) { - let outer = gtk4::Box::builder().orientation(gtk4::Orientation::Vertical).spacing(8).build(); - - let heading = gtk4::Label::builder().label(title).xalign(0.0).css_classes(["heading"]).build(); - outer.append(&heading); - - if let Some(desc) = description { - let desc_label = gtk4::Label::builder() - .label(desc) - .xalign(0.0) - .wrap(true) - .css_classes(["dim-label"]) - .build(); - outer.append(&desc_label); - } - - let list = gtk4::ListBox::builder() - .selection_mode(gtk4::SelectionMode::None) - .css_classes(["boxed-list"]) - .build(); - outer.append(&list); - - (outer, list) -} - -/// A single row: label (+ optional subtitle) on the left, one control on -/// the right — same shape as bos-settings' `Row.svelte`. -fn field_row(label: &str, subtitle: Option<&str>, control: &impl IsA) -> gtk4::ListBoxRow { - let row = gtk4::ListBoxRow::builder() - .selectable(false) - .activatable(false) - .css_classes(["field-row"]) - .build(); - - let hbox = gtk4::Box::builder().orientation(gtk4::Orientation::Horizontal).spacing(16).build(); - - let label_box = gtk4::Box::builder().orientation(gtk4::Orientation::Vertical).hexpand(true).valign(gtk4::Align::Center).build(); - label_box.append(>k4::Label::builder().label(label).xalign(0.0).build()); - if let Some(sub) = subtitle { - label_box.append(>k4::Label::builder().label(sub).xalign(0.0).wrap(true).css_classes(["field-row-subtitle"]).build()); - } - hbox.append(&label_box); - hbox.append(control); - - row.set_child(Some(&hbox)); - row -} - -fn text_entry(text: &str, width_chars: i32) -> gtk4::Entry { - gtk4::Entry::builder().text(text).width_chars(width_chars).valign(gtk4::Align::Center).css_classes(["field-input"]).build() -} - -fn spin_button(value: f64, min: f64, max: f64, step: f64, page: f64, digits: u32) -> gtk4::SpinButton { - let adj = gtk4::Adjustment::new(value, min, max, step, page, 0.0); - gtk4::SpinButton::builder().adjustment(&adj).digits(digits).width_chars(8).valign(gtk4::Align::Center).css_classes(["field-input"]).build() -} +use gtk4::prelude::*; pub fn build(cfg: &Config, on_save: impl Fn(Config) + 'static) -> gtk4::ScrolledWindow { let scroll = gtk4::ScrolledWindow::builder() @@ -83,242 +10,255 @@ pub fn build(cfg: &Config, on_save: impl Fn(Config) + 'static) -> gtk4::Scrolled .vexpand(true) .build(); - let content = gtk4::Box::builder().orientation(gtk4::Orientation::Vertical).spacing(24).build(); + let outer = gtk4::Box::builder() + .orientation(gtk4::Orientation::Vertical) + .spacing(16) + .margin_top(16) + .margin_bottom(16) + .margin_start(16) + .margin_end(16) + .build(); // ── General ────────────────────────────────────────────────── - let (general_group, general_list) = field_group("General", None); + let (general_frame, general_grid) = make_section("General"); - let type_pill_box = gtk4::Box::builder().orientation(gtk4::Orientation::Horizontal).spacing(4).valign(gtk4::Align::Center).build(); - let selected_type: Rc> = Rc::new(RefCell::new(cfg.settings.default_type.clone())); - let type_pills: Vec<(gtk4::Button, &'static str)> = NoteType::all_builtin() + let type_options = ["note", "todo", "reminder", "idea", "question"]; + let default_type_combo = gtk4::DropDown::from_strings(&type_options); + let dt_idx = type_options .iter() - .map(|&name| (bread_theme::gtk::chip(name), name)) - .collect(); - for (btn, name) in &type_pills { - bread_theme::gtk::set_chip_active(btn, *name == selected_type.borrow().as_str()); - type_pill_box.append(btn); - } - general_list.append(&field_row("Default type", None, &type_pill_box)); + .position(|&s| s == cfg.settings.default_type.as_str()) + .unwrap_or(0) as u32; + default_type_combo.set_selected(dt_idx); + attach_row(&general_grid, 0, "Default type", &default_type_combo); - let ws_tag_switch = gtk4::Switch::builder().active(cfg.settings.workspace_tag).valign(gtk4::Align::Center).build(); - general_list.append(&field_row( - "Workspace tag", - Some("Tag new notes with the Hyprland workspace they were created on"), - &ws_tag_switch, - )); + let ws_tag_switch = gtk4::Switch::builder() + .active(cfg.settings.workspace_tag) + .valign(gtk4::Align::Center) + .build(); + attach_row(&general_grid, 1, "Workspace tag", &ws_tag_switch); - let archive_spin = spin_button(cfg.settings.archive_after_days as f64, 1.0, 365.0, 1.0, 7.0, 0); - general_list.append(&field_row("Archive after (days)", None, &archive_spin)); + let archive_spin = gtk4::SpinButton::with_range(1.0, 365.0, 1.0); + archive_spin.set_value(cfg.settings.archive_after_days as f64); + attach_row(&general_grid, 2, "Archive after (days)", &archive_spin); - let snooze_entry = text_entry(&cfg.settings.snooze_options.join(", "), 24); - general_list.append(&field_row("Snooze options", Some("Comma-separated (e.g. 15m, 1h, tomorrow_morning)"), &snooze_entry)); + let snooze_entry = gtk4::Entry::builder() + .text(&cfg.settings.snooze_options.join(", ")) + .hexpand(true) + .build(); + attach_row(&general_grid, 3, "Snooze options", &snooze_entry); - content.append(&general_group); + outer.append(&general_frame); // ── Reminders ──────────────────────────────────────────────── - let (rem_group, rem_list) = field_group("Reminders", None); + let (rem_frame, rem_grid) = make_section("Reminders"); - let morning_entry = text_entry(&cfg.reminders.default_morning, 10); - rem_list.append(&field_row("Default morning", Some("Used for \"tomorrow_morning\" snoozes and recurring reminders"), &morning_entry)); + let morning_entry = gtk4::Entry::builder() + .text(&cfg.reminders.default_morning) + .placeholder_text("HH:MM") + .build(); + attach_row(&rem_grid, 0, "Default morning", &morning_entry); - let grace_spin = spin_button(cfg.reminders.missed_grace_minutes as f64, 0.0, 1440.0, 5.0, 30.0, 0); - rem_list.append(&field_row("Missed grace (minutes)", Some("How late a reminder can fire before it's considered missed"), &grace_spin)); + let grace_spin = gtk4::SpinButton::with_range(0.0, 1440.0, 5.0); + grace_spin.set_value(cfg.reminders.missed_grace_minutes as f64); + attach_row(&rem_grid, 1, "Missed grace (minutes)", &grace_spin); - content.append(&rem_group); + outer.append(&rem_frame); - // ── Local classifier ─────────────────────────────────────────── - let (model_group, model_list) = field_group( - "Local Classifier", - Some("Optional local ONNX model for classifying note type/time without a network round-trip. These paths are shared with breadpad — both apps read the same model files."), - ); + // ── Model ───────────────────────────────────────────────────── + let (model_frame, model_grid) = make_section("Model (Tier 2 ONNX)"); - let model_path_entry = text_entry(&cfg.model.path, 30); - model_list.append(&field_row("Model path", None, &model_path_entry)); + let model_path_entry = gtk4::Entry::builder() + .text(&cfg.model.path) + .hexpand(true) + .build(); + attach_row(&model_grid, 0, "ONNX path", &model_path_entry); - let tokenizer_entry = text_entry(&cfg.model.tokenizer, 30); - model_list.append(&field_row("Tokenizer path", None, &tokenizer_entry)); + let tokenizer_entry = gtk4::Entry::builder() + .text(&cfg.model.tokenizer) + .hexpand(true) + .build(); + attach_row(&model_grid, 1, "Tokenizer path", &tokenizer_entry); - let ort_dylib_entry = text_entry(&cfg.model.ort_dylib_path, 30); - model_list.append(&field_row("Runtime library path", None, &ort_dylib_entry)); + let ort_dylib_entry = gtk4::Entry::builder() + .text(&cfg.model.ort_dylib_path) + .hexpand(true) + .build(); + attach_row(&model_grid, 2, "ORT dylib path", &ort_dylib_entry); - content.append(&model_group); + outer.append(&model_frame); - // ── AI classification (Ollama) ────────────────────────────────── - let (ollama_group, ollama_list) = field_group( - "AI Classification", - Some("Uses a local Ollama model as a fallback classifier when the ONNX model is unavailable or unsure."), - ); + // ── Ollama (Tier 3) ─────────────────────────────────────────── + let (ollama_frame, ollama_grid) = make_section("Ollama (Tier 3)"); - let ollama_enabled_switch = gtk4::Switch::builder().active(cfg.model.ollama.enabled).valign(gtk4::Align::Center).build(); - ollama_list.append(&field_row("Enabled", None, &ollama_enabled_switch)); + let ollama_enabled = gtk4::Switch::builder() + .active(cfg.model.ollama.enabled) + .valign(gtk4::Align::Center) + .build(); + attach_row(&ollama_grid, 0, "Enabled", &ollama_enabled); - let ollama_endpoint_entry = text_entry(&cfg.model.ollama.endpoint, 24); - ollama_list.append(&field_row("Endpoint", None, &ollama_endpoint_entry)); + let ollama_endpoint = gtk4::Entry::builder() + .text(&cfg.model.ollama.endpoint) + .hexpand(true) + .build(); + attach_row(&ollama_grid, 1, "Endpoint", &ollama_endpoint); - let ollama_model_entry = text_entry(&cfg.model.ollama.model, 16); - ollama_list.append(&field_row("Model", None, &ollama_model_entry)); + let ollama_model = gtk4::Entry::builder() + .text(&cfg.model.ollama.model) + .build(); + attach_row(&ollama_grid, 2, "Model", &ollama_model); - let ollama_thresh_spin = spin_button(cfg.model.ollama.confidence_threshold as f64, 0.0, 1.0, 0.05, 0.1, 2); - ollama_list.append(&field_row("Confidence threshold", None, &ollama_thresh_spin)); + let ollama_thresh = gtk4::SpinButton::with_range(0.0, 1.0, 0.05); + ollama_thresh.set_value(cfg.model.ollama.confidence_threshold as f64); + ollama_thresh.set_digits(2); + attach_row(&ollama_grid, 3, "Confidence threshold", &ollama_thresh); - content.append(&ollama_group); + outer.append(&ollama_frame); - // ── Calendar sync ──────────────────────────────────────────── - let (cal_group, cal_list) = field_group("Calendar Sync", Some("Sync reminders to a Nextcloud calendar via CalDAV.")); + // ── Calendar ───────────────────────────────────────────────── + let (cal_frame, cal_grid) = make_section("Nextcloud Calendar (CalDAV)"); - let cal_enabled_switch = gtk4::Switch::builder().active(cfg.calendar.enabled).valign(gtk4::Align::Center).build(); - cal_list.append(&field_row("Enabled", None, &cal_enabled_switch)); + let cal_enabled = gtk4::Switch::builder() + .active(cfg.calendar.enabled) + .valign(gtk4::Align::Center) + .build(); + attach_row(&cal_grid, 0, "Enabled", &cal_enabled); - let cal_url_entry = text_entry(&cfg.calendar.url, 30); - cal_list.append(&field_row("Calendar URL", None, &cal_url_entry)); + let cal_url = gtk4::Entry::builder() + .text(&cfg.calendar.url) + .placeholder_text("https://nextcloud.example.com/remote.php/dav/calendars/you/personal/") + .hexpand(true) + .build(); + attach_row(&cal_grid, 1, "Calendar URL", &cal_url); - let cal_user_entry = text_entry(&cfg.calendar.username, 16); - cal_list.append(&field_row("Username", None, &cal_user_entry)); + let cal_user = gtk4::Entry::builder() + .text(&cfg.calendar.username) + .build(); + attach_row(&cal_grid, 2, "Username", &cal_user); - let cal_pass_entry = gtk4::PasswordEntry::builder().text(&cfg.calendar.password).show_peek_icon(true).valign(gtk4::Align::Center).css_classes(["field-input"]).build(); - cal_list.append(&field_row("App password", None, &cal_pass_entry)); + let cal_pass = gtk4::Entry::builder() + .text(&cfg.calendar.password) + .input_purpose(gtk4::InputPurpose::Password) + .visibility(false) + .build(); + attach_row(&cal_grid, 3, "App password", &cal_pass); - content.append(&cal_group); + outer.append(&cal_frame); - // ── Status (instant-apply — no Save button) ───────────────── - let status_label = gtk4::Label::builder().label("").xalign(0.0).css_classes(["dim-label"]).margin_top(4).build(); - content.append(&status_label); + // ── Save ────────────────────────────────────────────────────── + let status_label = gtk4::Label::builder() + .label("") + .xalign(0.0) + .css_classes(["dim-label"]) + .build(); + let save_btn = gtk4::Button::builder() + .label("Save Settings") + .css_classes(["confirm-button"]) + .halign(gtk4::Align::End) + .build(); - // Reads every widget's current value and persists immediately. Every - // control below calls this on its own "committed a change" signal - // (switch/spin fire on change; entries fire on Enter or focus-out). - let apply_now: Rc = Rc::new({ - let selected_type = selected_type.clone(); - let ws_tag_switch = ws_tag_switch.clone(); - let archive_spin = archive_spin.clone(); - let snooze_entry = snooze_entry.clone(); - let morning_entry = morning_entry.clone(); - let grace_spin = grace_spin.clone(); - let model_path_entry = model_path_entry.clone(); - let tokenizer_entry = tokenizer_entry.clone(); - let ort_dylib_entry = ort_dylib_entry.clone(); - let ollama_enabled_switch = ollama_enabled_switch.clone(); - let ollama_endpoint_entry = ollama_endpoint_entry.clone(); - let ollama_model_entry = ollama_model_entry.clone(); - let ollama_thresh_spin = ollama_thresh_spin.clone(); - let cal_enabled_switch = cal_enabled_switch.clone(); - let cal_url_entry = cal_url_entry.clone(); - let cal_user_entry = cal_user_entry.clone(); - let cal_pass_entry = cal_pass_entry.clone(); - let status_label = status_label.clone(); + { + let dtc = default_type_combo.clone(); + let wts = ws_tag_switch.clone(); + let ars = archive_spin.clone(); + let sne = snooze_entry.clone(); + let moe = morning_entry.clone(); + let grs = grace_spin.clone(); + let mpe = model_path_entry.clone(); + let tke = tokenizer_entry.clone(); + let ode = ort_dylib_entry.clone(); + let oec = ollama_enabled.clone(); + let oee = ollama_endpoint.clone(); + let ome = ollama_model.clone(); + let ots = ollama_thresh.clone(); + let cec = cal_enabled.clone(); + let cuc = cal_url.clone(); + let csc = cal_user.clone(); + let cpc = cal_pass.clone(); + let sl = status_label.clone(); - move || { + save_btn.connect_clicked(move |_| { let new_cfg = Config { settings: Settings { - default_type: selected_type.borrow().clone(), - workspace_tag: ws_tag_switch.is_active(), - snooze_options: snooze_entry + default_type: type_options + .get(dtc.selected() as usize) + .copied() + .unwrap_or("note") + .to_string(), + workspace_tag: wts.is_active(), + snooze_options: sne .text() .split(',') .map(|s| s.trim().to_string()) .filter(|s| !s.is_empty()) .collect(), - archive_after_days: archive_spin.value() as i64, + archive_after_days: ars.value() as i64, }, reminders: RemindersConfig { - default_morning: morning_entry.text().to_string(), - missed_grace_minutes: grace_spin.value() as i64, + default_morning: moe.text().to_string(), + missed_grace_minutes: grs.value() as i64, }, model: ModelConfig { - path: model_path_entry.text().to_string(), - tokenizer: tokenizer_entry.text().to_string(), - ort_dylib_path: ort_dylib_entry.text().to_string(), + path: mpe.text().to_string(), + tokenizer: tke.text().to_string(), + ort_dylib_path: ode.text().to_string(), ollama: OllamaConfig { - enabled: ollama_enabled_switch.is_active(), - endpoint: ollama_endpoint_entry.text().to_string(), - model: ollama_model_entry.text().to_string(), - confidence_threshold: ollama_thresh_spin.value() as f32, + enabled: oec.is_active(), + endpoint: oee.text().to_string(), + model: ome.text().to_string(), + confidence_threshold: ots.value() as f32, }, }, calendar: CalendarConfig { - enabled: cal_enabled_switch.is_active(), - url: cal_url_entry.text().to_string(), - username: cal_user_entry.text().to_string(), - password: cal_pass_entry.text().to_string(), + enabled: cec.is_active(), + url: cuc.text().to_string(), + username: csc.text().to_string(), + password: cpc.text().to_string(), }, }; match new_cfg.save() { Ok(()) => { - status_label.set_label("Saved."); + sl.set_label("Settings saved."); on_save(new_cfg); } - Err(e) => status_label.set_label(&format!("Save failed: {}", e)), + Err(e) => sl.set_label(&format!("Save failed: {}", e)), } - } - }); - - // Type pills, switches, spinners apply the moment they change. - for (btn, name) in &type_pills { - let apply_now = apply_now.clone(); - let sel = selected_type.clone(); - let name = *name; - let all_btns: Vec = type_pills.iter().map(|(b, _)| b.clone()).collect(); - btn.connect_clicked(move |clicked| { - *sel.borrow_mut() = name.to_string(); - for b in &all_btns { bread_theme::gtk::set_chip_active(b, false); } - bread_theme::gtk::set_chip_active(clicked, true); - apply_now(); }); } - macro_rules! apply_on_active { - ($sw:expr) => { - let apply_now = apply_now.clone(); - $sw.connect_state_set(move |_, _| { apply_now(); glib::Propagation::Proceed }); - }; - } - apply_on_active!(ws_tag_switch); - apply_on_active!(ollama_enabled_switch); - apply_on_active!(cal_enabled_switch); - macro_rules! apply_on_value_changed { - ($spin:expr) => { - let apply_now = apply_now.clone(); - $spin.connect_value_changed(move |_| apply_now()); - }; - } - apply_on_value_changed!(archive_spin); - apply_on_value_changed!(grace_spin); - apply_on_value_changed!(ollama_thresh_spin); - // Entries: apply on Enter, and on focus-out so a click-away doesn't - // silently discard the edit. - macro_rules! apply_on_entry { - ($entry:expr) => { - let apply_now_activate = apply_now.clone(); - $entry.connect_activate(move |_| apply_now_activate()); - let apply_now_focus = apply_now.clone(); - let focus = gtk4::EventControllerFocus::new(); - focus.connect_leave(move |_| apply_now_focus()); - $entry.add_controller(focus); - }; - } - apply_on_entry!(snooze_entry); - apply_on_entry!(morning_entry); - apply_on_entry!(model_path_entry); - apply_on_entry!(tokenizer_entry); - apply_on_entry!(ort_dylib_entry); - apply_on_entry!(ollama_endpoint_entry); - apply_on_entry!(ollama_model_entry); - apply_on_entry!(cal_url_entry); - apply_on_entry!(cal_user_entry); - apply_on_entry!(cal_pass_entry); - - content.set_halign(gtk4::Align::Start); - content.set_size_request(900, -1); - - let outer = gtk4::Box::builder() - .orientation(gtk4::Orientation::Vertical) - .margin_start(12) - .margin_end(12) - .margin_top(12) - .margin_bottom(16) + let btn_row = gtk4::Box::builder() + .orientation(gtk4::Orientation::Horizontal) + .spacing(8) .build(); - outer.append(&content); + btn_row.append(&status_label); + btn_row.append(>k4::Box::builder().hexpand(true).build()); + btn_row.append(&save_btn); + outer.append(&btn_row); scroll.set_child(Some(&outer)); scroll } + +fn make_section(title: &str) -> (gtk4::Frame, gtk4::Grid) { + let frame = gtk4::Frame::builder().label(title).build(); + let grid = gtk4::Grid::builder() + .row_spacing(8) + .column_spacing(16) + .margin_top(8) + .margin_bottom(8) + .margin_start(8) + .margin_end(8) + .build(); + frame.set_child(Some(&grid)); + (frame, grid) +} + +fn attach_row(grid: >k4::Grid, row: i32, label: &str, widget: &impl gtk4::prelude::IsA) { + let lbl = gtk4::Label::builder() + .label(label) + .xalign(0.0) + .hexpand(false) + .width_chars(24) + .build(); + grid.attach(&lbl, 0, row, 1, 1); + grid.attach(widget, 1, row, 1, 1); +} diff --git a/breadman/src/views/upcoming.rs b/breadman/src/views/upcoming.rs index be66376..35480c0 100644 --- a/breadman/src/views/upcoming.rs +++ b/breadman/src/views/upcoming.rs @@ -1,8 +1,7 @@ -use super::row::{build_empty_state, RowSpec}; use breadpad_shared::types::{Note, NoteType}; use gtk4::prelude::*; -pub fn build(notes: &[Note], state: crate::AppState) -> gtk4::ScrolledWindow { +pub fn build(notes: &[Note]) -> gtk4::ScrolledWindow { let scroll = gtk4::ScrolledWindow::builder() .hscrollbar_policy(gtk4::PolicyType::Never) .vscrollbar_policy(gtk4::PolicyType::Automatic) @@ -27,21 +26,61 @@ pub fn build(notes: &[Note], state: crate::AppState) -> gtk4::ScrolledWindow { upcoming.sort_by_key(|n| n.effective_time().unwrap()); if upcoming.is_empty() { - list.append(&build_empty_state("x-office-calendar-symbolic", "No upcoming reminders or todos.", None)); + let label = gtk4::Label::builder() + .label("No upcoming reminders or todos.") + .margin_top(32) + .build(); + list.append(&label); } else { for note in upcoming { - let time_str = note - .effective_time() - .map(|t| { - let local: chrono::DateTime = t.into(); - local.format("%a %b %d, %H:%M").to_string() - }) - .unwrap_or_default(); - let spec = RowSpec { date_label: time_str, note, show_type_badge: true, show_done: true }; - list.append(&super::row::build(spec, state.clone())); + let card = build_upcoming_card(note); + list.append(&card); } } scroll.set_child(Some(&list)); scroll } + +fn build_upcoming_card(note: &Note) -> gtk4::Box { + let row = gtk4::Box::builder() + .orientation(gtk4::Orientation::Horizontal) + .spacing(8) + .margin_start(8) + .margin_end(8) + .margin_top(4) + .margin_bottom(4) + .css_classes(["note-card"]) + .build(); + + let time_str = note + .effective_time() + .map(|t| { + let local: chrono::DateTime = t.into(); + local.format("%a %b %d, %H:%M").to_string() + }) + .unwrap_or_default(); + + let time_label = gtk4::Label::builder() + .label(&time_str) + .width_chars(18) + .xalign(0.0) + .build(); + + let body_label = gtk4::Label::builder() + .label(¬e.body) + .hexpand(true) + .xalign(0.0) + .ellipsize(gtk4::pango::EllipsizeMode::End) + .build(); + + let type_label = gtk4::Label::builder() + .label(note.note_type.as_str()) + .css_classes(["type-chip"]) + .build(); + + row.append(&time_label); + row.append(&body_label); + row.append(&type_label); + row +} diff --git a/breadpad-shared/Cargo.toml b/breadpad-shared/Cargo.toml index a56c187..01470f9 100644 --- a/breadpad-shared/Cargo.toml +++ b/breadpad-shared/Cargo.toml @@ -7,8 +7,7 @@ authors.workspace = true [dependencies] -bread-theme = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.4", features = ["gtk"] } -gtk4.workspace = true +bread-theme = { git = "https://github.com/Breadway/bread-ecosystem", tag = "v0.2.8", features = ["gtk"] } anyhow.workspace = true tracing.workspace = true serde.workspace = true @@ -20,7 +19,7 @@ tokio.workspace = true zbus.workspace = true ort.workspace = true tokenizers.workspace = true -bread-onnx = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.2" } +ndarray.workspace = true toml.workspace = true dirs.workspace = true regex.workspace = true diff --git a/breadpad-shared/src/ai.rs b/breadpad-shared/src/ai.rs index 24f2bd4..7bb69ad 100644 --- a/breadpad-shared/src/ai.rs +++ b/breadpad-shared/src/ai.rs @@ -73,7 +73,7 @@ impl OllamaClient { let classification: OllamaClassification = extract_json(&ollama_resp.response) .ok_or_else(|| anyhow::anyhow!( "no JSON object found in response — raw: {:?}", - ollama_resp.response + &ollama_resp.response ))?; let note_type = classification diff --git a/breadpad-shared/src/classifier.rs b/breadpad-shared/src/classifier.rs index 35c2650..e752a31 100644 --- a/breadpad-shared/src/classifier.rs +++ b/breadpad-shared/src/classifier.rs @@ -2,8 +2,6 @@ use crate::ai::OllamaClient; use crate::config::OllamaConfig; use crate::parser::parse_rule_based; use crate::types::{ClassificationResult, NoteType}; -use bread_onnx::{build_session, Provider}; -use ort::session::builder::GraphOptimizationLevel; use std::path::PathBuf; /// Minimum Tier 1 confidence needed to skip Tier 2 entirely. @@ -18,7 +16,7 @@ pub enum ExecutionProvider { impl ExecutionProvider { pub fn as_str(&self) -> &str { match self { - ExecutionProvider::Gpu => "MIGraphX (iGPU)", + ExecutionProvider::Gpu => "ROCm (iGPU)", ExecutionProvider::Cpu => "CPU", } } @@ -109,7 +107,9 @@ impl Classifier { // ── Tier 2 ─────────────────────────────────────────────────────────── // ONNX model classifies the type only; Tier 1's time/rrule/body are kept. - let tier2 = if let (Some(session), Some(tokenizer)) = (&mut self.session, &self.tokenizer) { + let tier2 = if let (Some(session), Some(tokenizer)) = + (&mut self.session, &self.tokenizer) + { match run_onnx(session, tokenizer, text) { Ok(r) => { tracing::debug!("Tier 2: {:?} conf={:.2}", r.note_type, r.confidence); @@ -163,18 +163,9 @@ impl Classifier { // entailment score across all five passes. const HYPOTHESES: [(&str, &str); 5] = [ ("This note is a task or action item to complete.", "todo"), - ( - "This note is a reminder with a specific time or deadline.", - "reminder", - ), - ( - "This note is an idea, suggestion, or creative thought.", - "idea", - ), - ( - "This note is a general observation or piece of information.", - "note", - ), + ("This note is a reminder with a specific time or deadline.", "reminder"), + ("This note is an idea, suggestion, or creative thought.", "idea"), + ("This note is a general observation or piece of information.", "note"), ("This note is a question that needs an answer.", "question"), ]; @@ -193,17 +184,15 @@ fn run_onnx( .map_err(|e| anyhow::anyhow!("tokenize: {}", e))?; let ids: Vec = encoding.get_ids().iter().map(|&x| x as i64).collect(); - let mask: Vec = encoding - .get_attention_mask() - .iter() - .map(|&x| x as i64) - .collect(); + let mask: Vec = encoding.get_attention_mask().iter().map(|&x| x as i64).collect(); let len = ids.len(); - let ids_tensor = ort::value::Tensor::::from_array((vec![1i64, len as i64], ids)) - .map_err(|e| anyhow::anyhow!("ids tensor: {}", e))?; - let mask_tensor = ort::value::Tensor::::from_array((vec![1i64, len as i64], mask)) - .map_err(|e| anyhow::anyhow!("mask tensor: {}", e))?; + let ids_tensor = ort::value::Tensor::::from_array( + (vec![1i64, len as i64], ids) + ).map_err(|e| anyhow::anyhow!("ids tensor: {}", e))?; + let mask_tensor = ort::value::Tensor::::from_array( + (vec![1i64, len as i64], mask) + ).map_err(|e| anyhow::anyhow!("mask tensor: {}", e))?; let inputs = ort::inputs![ "input_ids" => ids_tensor, @@ -218,7 +207,10 @@ fn run_onnx( .map_err(|e| anyhow::anyhow!("extract logits: {}", e))?; let (_, logits_slice) = logits; - entailment_scores[i] = logits_slice.get(ENTAILMENT_IDX).copied().unwrap_or(0.0); + entailment_scores[i] = logits_slice + .get(ENTAILMENT_IDX) + .copied() + .unwrap_or(0.0); } let best_idx = entailment_scores @@ -251,30 +243,42 @@ fn softmax_single(logits: &[f32], idx: usize) -> f32 { exps[idx] / sum } -fn try_load_session(path: &std::path::Path) -> (Option, ExecutionProvider) { - // WHY: distro onnxruntime-rocm is MIGraphX, not classic ROCm; bread-onnx - // appends CPU so a missing GPU EP does not disable Tier 2. - match build_session( - path, - GraphOptimizationLevel::Level3, - &[Provider::MiGraphX { device_id: 0 }], - ) { +fn try_load_session( + path: &std::path::Path, +) -> (Option, ExecutionProvider) { + // Try ROCm (iGPU) first, fall back to CPU. + let rocm_available = { + use ort::execution_providers::ExecutionProvider as _; + ort::ep::ROCm::default().is_available().unwrap_or(false) + }; + if rocm_available { + match build_onnx_session(path, ort::ep::ROCm::default().build()) { + Ok(s) => { + tracing::info!("ONNX session loaded (ROCm iGPU)"); + return (Some(s), ExecutionProvider::Gpu); + } + Err(e) => tracing::debug!("ROCm EP unavailable: {}; trying CPU", e), + } + } + match build_onnx_session(path, ort::ep::CPU::default().build()) { Ok(s) => { - tracing::info!("ONNX session loaded (MIGraphX, CPU fallback)"); - (Some(s), ExecutionProvider::Gpu) + tracing::info!("ONNX session loaded (CPU)"); + (Some(s), ExecutionProvider::Cpu) } Err(e) => { - tracing::debug!("MIGraphX session failed: {}; trying CPU", e); - match build_session(path, GraphOptimizationLevel::Level3, &[Provider::Cpu]) { - Ok(s) => { - tracing::info!("ONNX session loaded (CPU)"); - (Some(s), ExecutionProvider::Cpu) - } - Err(e) => { - tracing::warn!("failed to load ONNX session: {}; Tier 2 disabled", e); - (None, ExecutionProvider::Cpu) - } - } + tracing::warn!("failed to load ONNX session: {}; Tier 2 disabled", e); + (None, ExecutionProvider::Cpu) } } } + +fn build_onnx_session( + path: &std::path::Path, + ep: ort::ep::ExecutionProviderDispatch, +) -> anyhow::Result { + let mut builder = ort::session::Session::builder() + .map_err(|e| anyhow::anyhow!("builder: {}", e))? + .with_execution_providers([ep]) + .map_err(|e| anyhow::anyhow!("ep: {}", e))?; + builder.commit_from_file(path).map_err(|e| anyhow::anyhow!("load: {}", e)) +} diff --git a/breadpad-shared/src/config.rs b/breadpad-shared/src/config.rs index e0f9bcf..9d81e5e 100644 --- a/breadpad-shared/src/config.rs +++ b/breadpad-shared/src/config.rs @@ -135,7 +135,7 @@ impl Default for RemindersConfig { } } -#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct CalendarConfig { #[serde(default = "default_calendar_enabled")] pub enabled: bool, @@ -150,6 +150,17 @@ pub struct CalendarConfig { pub password: String, } +impl Default for CalendarConfig { + fn default() -> Self { + CalendarConfig { + enabled: false, + url: String::new(), + username: String::new(), + password: String::new(), + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct Config { #[serde(default)] diff --git a/breadpad-shared/src/parser.rs b/breadpad-shared/src/parser.rs index 27f63b9..e398e73 100644 --- a/breadpad-shared/src/parser.rs +++ b/breadpad-shared/src/parser.rs @@ -85,6 +85,43 @@ fn rrule_weekday(wd: Weekday) -> &'static str { } } +/// Explicit type prefixes, checked before any lexical heuristics. Short forms +/// exist so the capture popup can be driven without reaching for the mouse — +/// see `breadpad_shared::parser::detect_prefix_type`, which the popup's entry +/// uses to live-highlight the matching chip as the user types. +const TYPE_PREFIXES: &[(&str, NoteType)] = &[ + ("td:", NoteType::Todo), + ("rem:", NoteType::Reminder), + ("idea:", NoteType::Idea), + ("note:", NoteType::Note), + ("q:", NoteType::Question), +]; + +/// If `text` starts with one of [`TYPE_PREFIXES`] (case-insensitive), returns +/// the type it forces. Used both to classify at save time and to live-drive +/// the popup's chip highlighting as the user types. +pub fn detect_prefix_type(text: &str) -> Option { + let lower = text.trim_start().to_lowercase(); + TYPE_PREFIXES + .iter() + .find(|(prefix, _)| lower.starts_with(prefix)) + .map(|(_, nt)| nt.clone()) +} + +/// Strips a leading explicit type prefix (if any), returning the forced type +/// and the remaining text with the prefix and any following whitespace removed. +fn strip_explicit_prefix(text: &str) -> (Option, String) { + let trimmed = text.trim_start(); + let lower = trimmed.to_lowercase(); + for (prefix, nt) in TYPE_PREFIXES { + if lower.starts_with(prefix) { + let rest = trimmed[prefix.len()..].trim_start().to_string(); + return (Some(nt.clone()), rest); + } + } + (None, text.to_string()) +} + fn next_occurrence_of_weekday(wd: Weekday, time: NaiveTime) -> DateTime { let local = Local::now(); let days_ahead = (wd.num_days_from_monday() as i64 @@ -105,6 +142,8 @@ fn next_occurrence_of_weekday(wd: Weekday, time: NaiveTime) -> DateTime { } pub fn parse_rule_based(text: &str, default_morning: &str) -> ClassificationResult { + let (forced_type, text_owned) = strip_explicit_prefix(text); + let text: &str = &text_owned; let p = patterns(); let morning_time: NaiveTime = default_morning .split(':') @@ -290,8 +329,9 @@ pub fn parse_rule_based(text: &str, default_morning: &str) -> ClassificationResu .to_string(); } - // Infer note type - let note_type = infer_type(text, extracted_time.is_some(), rrule.is_some()); + // Infer note type — an explicit prefix (`td:`, `rem:`, …) always wins. + let note_type = + forced_type.clone().unwrap_or_else(|| infer_type(text, extracted_time.is_some(), rrule.is_some())); // Trim artifacts cleaned = cleaned @@ -303,7 +343,9 @@ pub fn parse_rule_based(text: &str, default_morning: &str) -> ClassificationResu // Calibrated confidence: high when structural signals drove the decision, // low when we fell back to "note" with no positive evidence. - let confidence = if rrule.is_some() || extracted_time.is_some() { + let confidence = if forced_type.is_some() { + 0.99 // explicit prefix — unambiguous + } else if rrule.is_some() || extracted_time.is_some() { 0.95 // time/recurrence extraction is deterministic } else { match ¬e_type { @@ -324,67 +366,6 @@ pub fn parse_rule_based(text: &str, default_morning: &str) -> ClassificationResu } } -fn infer_type(text: &str, has_time: bool, has_rrule: bool) -> NoteType { - let lower = text.to_lowercase(); - if has_rrule || has_time { - return NoteType::Reminder; - } - if lower.contains("buy ") - || lower.contains("pick up") - || lower.contains("clean ") - || lower.starts_with("call ") - || lower.starts_with("email ") - || lower.starts_with("fix ") - || lower.starts_with("check ") - || lower.starts_with("finish ") - || lower.starts_with("write ") - || lower.starts_with("update ") - || lower.starts_with("prepare ") - || lower.starts_with("schedule ") - || lower.starts_with("organize ") - || lower.starts_with("deploy ") - || lower.starts_with("install ") - || lower.starts_with("send ") - || lower.starts_with("submit ") - || lower.starts_with("create ") - || lower.starts_with("setup ") - || lower.starts_with("restore ") - || lower.starts_with("archive ") - || lower.starts_with("export ") - || lower.starts_with("import ") - || lower.starts_with("approve ") - || lower.starts_with("configure ") - || lower.starts_with("refactor ") - || lower.starts_with("review ") - { - return NoteType::Todo; - } - if lower.starts_with("what if ") - || lower.starts_with("idea:") - || lower.contains("could ") - || lower.contains("maybe ") - || lower.starts_with("should we ") - { - return NoteType::Idea; - } - if lower.starts_with("why ") - || lower.starts_with("how ") - || (lower.starts_with("what ") && !lower.starts_with("what if ")) - || lower.starts_with("when ") - || lower.starts_with("where ") - || lower.starts_with("who ") - || lower.starts_with("will ") - || lower.starts_with("is ") - || lower.starts_with("are ") - || lower.starts_with("did ") - || lower.starts_with("does ") - || lower.ends_with('?') - { - return NoteType::Question; - } - NoteType::Note -} - #[cfg(test)] mod tests { use super::*; @@ -471,6 +452,70 @@ mod tests { assert_eq!(p("idea: reactive state module in Lua").note_type, NoteType::Idea); } + #[test] + fn idea_prefix_stripped_from_body() { + let r = p("idea: reactive state module in Lua"); + assert_eq!(r.body, "reactive state module in Lua"); + } + + // ---- Explicit short prefixes (td:, rem:, note:, q:) ---- + + #[test] + fn td_prefix_is_todo() { + let r = p("td: buy milk"); + assert_eq!(r.note_type, NoteType::Todo); + assert_eq!(r.body, "buy milk"); + } + + #[test] + fn rem_prefix_is_reminder() { + let r = p("rem: water the plants"); + assert_eq!(r.note_type, NoteType::Reminder); + assert_eq!(r.body, "water the plants"); + } + + #[test] + fn note_prefix_is_note() { + // Without the prefix this would classify as Todo ("check ..."). + let r = p("note: check engine light has been on for a week"); + assert_eq!(r.note_type, NoteType::Note); + assert_eq!(r.body, "check engine light has been on for a week"); + } + + #[test] + fn q_prefix_is_question() { + // Without the prefix this has no strong signal and would fall to Note. + let r = p("q: ONNX rocm vs cpu perf"); + assert_eq!(r.note_type, NoteType::Question); + assert_eq!(r.body, "ONNX rocm vs cpu perf"); + } + + #[test] + fn explicit_prefix_confidence_is_high() { + assert_eq!(p("td: buy milk").confidence, 0.99); + } + + #[test] + fn explicit_prefix_is_case_insensitive() { + assert_eq!(p("TD: buy milk").note_type, NoteType::Todo); + assert_eq!(p("Rem: standup").note_type, NoteType::Reminder); + } + + #[test] + fn explicit_prefix_overrides_time_extraction_type() { + // "at 7pm" alone would infer Reminder; an explicit td: prefix wins. + let r = p("td: pack bag at 7pm"); + assert_eq!(r.note_type, NoteType::Todo); + assert!(r.time.is_some(), "time should still be extracted"); + } + + #[test] + fn detect_prefix_type_matches_parse() { + assert_eq!(detect_prefix_type("td: buy milk"), Some(NoteType::Todo)); + assert_eq!(detect_prefix_type("rem: call mum"), Some(NoteType::Reminder)); + assert_eq!(detect_prefix_type("no prefix here"), None); + } + #[test] fn idea_maybe() { assert_eq!(p("maybe we could cache the ONNX model").note_type, NoteType::Idea); @@ -551,7 +596,7 @@ mod tests { let r = p("take a break in 30 minutes"); let t = r.time.unwrap(); let delta = (t - before).num_seconds(); - assert!((29 * 60..=31 * 60).contains(&delta), "delta was {}s", delta); + assert!(delta >= 29 * 60 && delta <= 31 * 60, "delta was {}s", delta); } #[test] @@ -559,7 +604,7 @@ mod tests { let before = Utc::now(); let r = p("ping in 1 minute"); let delta = (r.time.unwrap() - before).num_seconds(); - assert!((55..=65).contains(&delta), "delta was {}s", delta); + assert!(delta >= 55 && delta <= 65, "delta was {}s", delta); } #[test] @@ -567,7 +612,7 @@ mod tests { let before = Utc::now(); let r = p("review PR in 2 hours"); let delta_min = (r.time.unwrap() - before).num_minutes(); - assert!((119..=121).contains(&delta_min), "delta was {}min", delta_min); + assert!(delta_min >= 119 && delta_min <= 121, "delta was {}min", delta_min); } #[test] @@ -575,7 +620,7 @@ mod tests { let before = Utc::now(); let r = p("follow up in 3 days"); let delta_h = (r.time.unwrap() - before).num_hours(); - assert!((71..=73).contains(&delta_h), "delta was {}h", delta_h); + assert!(delta_h >= 71 && delta_h <= 73, "delta was {}h", delta_h); } // ---- Time extraction: tomorrow ---- @@ -778,7 +823,7 @@ mod tests { let before = Utc::now(); let r = p("check on the server in an hour"); let delta_min = (r.time.unwrap() - before).num_minutes(); - assert!((59..=61).contains(&delta_min), "delta was {}min", delta_min); + assert!(delta_min >= 59 && delta_min <= 61, "delta was {}min", delta_min); } #[test] @@ -798,7 +843,7 @@ mod tests { let before = Utc::now(); let r = p("in a couple of hours remind me to check the oven"); let delta_min = (r.time.unwrap() - before).num_minutes(); - assert!((119..=121).contains(&delta_min), "delta was {}min", delta_min); + assert!(delta_min >= 119 && delta_min <= 121, "delta was {}min", delta_min); } #[test] @@ -819,7 +864,7 @@ mod tests { let before = Utc::now(); let r = p("in half an hour submit the report"); let delta_min = (r.time.unwrap() - before).num_minutes(); - assert!((29..=31).contains(&delta_min), "delta was {}min", delta_min); + assert!(delta_min >= 29 && delta_min <= 31, "delta was {}min", delta_min); } // ---- Tonight / this evening ---- @@ -908,3 +953,63 @@ mod tests { assert!(rule.as_str().contains("BYHOUR=16"), "rule: {}", rule.as_str()); } } + +fn infer_type(text: &str, has_time: bool, has_rrule: bool) -> NoteType { + let lower = text.to_lowercase(); + if has_rrule || has_time { + return NoteType::Reminder; + } + if lower.contains("buy ") + || lower.contains("pick up") + || lower.contains("clean ") + || lower.starts_with("call ") + || lower.starts_with("email ") + || lower.starts_with("fix ") + || lower.starts_with("check ") + || lower.starts_with("finish ") + || lower.starts_with("write ") + || lower.starts_with("update ") + || lower.starts_with("prepare ") + || lower.starts_with("schedule ") + || lower.starts_with("organize ") + || lower.starts_with("deploy ") + || lower.starts_with("install ") + || lower.starts_with("send ") + || lower.starts_with("submit ") + || lower.starts_with("create ") + || lower.starts_with("setup ") + || lower.starts_with("restore ") + || lower.starts_with("archive ") + || lower.starts_with("export ") + || lower.starts_with("import ") + || lower.starts_with("approve ") + || lower.starts_with("configure ") + || lower.starts_with("refactor ") + || lower.starts_with("review ") + { + return NoteType::Todo; + } + if lower.starts_with("what if ") + || lower.contains("could ") + || lower.contains("maybe ") + || lower.starts_with("should we ") + { + return NoteType::Idea; + } + if lower.starts_with("why ") + || lower.starts_with("how ") + || (lower.starts_with("what ") && !lower.starts_with("what if ")) + || lower.starts_with("when ") + || lower.starts_with("where ") + || lower.starts_with("who ") + || lower.starts_with("will ") + || lower.starts_with("is ") + || lower.starts_with("are ") + || lower.starts_with("did ") + || lower.starts_with("does ") + || lower.ends_with('?') + { + return NoteType::Question; + } + NoteType::Note +} diff --git a/breadpad-shared/src/scheduler.rs b/breadpad-shared/src/scheduler.rs index 580e247..ff5ff26 100644 --- a/breadpad-shared/src/scheduler.rs +++ b/breadpad-shared/src/scheduler.rs @@ -176,7 +176,7 @@ pub(crate) fn parse_next_from_rrule(rrule_str: &str, default_morning: &str) -> O } else { (now.date_naive() + chrono::Duration::days(1)).and_time(fire_time) }; - Some(local_naive_to_utc(naive)) + return Some(local_naive_to_utc(naive)); } "WEEKLY" => { use chrono::Datelike; @@ -204,7 +204,7 @@ pub(crate) fn parse_next_from_rrule(rrule_str: &str, default_morning: &str) -> O }; let target_date = (now.date_naive() + chrono::Duration::days(days_ahead)).and_time(fire_time); - Some(local_naive_to_utc(target_date)) + return Some(local_naive_to_utc(target_date)); } _ => None, } diff --git a/breadpad-shared/src/store.rs b/breadpad-shared/src/store.rs index a8f52c8..bc8b86a 100644 --- a/breadpad-shared/src/store.rs +++ b/breadpad-shared/src/store.rs @@ -118,11 +118,11 @@ impl Store { Ok(()) } - fn rewrite_notes(&self, f: F) -> Result<()> + fn rewrite_notes(&self, mut f: F) -> Result<()> where F: FnMut(Note) -> Note, { - let notes: Vec = self.load_all()?.into_iter().map(f).collect(); + let notes: Vec = self.load_all()?.into_iter().map(|n| f(n)).collect(); self.write_all(&self.notes_path, ¬es) } @@ -145,7 +145,7 @@ impl Store { let notes = self.load_all()?; let (to_archive, keep): (Vec, Vec) = notes .into_iter() - .partition(|n| n.done && n.completed.is_some_and(|c| c < cutoff)); + .partition(|n| n.done && n.completed.map_or(false, |c| c < cutoff)); if to_archive.is_empty() { return Ok(0); diff --git a/breadpad-shared/src/theme.rs b/breadpad-shared/src/theme.rs index f7e2177..8cc2512 100644 --- a/breadpad-shared/src/theme.rs +++ b/breadpad-shared/src/theme.rs @@ -13,11 +13,6 @@ pub fn apply_live() { }); } -/// Bind a window to the palette of the monitor it is rendered on. -pub fn bind_window(window: &impl gtk4::prelude::IsA) { - bread_theme::gtk::bind_window_auto(window); -} - /// Generate the full breadpad/breadman CSS string. The base — `@define-color` /// palette, fonts, and generic widget styling — comes from the shared /// `bread_theme::stylesheet`, so breadpad and breadman look identical to the @@ -33,33 +28,12 @@ pub fn build_css(palette: &Palette, user_css: Option<&str>) -> String { /* breadpad/breadman-specific components */ window { border-radius: 8px; } -/* breadman/views/settings.rs — matches bos-settings' Row.svelte/ - NumberField.svelte/TextField.svelte exactly (same design tokens: 12/16px - row padding, transparent-at-rest input border, ch-width inputs) rather - than libadwaita's own AdwActionRow/AdwSpinRow padding and internal - spin-button sizing, which run noticeably taller/wider and don't expose a - way to constrain from the outside. `list.boxed-list` already gets its - surface fill + radius from the shared stylesheet; this only adds the - compact row padding and the divider between rows. */ -list.boxed-list row.field-row { padding: 12px 16px; min-height: 0; } -list.boxed-list row.field-row:not(:last-child) { border-bottom: 1px solid alpha(@on-surface, 0.08); } -.field-row-subtitle { opacity: 0.6; font-size: 12px; } - -.field-input { - background-color: @bg; - color: @on-surface; - border: 1px solid transparent; - border-radius: 6px; - padding: 4px 8px; -} -.field-input:focus-within { border-color: @accent; outline: none; } - .popup-entry { background: @bg; color: @fg; border: 2px solid @blue; border-radius: 6px; - padding: 14px; + padding: 12px 16px; font-size: 14px; caret-color: @fg; } @@ -69,9 +43,14 @@ list.boxed-list row.field-row:not(:last-child) { border-bottom: 1px solid alpha( border-color: @teal; } +/* Shared "selected/active" language: a ghost/ outline default state that + stays quiet, and a solid, high-contrast accent fill for whatever is + currently selected — used identically by .type-chip.active and + .sidebar-row:selected so the two windows read as one system. */ .type-chip { - background: @overlay; - color: @on-overlay; + background: transparent; + color: alpha(@fg, 0.6); + border: 1px solid alpha(@fg, 0.18); border-radius: 999px; padding: 4px 12px; font-size: 12px; @@ -81,36 +60,69 @@ list.boxed-list row.field-row:not(:last-child) { border-bottom: 1px solid alpha( .type-chip.active { background: @blue; color: @on-accent; + border-color: @blue; + font-weight: 600; } -/* Per-type tint, matching the note-card-{type} accent-bar colors below - - the flat cream badge was the same high-contrast fill for every type, - out-shouting note body text while telling you nothing extra. */ -.type-chip-todo { background: alpha(@green, 0.18); color: @green; } -.type-chip-reminder { background: alpha(@yellow, 0.18); color: @yellow; } -.type-chip-idea { background: alpha(@pink, 0.18); color: @pink; } -.type-chip-question { background: alpha(@teal, 0.18); color: @teal; } -.type-chip-note { background: alpha(@blue, 0.18); color: @blue; } +/* Per-type tint so info badges (note cards, workspace/recur tags) stay + scannable at a glance without the old full-color emoji. */ +.note-card-todo .type-chip { color: @green; border-color: alpha(@green, 0.4); } +.note-card-reminder .type-chip { color: @yellow; border-color: alpha(@yellow, 0.4); } +.note-card-idea .type-chip { color: @pink; border-color: alpha(@pink, 0.4); } +.note-card-question .type-chip { color: @teal; border-color: alpha(@teal, 0.4); } +.note-card-note .type-chip { color: @blue; border-color: alpha(@blue, 0.4); } .confirm-button { background: @blue; color: @on-accent; border: none; border-radius: 8px; - padding: 8px 16px; + padding: 10px 22px; + min-height: 20px; + font-size: 15px; font-weight: bold; } +.confirm-button:hover { background: shade(@blue, 1.1); } + +/* Separates the primary action from the chip row it sits beside so it + doesn't read as just another pill. */ +.confirm-wrap { + border-left: 1px solid alpha(@fg, 0.12); + padding-left: 12px; + margin-left: 4px; +} + +.prefix-hint { + color: alpha(@fg, 0.4); + font-size: 10px; + letter-spacing: 0.3px; +} + .note-card { - background: shade(@bg, 1.1); - border-radius: 8px; + background: shade(@bg, 1.12); + border: 1px solid alpha(@fg, 0.07); + border-radius: 10px; padding: 12px; - margin: 8px; + margin: 6px 0; border-left: 3px solid @blue; } .note-card:hover { - background: shade(@bg, 1.2); + background: shade(@bg, 1.22); + border-color: alpha(@fg, 0.12); +} + +.note-title { + font-weight: 600; +} + +.note-card .action-btn { + opacity: 0.45; +} + +.note-card:hover .action-btn { + opacity: 1; } .search-entry { @@ -118,7 +130,8 @@ list.boxed-list row.field-row:not(:last-child) { border-bottom: 1px solid alpha( color: @fg; border: 1px solid @overlay; border-radius: 6px; - padding: 8px 12px; + padding: 5px 10px; + font-size: 13px; } .search-entry:focus { @@ -139,7 +152,27 @@ list.boxed-list row.field-row:not(:last-child) { border-bottom: 1px solid alpha( .sidebar-row:selected { background: @blue; color: @on-accent; - font-weight: 500; + font-weight: 600; +} + +.sidebar-row-minor { + opacity: 0.5; + font-size: 12px; +} + +.sidebar-count { + color: alpha(@fg, 0.45); + font-size: 11px; +} + +.icon-todo { color: @green; } +.icon-reminder { color: @yellow; } +.icon-idea { color: @pink; } +.icon-note { color: @blue; } +.icon-question { color: @teal; } + +.sidebar-row:selected .sidebar-count { + color: alpha(@on-accent, 0.75); } .sidebar-section-label { @@ -154,14 +187,15 @@ list.boxed-list row.field-row:not(:last-child) { border-bottom: 1px solid alpha( background: transparent; border: none; border-radius: 6px; - padding: 2px 7px; - min-width: 28px; - min-height: 28px; - font-size: 14px; + padding: 3px 8px; + min-width: 32px; + min-height: 32px; + font-size: 16px; } .action-btn:hover { background: shade(@bg, 1.3); + opacity: 1; } .done-btn { color: @green; } @@ -170,12 +204,8 @@ list.boxed-list row.field-row:not(:last-child) { border-bottom: 1px solid alpha( .edit-btn { color: @blue; } .edit-btn:hover { background: alpha(@blue, 0.15); } -/* Fixed red, not @red - pywal can hand `red` any hue depending on the - wallpaper (see bread-theme's button.destructive-action for the same - reasoning), which would make delete indistinguishable from a normal - accent action. */ -.danger-btn { color: #e01b24; } -.danger-btn:hover { background: alpha(#e01b24, 0.15); } +.danger-btn { color: @red; } +.danger-btn:hover { background: alpha(@red, 0.15); } .note-card-todo { border-left-color: @green; } .note-card-reminder { border-left-color: @yellow; } @@ -209,15 +239,12 @@ list.boxed-list row.field-row:not(:last-child) { border-bottom: 1px solid alpha( color: @fg; } -/* Dismiss and Snooze are both secondary/outline actions and should read at - equal weight - Dismiss used to sit at 0.6 alpha next to Snooze's full - opacity, which made the button that closes the reminder look disabled. */ .reminder-dismiss { background: transparent; border: 1px solid @overlay; border-radius: 8px; padding: 8px 16px; - color: @fg; + color: alpha(@fg, 0.6); } .reminder-dismiss:hover { background: shade(@bg, 1.1); } @@ -232,40 +259,15 @@ list.boxed-list row.field-row:not(:last-child) { border-bottom: 1px solid alpha( .reminder-snooze:hover { background: shade(@bg, 1.1); } -/* Left-aligned (the button's child label sets xalign itself), full-width - row with a hairline divider so the list reads as distinct clickable rows - even before hover - a hover tint alone doesn't show up in a static - reading of the popover's default state. */ .snooze-option { background: transparent; border: none; border-radius: 6px; - padding: 10px 12px; + padding: 8px 12px; color: @fg; - border-bottom: 1px solid alpha(@overlay, 0.15); } .snooze-option:hover { background: shade(@bg, 1.2); } - -.snooze-custom-entry { - background: @bg; - color: @fg; - border: 1px solid @overlay; - border-radius: 6px; - padding: 8px 12px; - margin: 4px; -} - -.snooze-custom-entry:focus-within { border-color: @blue; outline: none; } - -/* Matches the reminder alert card's flat-bordered elevation (1px border, - 8px radius, no shadow) instead of GTK's default arrow+drop-shadow popover - chrome - the two surfaces used to speak two different elevation - languages. */ -popover.snooze-popover > contents { - border: 1px solid @overlay; - box-shadow: none; -} "#); if let Some(extra) = user_css { @@ -283,7 +285,7 @@ mod tests { #[test] fn css_defines_bg_color() { let css = build_css(&Palette::default(), None); - assert!(css.contains("@define-color bg #0c0c0c"), "css missing bg: {}", &css[..300]); + assert!(css.contains("@define-color bg #1e1e2e"), "css missing bg: {}", &css[..300]); } #[test] @@ -328,11 +330,9 @@ mod tests { #[test] fn css_reflects_custom_palette_colors() { - let p = Palette { - background: "#deadbe".into(), - color4: "#cafe00".into(), - ..Default::default() - }; + let mut p = Palette::default(); + p.background = "#deadbe".into(); + p.color4 = "#cafe00".into(); let css = build_css(&p, None); assert!(css.contains("@define-color bg #deadbe"), "css: {}", &css[..300]); assert!(css.contains("@define-color blue #cafe00"), "css: {}", &css[..300]); diff --git a/breadpad-shared/src/types.rs b/breadpad-shared/src/types.rs index e1d5729..5be0b54 100644 --- a/breadpad-shared/src/types.rs +++ b/breadpad-shared/src/types.rs @@ -15,9 +15,6 @@ pub enum NoteType { } impl NoteType { - // Not std::str::FromStr — infallible, returns Self directly rather than - // Result, and used across 30+ call sites as NoteType::from_str(..). - #[allow(clippy::should_implement_trait)] pub fn from_str(s: &str) -> Self { match s.to_lowercase().as_str() { "todo" => NoteType::Todo, diff --git a/breadpad-shared/tests/classifier.rs b/breadpad-shared/tests/classifier.rs index 7f0294a..3b5527c 100644 --- a/breadpad-shared/tests/classifier.rs +++ b/breadpad-shared/tests/classifier.rs @@ -1,24 +1,17 @@ use breadpad_shared::classifier::{Classifier, ExecutionProvider}; use breadpad_shared::types::NoteType; use chrono::Timelike; -use std::path::PathBuf; -/// Rule-based path only — a present `~/.local/share/breadpad/model` must not -/// change these assertions. fn cl() -> Classifier { - Classifier::load_with_paths( - "08:00", - PathBuf::from("/nonexistent/classifier.onnx"), - PathBuf::from("/nonexistent/tokenizer.json"), - ) + Classifier::load("08:00") } #[test] fn active_provider_is_valid() { // The active provider depends on the host: a machine with the ONNX model present and - // a working MIGraphX iGPU loads `Gpu`, otherwise `Cpu`. Either is valid — but when no + // a working ROCm iGPU loads `Gpu`, otherwise `Cpu`. Either is valid — but when no // model is available we must be on CPU (no session => no GPU EP in use). - let c = Classifier::load("08:00"); + let c = cl(); assert!(matches!( c.active_provider, ExecutionProvider::Cpu | ExecutionProvider::Gpu @@ -56,28 +49,19 @@ fn classify_reminder_via_fallback() { #[test] fn classify_idea_via_fallback() { let mut c = cl(); - assert_eq!( - c.classify("what if we added a calendar view").note_type, - NoteType::Idea - ); + assert_eq!(c.classify("what if we added a calendar view").note_type, NoteType::Idea); } #[test] fn classify_question_via_fallback() { let mut c = cl(); - assert_eq!( - c.classify("why does this fail?").note_type, - NoteType::Question - ); + assert_eq!(c.classify("why does this fail?").note_type, NoteType::Question); } #[test] fn classify_note_via_fallback() { let mut c = cl(); - assert_eq!( - c.classify("meeting went well today").note_type, - NoteType::Note - ); + assert_eq!(c.classify("meeting went well today").note_type, NoteType::Note); } #[test] @@ -90,11 +74,7 @@ fn classify_recurrence_via_fallback() { #[test] fn classify_custom_morning_time() { - let mut c = Classifier::load_with_paths( - "07:15", - PathBuf::from("/nonexistent/classifier.onnx"), - PathBuf::from("/nonexistent/tokenizer.json"), - ); + let mut c = Classifier::load("07:15"); let r = c.classify("sync tomorrow morning"); let t = r.time.expect("should have a time for tomorrow morning"); let local: chrono::DateTime = t.into(); @@ -134,16 +114,12 @@ fn classify_returns_cleaned_body() { let mut c = cl(); let r = c.classify("call mum at 6pm"); assert!(r.body.contains("call mum"), "body: {}", r.body); - assert!( - !r.body.contains("6pm"), - "time phrase should be stripped from body: {}", - r.body - ); + assert!(!r.body.contains("6pm"), "time phrase should be stripped from body: {}", r.body); } #[test] fn model_path_points_to_expected_location() { - let c = Classifier::load("08:00"); + let c = cl(); assert!( c.model_path.to_str().unwrap().contains("breadpad"), "model path: {:?}", diff --git a/breadpad-shared/tests/config.rs b/breadpad-shared/tests/config.rs index 462afe6..6e057e5 100644 --- a/breadpad-shared/tests/config.rs +++ b/breadpad-shared/tests/config.rs @@ -279,19 +279,15 @@ fn resolved_ort_dylib_empty_returns_none() { #[test] fn resolved_ort_dylib_whitespace_only_returns_none() { - let m = ModelConfig { - ort_dylib_path: " ".into(), - ..Default::default() - }; + let mut m = ModelConfig::default(); + m.ort_dylib_path = " ".into(); assert!(m.resolved_ort_dylib_path().is_none()); } #[test] fn resolved_ort_dylib_set_returns_some() { - let m = ModelConfig { - ort_dylib_path: "/usr/lib/libonnxruntime.so".into(), - ..Default::default() - }; + let mut m = ModelConfig::default(); + m.ort_dylib_path = "/usr/lib/libonnxruntime.so".into(); assert_eq!( m.resolved_ort_dylib_path().unwrap().to_str().unwrap(), "/usr/lib/libonnxruntime.so" diff --git a/breadpad-shared/tests/pipeline.rs b/breadpad-shared/tests/pipeline.rs index 603a199..84cc850 100644 --- a/breadpad-shared/tests/pipeline.rs +++ b/breadpad-shared/tests/pipeline.rs @@ -9,18 +9,12 @@ use breadpad_shared::classifier::Classifier; use breadpad_shared::store::Store; use breadpad_shared::types::{Note, NoteType}; use chrono::Timelike; -use std::path::PathBuf; use tempfile::TempDir; // Mirrors commit_note() in breadpad/src/main.rs. // `user_type` is the type the user selected in the chip row (default = NoteType::Note). fn capture(store: &Store, text: &str, user_type: NoteType) -> Note { - // WHY: pipeline tests cover classify→save→reload, not a host ONNX model. - let mut classifier = Classifier::load_with_paths( - "08:00", - PathBuf::from("/nonexistent/classifier.onnx"), - PathBuf::from("/nonexistent/tokenizer.json"), - ); + let mut classifier = Classifier::load("08:00"); let result = classifier.classify(text); let mut note = Note::new(text.into(), user_type.clone(), None); @@ -67,11 +61,7 @@ fn todo_note_appears_in_store() { #[test] fn idea_note_appears_in_store() { let (dir, store) = setup(); - capture( - &store, - "what if we added dark mode", - NoteType::from_str("note"), - ); + capture(&store, "what if we added dark mode", NoteType::from_str("note")); let notes = breadman_store(&dir).load_all().unwrap(); assert_eq!(notes.len(), 1); @@ -81,11 +71,7 @@ fn idea_note_appears_in_store() { #[test] fn question_note_appears_in_store() { let (dir, store) = setup(); - capture( - &store, - "why does the cache miss on cold start?", - NoteType::from_str("note"), - ); + capture(&store, "why does the cache miss on cold start?", NoteType::from_str("note")); let notes = breadman_store(&dir).load_all().unwrap(); assert_eq!(notes.len(), 1); @@ -111,10 +97,7 @@ fn reminder_has_time_set() { let notes = breadman_store(&dir).load_all().unwrap(); assert_eq!(notes[0].note_type, NoteType::Reminder); - assert!( - notes[0].time.is_some(), - "reminder should have a scheduled time" - ); + assert!(notes[0].time.is_some(), "reminder should have a scheduled time"); let local: chrono::DateTime = notes[0].time.unwrap().into(); assert_eq!(local.hour(), 18); } @@ -125,21 +108,14 @@ fn reminder_body_has_time_stripped() { capture(&store, "call mum at 6pm", NoteType::from_str("note")); let notes = breadman_store(&dir).load_all().unwrap(); - assert!( - !notes[0].body.contains("6pm"), - "time phrase should be removed from body" - ); + assert!(!notes[0].body.contains("6pm"), "time phrase should be removed from body"); assert!(notes[0].body.contains("call mum")); } #[test] fn in_duration_reminder_has_time() { let (dir, store) = setup(); - capture( - &store, - "check on the build in 30 minutes", - NoteType::from_str("note"), - ); + capture(&store, "check on the build in 30 minutes", NoteType::from_str("note")); let notes = breadman_store(&dir).load_all().unwrap(); assert_eq!(notes[0].note_type, NoteType::Reminder); @@ -151,11 +127,7 @@ fn in_duration_reminder_has_time() { #[test] fn recurring_reminder_has_rrule() { let (dir, store) = setup(); - capture( - &store, - "standup every monday at 9am", - NoteType::from_str("note"), - ); + capture(&store, "standup every monday at 9am", NoteType::from_str("note")); let notes = breadman_store(&dir).load_all().unwrap(); assert_eq!(notes[0].note_type, NoteType::Reminder); @@ -167,20 +139,11 @@ fn recurring_reminder_has_rrule() { #[test] fn daily_reminder_has_rrule() { let (dir, store) = setup(); - capture( - &store, - "drink water every day at 8am", - NoteType::from_str("note"), - ); + capture(&store, "drink water every day at 8am", NoteType::from_str("note")); let notes = breadman_store(&dir).load_all().unwrap(); assert_eq!(notes[0].note_type, NoteType::Reminder); - assert!(notes[0] - .rrule - .as_ref() - .unwrap() - .as_str() - .contains("FREQ=DAILY")); + assert!(notes[0].rrule.as_ref().unwrap().as_str().contains("FREQ=DAILY")); } // ---- user-forced type is respected ---- @@ -192,11 +155,7 @@ fn user_selected_type_overrides_classifier() { capture(&store, "fix the login bug", NoteType::Idea); let notes = breadman_store(&dir).load_all().unwrap(); - assert_eq!( - notes[0].note_type, - NoteType::Idea, - "user chip selection should win over classifier" - ); + assert_eq!(notes[0].note_type, NoteType::Idea, "user chip selection should win over classifier"); } #[test] @@ -214,11 +173,7 @@ fn user_selected_reminder_overrides_classifier() { fn three_notes_all_visible_to_breadman() { let (dir, store) = setup(); capture(&store, "buy milk", NoteType::from_str("note")); - capture( - &store, - "what if we rewrote in Zig", - NoteType::from_str("note"), - ); + capture(&store, "what if we rewrote in Zig", NoteType::from_str("note")); capture(&store, "team standup went well", NoteType::from_str("note")); let notes = breadman_store(&dir).load_all().unwrap(); diff --git a/breadpad-test/src/main.rs b/breadpad-test/src/main.rs index 4c96a43..54a7d03 100644 --- a/breadpad-test/src/main.rs +++ b/breadpad-test/src/main.rs @@ -389,7 +389,7 @@ fn cmd_show(index: usize, corpus_path: &Path, tier: &TierArg) -> Result<()> { println!(); let sep = "─".repeat(62); - println!("{:<14} {:<26} actual", "field", "expected"); + println!("{:<14} {:<26} {}", "field", "expected", "actual"); println!("{sep}"); println!( "{:<14} {:<26} {}", diff --git a/breadpad/Cargo.toml b/breadpad/Cargo.toml index 1465ac4..c835bb9 100644 --- a/breadpad/Cargo.toml +++ b/breadpad/Cargo.toml @@ -11,11 +11,6 @@ path = "src/main.rs" [dependencies] breadpad-shared = { path = "../breadpad-shared" } -# Capture primitives for `--screenshot` mode — see src/screenshot.rs. -bread-screenshots = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.2" } -# Bread event bus: emit after a real capture/reminder fire. Fail-silent if -# breadd is down. See EVENTS.md. -bread-utils = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.2", features = ["bread-client"] } anyhow.workspace = true ort.workspace = true tracing.workspace = true diff --git a/breadpad/src/listen.rs b/breadpad/src/listen.rs deleted file mode 100644 index e8649a5..0000000 --- a/breadpad/src/listen.rs +++ /dev/null @@ -1,83 +0,0 @@ -//! Long-running command subscription for `bread.command.pad.*`. -//! -//! `breadpad` is still a one-shot capture popup by default. `breadpad listen` -//! is the optional persistent process that can honor bus commands. See -//! `EVENTS.md`. - -use anyhow::Result; -use bread_utils::bread_client::{BreadClient, BreadEvent}; - -/// Sibling-app id in `bread_shared::apps::KNOWN_APPS`. -const APP_ID: &str = "pad"; - -/// Subscribe to `bread.command.pad.**` 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() -> Result<()> { - let client = BreadClient::connect(APP_ID); - if client.health().is_none() { - tracing::warn!("breadd unreachable; command subscription will connect when it comes back"); - } - - let _commands = client.subscribe("bread.command.pad.**", |event| { - handle_command(&event); - }); - - tracing::info!("listening for bread.command.pad.**"); - loop { - std::thread::park(); - } -} - -/// Reacts to `bread.command.pad.*` verbs. Only `capture` 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 { - "capture" => handle_capture(), - other => { - tracing::debug!("ignoring unrecognized command verb '{other}'"); - } - } -} - -fn handle_capture() { - // Same as running `breadpad` with no args: open the capture popup. - let result = spawn_self(); - let client = BreadClient::connect(APP_ID); - match result { - Ok(_) => client.emit("bread.pad.capture.done", serde_json::json!({})), - Err(e) => { - tracing::warn!("bread.command.pad.capture failed: {e}"); - client.emit( - "bread.pad.capture.failed", - serde_json::json!({ "error": e.to_string() }), - ); - } - } -} - -fn spawn_self() -> std::io::Result { - let exe = std::env::current_exe().unwrap_or_else(|_| std::path::PathBuf::from("breadpad")); - std::process::Command::new(exe).spawn() -} - -fn command_verb(event_name: &str) -> Option<&str> { - event_name.strip_prefix("bread.command.pad.") -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn command_verb_strips_pad_prefix() { - assert_eq!(command_verb("bread.command.pad.capture"), Some("capture")); - assert_eq!(command_verb("bread.command.pad.snooze"), Some("snooze")); - assert_eq!(command_verb("bread.command.box.open"), None); - assert_eq!(command_verb("bread.pad.captured"), None); - } -} diff --git a/breadpad/src/main.rs b/breadpad/src/main.rs index db05e04..033a114 100644 --- a/breadpad/src/main.rs +++ b/breadpad/src/main.rs @@ -3,36 +3,16 @@ use breadpad_shared::{ calendar::CalDavClient, classifier::Classifier, config::Config, - parser::parse_rule_based, scheduler::Scheduler, store::Store, types::{Note, NoteType}, }; -use bread_utils::bread_client::BreadClient; use gtk4::{glib, prelude::*}; use gtk4_layer_shell::{Edge, KeyboardMode, Layer, LayerShell}; use std::cell::RefCell; use std::rc::Rc; use std::sync::{Arc, Once}; -mod listen; -mod screenshot; - -/// Sibling-app id in `bread_shared::apps::KNOWN_APPS`. Events are `bread.pad.*`. -const APP_ID: &str = "pad"; - -/// `bread.pad.captured` after a successful quick-capture save. Fire-and-forget: -/// `BreadClient::emit` is a silent no-op when breadd is down or missing. -fn emit_captured(id: &str) { - BreadClient::connect(APP_ID).emit("bread.pad.captured", serde_json::json!({ "id": id })); -} - -/// `bread.pad.reminder.due` when `breadpad fire ` actually shows a reminder -/// (the existing systemd-timer hook — not a new daemon). Same fail-silent emit. -fn emit_reminder_due(id: &str) { - BreadClient::connect(APP_ID).emit("bread.pad.reminder.due", serde_json::json!({ "id": id })); -} - static ORT_INIT: Once = Once::new(); fn init_ort_once(cfg: &Config) { @@ -51,9 +31,6 @@ fn init_ort_once(cfg: &Config) { } mod args { - use bread_utils::screenshot_cli::{validate_pair, DEFAULT_HEIGHT, DEFAULT_WIDTH}; - use std::path::Path; - #[derive(Debug)] pub struct Args { pub note_type: Option, @@ -64,32 +41,6 @@ mod args { pub model_info: bool, pub calendar_test: bool, pub calendar_list_uid: Option, - pub screenshot: Option, - pub output: Option, - pub width: u32, - pub height: u32, - pub listen: bool, - } - - impl Args { - /// `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 { - if let Err(e) = validate_pair( - self.screenshot.as_deref(), - self.output.as_deref().map(Path::new), - ) { - eprintln!("breadpad: {e}"); - std::process::exit(1); - } - Some(crate::screenshot::ScreenshotRequest { - view: self.screenshot.clone()?, - output: self.output.clone()?.into(), - width: self.width, - height: self.height, - }) - } } pub fn parse() -> Args { @@ -102,11 +53,6 @@ mod args { model_info: false, calendar_test: false, calendar_list_uid: None, - screenshot: None, - output: None, - width: DEFAULT_WIDTH, - height: DEFAULT_HEIGHT, - listen: false, }; let raw: Vec = std::env::args().skip(1).collect(); let mut i = 0; @@ -120,7 +66,6 @@ mod args { "--status" => args.status = true, "download-model" => args.download_model = true, "model-info" => args.model_info = true, - "listen" => args.listen = true, "fire" => { i += 1; args.fire_id = raw.get(i).cloned(); @@ -137,26 +82,6 @@ mod args { _ => {} } } - "--screenshot" => { - i += 1; - args.screenshot = raw.get(i).cloned(); - } - "--output" => { - i += 1; - args.output = raw.get(i).cloned(); - } - "--width" => { - i += 1; - if let Some(v) = raw.get(i).and_then(|s| s.parse().ok()) { - args.width = v; - } - } - "--height" => { - i += 1; - if let Some(v) = raw.get(i).and_then(|s| s.parse().ok()) { - args.height = v; - } - } _ => {} } i += 1; @@ -174,9 +99,6 @@ fn main() -> Result<()> { .init(); let args = args::parse(); - if args.listen { - return listen::run(); - } let cfg = Config::load()?; if args.status { @@ -198,23 +120,7 @@ fn main() -> Result<()> { return cmd_calendar_list_uid(¬e_id, &cfg); } - let screenshot_req = args.screenshot_request(); - if let Some(req) = &screenshot_req { - if req.view == "reminder" || req.view == "reminder-snooze" { - // The real path (`fire `, above) needs a real due note from - // the Store. A screenshot doesn't have one to work with — and - // shouldn't wait for one — so it builds a throwaway sample - // instead, never touching the Store at all. - let mut sample = Note::new( - "Sample reminder text".into(), - NoteType::from_str("reminder"), - None, - ); - sample.time = Some(chrono::Utc::now()); - return run_reminder_window(sample, &cfg, screenshot_req); - } - } - run_popup(args.note_type, args.no_classify, cfg, screenshot_req) + run_popup(args.note_type, args.no_classify, cfg) } fn cmd_status(cfg: &Config) -> Result<()> { @@ -359,8 +265,6 @@ fn cmd_fire(id: &str, cfg: &Config) -> Result<()> { return Ok(()); } - emit_reminder_due(¬e.id); - // Schedule next recurrence before showing UI if note.rrule.is_some() { if let Some(next) = Scheduler::next_recurrence(¬e, &cfg.reminders.default_morning) { @@ -372,28 +276,19 @@ fn cmd_fire(id: &str, cfg: &Config) -> Result<()> { } } - run_reminder_window(note, cfg, None) + run_reminder_window(note, cfg) } -fn run_reminder_window( - note: breadpad_shared::types::Note, - cfg: &Config, - screenshot_req: Option, -) -> Result<()> { - let mut builder = gtk4::Application::builder().application_id("com.breadway.breadpad.reminder"); - if screenshot_req.is_some() { - // Same reasoning as run_popup's NON_UNIQUE: a screenshot run must - // get its own fresh window, never activate a real reminder that - // happens to already be showing. - builder = builder.flags(gtk4::gio::ApplicationFlags::NON_UNIQUE); - } - let app = builder.build(); +fn run_reminder_window(note: breadpad_shared::types::Note, cfg: &Config) -> Result<()> { + let app = gtk4::Application::builder() + .application_id("com.breadway.breadpad.reminder") + .build(); let note = Arc::new(note); let cfg = Arc::new(cfg.clone()); app.connect_activate(move |app| { - build_reminder_window(app, note.clone(), cfg.clone(), screenshot_req.clone()); + build_reminder_window(app, note.clone(), cfg.clone()); }); app.run_with_args::(&[]); @@ -436,7 +331,6 @@ fn build_reminder_window( app: >k4::Application, note: Arc, cfg: Arc, - screenshot_req: Option, ) { let window = gtk4::ApplicationWindow::builder() .application(app) @@ -451,7 +345,6 @@ fn build_reminder_window( window.set_layer(Layer::Overlay); window.set_keyboard_mode(KeyboardMode::Exclusive); window.auto_exclusive_zone_enable(); - breadpad_shared::theme::bind_window(&window); apply_css(&cfg); @@ -499,7 +392,7 @@ fn build_reminder_window( let local: chrono::DateTime = t.into(); header.append( >k4::Label::builder() - .label(local.format("%H:%M").to_string()) + .label(&local.format("%H:%M").to_string()) .css_classes(["reminder-time"]) .build(), ); @@ -524,15 +417,14 @@ fn build_reminder_window( .orientation(gtk4::Orientation::Horizontal) .build()); - // Button row — same inset as the header/body zone above (20px), which - // used to be 16px here, visible as a step across the divider. + // Button row let btn_row = gtk4::Box::builder() .orientation(gtk4::Orientation::Horizontal) .spacing(8) .margin_top(12) .margin_bottom(12) - .margin_start(20) - .margin_end(20) + .margin_start(16) + .margin_end(16) .build(); let dismiss_btn = gtk4::Button::builder() @@ -540,35 +432,23 @@ fn build_reminder_window( .css_classes(["reminder-dismiss"]) .build(); - // Snooze popover. No arrow and a matching flat border (see - // popover.snooze-popover in the shared theme) so it reads as the same - // elevation language as the reminder card, instead of GTK's default - // arrow+drop-shadow chrome next to the card's flat 1px border. + // Snooze popover let snooze_popover = gtk4::Popover::new(); - snooze_popover.set_has_arrow(false); - snooze_popover.add_css_class("snooze-popover"); let snooze_vbox = gtk4::Box::builder() .orientation(gtk4::Orientation::Vertical) - .spacing(0) - .margin_top(4) - .margin_bottom(4) + .spacing(4) + .margin_top(8) + .margin_bottom(8) + .margin_start(8) + .margin_end(8) .build(); - // Left-aligned row: a bare Button::builder().label() centers its text, - // so each option gets an explicit xalign(0.0) label as its child and - // hexpand(true) so the row fills the popover's full width instead of - // shrinking to the longest label. - let snooze_option_row = |label: &str| { - gtk4::Button::builder() - .child(>k4::Label::builder().label(label).xalign(0.0).build()) - .css_classes(["snooze-option"]) - .hexpand(true) - .build() - }; - for opt in &cfg.settings.snooze_options { let label = humanize_snooze(opt).to_string(); - let btn = snooze_option_row(&label); + let btn = gtk4::Button::builder() + .label(&label) + .css_classes(["snooze-option"]) + .build(); let key = opt.clone(); let note_c = note.clone(); let cfg_c = cfg.clone(); @@ -588,47 +468,6 @@ fn build_reminder_window( }); snooze_vbox.append(&btn); } - - // Custom… — reuses the same free-form time parsing breadman's dialogs - // already use, rather than inventing a separate time-picker widget. - let custom_entry = gtk4::Entry::builder() - .placeholder_text("tomorrow 9am / in 45 minutes") - .css_classes(["snooze-custom-entry"]) - .visible(false) - .build(); - { - let note_c = note.clone(); - let cfg_c = cfg.clone(); - let win_c = window.clone(); - let popover_c = snooze_popover.clone(); - let entry_c = custom_entry.clone(); - custom_entry.connect_activate(move |_| { - let text = entry_c.text().to_string(); - let parsed = parse_rule_based(&text, &cfg_c.reminders.default_morning); - if let Some(until) = parsed.time { - if let Ok(store) = Store::new().map(|s| s.with_calendar_if_enabled(&cfg_c)) { - let mut updated = note_c.as_ref().clone(); - updated.snoozed_until = Some(until); - let _ = store.update_note(&updated); - let _ = Scheduler::schedule(&updated); - } - popover_c.popdown(); - win_c.close(); - } - }); - } - let custom_btn = snooze_option_row("Custom\u{2026}"); - { - let custom_entry_c = custom_entry.clone(); - custom_btn.connect_clicked(move |btn| { - btn.set_visible(false); - custom_entry_c.set_visible(true); - custom_entry_c.grab_focus(); - }); - } - snooze_vbox.append(&custom_btn); - snooze_vbox.append(&custom_entry); - snooze_popover.set_child(Some(&snooze_vbox)); let snooze_btn = gtk4::MenuButton::builder() @@ -668,48 +507,25 @@ fn build_reminder_window( outer.append(&btn_row); window.set_child(Some(&outer)); - - if let Some(req) = screenshot_req { - if req.view == "reminder-snooze" { - screenshot::capture_with_snooze_open(&window, &req, snooze_popover.clone()); - } else { - screenshot::capture_window(&window, &req); - } - } - window.present(); } -fn run_popup( - preset_type: Option, - no_classify: bool, - cfg: Config, - screenshot_req: Option, -) -> Result<()> { +fn run_popup(preset_type: Option, no_classify: bool, cfg: Config) -> Result<()> { // Try to get current Hyprland workspace let workspace = get_active_workspace(); - let mut builder = gtk4::Application::builder().application_id("com.breadway.breadpad"); - if screenshot_req.is_some() { - // GApplication is single-instance by default; this machine typically - // already has a real breadpad instance, so without this a - // screenshot run would just toggle-close the *existing* instance's - // window instead of starting a fresh one that ever sees - // `screenshot_req` (see the `app.windows().first()` toggle below). - builder = builder.flags(gtk4::gio::ApplicationFlags::NON_UNIQUE); - } - let app = builder.build(); + let app = gtk4::Application::builder() + .application_id("com.breadway.breadpad") + .build(); let cfg = Arc::new(cfg); app.connect_activate(move |app| { - if screenshot_req.is_none() { - if let Some(win) = app.windows().first().cloned() { - win.close(); - return; - } + if let Some(win) = app.windows().first().cloned() { + win.close(); + return; } - build_window(app, cfg.clone(), workspace.clone(), preset_type.clone(), no_classify, screenshot_req.clone()); + build_window(app, cfg.clone(), workspace.clone(), preset_type.clone(), no_classify); }); let code = app.run_with_args::(&[]); @@ -735,7 +551,6 @@ fn build_window( workspace: Option, preset_type: Option, no_classify: bool, - screenshot_req: Option, ) { let window = gtk4::ApplicationWindow::builder() .application(app) @@ -754,7 +569,6 @@ fn build_window( window.set_anchor(Edge::Bottom, false); window.set_anchor(Edge::Left, false); window.set_anchor(Edge::Right, false); - breadpad_shared::theme::bind_window(&window); apply_css(&cfg); @@ -822,15 +636,44 @@ fn build_window( } } - // No submit button — this popup is keyboard-driven (grabs focus on - // open, Escape closes it) and a bare accent-teal checkmark used to sit - // next to the selected-type pill in the same accent teal, carrying two - // different meanings in one colour. A hint is enough. - let enter_hint = gtk4::Label::builder() - .label("Press Enter to add") - .css_classes(["dim-label"]) + // Live prefix grammar: typing "td: ", "rem: ", "idea: ", "note: ", or + // "q: " at the start of the entry drives the chip selection without + // ever touching the mouse — the chips become feedback for what you + // typed rather than the only way to pick a type. + { + let selected_type_clone = selected_type.clone(); + let chips_clone: Vec<(gtk4::Button, NoteType)> = chips.clone(); + entry.connect_changed(move |e| { + let Some(nt) = breadpad_shared::parser::detect_prefix_type(&e.text()) else { + return; + }; + *selected_type_clone.borrow_mut() = nt.clone(); + for (btn, chip_nt) in &chips_clone { + if *chip_nt == nt { + btn.add_css_class("active"); + } else { + btn.remove_css_class("active"); + } + } + }); + } + + let hint = gtk4::Label::builder() + .label("td: · rem: · idea: · note: · q:") + .css_classes(["prefix-hint"]) + .xalign(0.0) .build(); + // Confirm button + let confirm_btn = gtk4::Button::builder() + .label("✓") + .css_classes(["confirm-button"]) + .build(); + let confirm_wrap = gtk4::Box::builder() + .css_classes(["confirm-wrap"]) + .build(); + confirm_wrap.append(&confirm_btn); + let bottom_row = gtk4::Box::builder() .orientation(gtk4::Orientation::Horizontal) .spacing(8) @@ -839,9 +682,10 @@ fn build_window( let spacer = gtk4::Box::builder().hexpand(true).build(); bottom_row.append(&spacer); - bottom_row.append(&enter_hint); + bottom_row.append(&confirm_wrap); vbox.append(&entry); + vbox.append(&hint); vbox.append(&bottom_row); window.set_child(Some(&vbox)); @@ -875,6 +719,12 @@ fn build_window( } }; + // Confirm button click + { + let save = save_and_close.clone(); + confirm_btn.connect_clicked(move |_| save()); + } + // Entry activate (Enter key) { let save = save_and_close.clone(); @@ -893,10 +743,6 @@ fn build_window( }); window.add_controller(key_ctrl); - if let Some(req) = screenshot_req { - screenshot::dispatch(&window, req); - } - window.present(); entry.grab_focus(); } @@ -943,7 +789,6 @@ fn save_note_classified( tracing::error!("failed to save note: {}", e); return; } - emit_captured(¬e.id); if note.time.is_some() { if let Err(e) = Scheduler::schedule(¬e) { tracing::warn!("failed to schedule reminder: {}", e); diff --git a/breadpad/src/screenshot.rs b/breadpad/src/screenshot.rs deleted file mode 100644 index 98f5ab0..0000000 --- a/breadpad/src/screenshot.rs +++ /dev/null @@ -1,110 +0,0 @@ -//! `--screenshot` CLI mode: render breadpad's compose popup, capture it via -//! `bread-screenshots`, then exit — driven by `bread-ecosystem`'s -//! `bread-capture` orchestrator, or run standalone for one-off captures. -//! -//! No clap here (unlike breadbar/breadbox/breadclip/breadsearch): breadpad -//! already has its own small hand-rolled flag parser (`mod args`) covering -//! `--type`/`--no-classify`/`--status`/`fire`/`calendar`/etc, and clap's -//! default "reject unknown flags" behavior would break every one of those -//! if bolted on as a second, separate parser. `--screenshot`/`--output`/ -//! `--width`/`--height` are just three more fields on that same `Args` -//! struct instead. -//! -//! Three views: "popup" (the compose window from `run_popup`), "reminder" -//! (the alert window from `run_reminder_window`/`build_reminder_window`, -//! normally only reachable via a real due note through `fire `, built -//! here against a fabricated sample `Note` instead — see `main`'s -//! `screenshot_req.view == "reminder"` branch, which skips the Store lookup -//! entirely), and "reminder-snooze" (the same window with its snooze -//! popover open). - -use bread_utils::screenshot_cli::SETTLE_DELAY; -use gtk4::prelude::*; -use std::path::PathBuf; -use std::time::Duration; - -/// Delay before popping the snooze popover open — same reasoning as every -/// other app's PRE_POPUP_DELAY: the parent window's own layout needs a beat -/// to settle first. -const PRE_POPUP_DELAY: Duration = SETTLE_DELAY; - -#[derive(Clone)] -pub struct ScreenshotRequest { - pub view: String, - pub output: PathBuf, - pub width: u32, - pub height: u32, -} - -/// 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 popup UI. -pub fn dispatch(window: >k4::ApplicationWindow, req: ScreenshotRequest) { - match req.view.as_str() { - "popup" => { - 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!("breadpad: unknown screenshot view '{other}' (known: popup, reminder, reminder-snooze)"); - std::process::exit(1); - } - } -} - -/// Same shape as `dispatch`'s "popup" arm, for the reminder window itself -/// (view "reminder") — pulled out since `build_reminder_window` calls this -/// directly rather than going through `dispatch` (the reminder window is -/// built via a completely separate `run_reminder_window` entry point, not -/// `run_popup`'s). -pub fn capture_window(window: >k4::ApplicationWindow, req: &ScreenshotRequest) { - let output = req.output.clone(); - 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)); - }); - }); -} - -/// View "reminder-snooze": force the snooze popover open shortly after the -/// window maps, then capture once *it* maps. -pub fn capture_with_snooze_open( - window: >k4::ApplicationWindow, - req: &ScreenshotRequest, - snooze_popover: gtk4::Popover, -) { - let output = req.output.clone(); - let (width, height) = (req.width as i32, req.height as i32); - let popover_to_open = snooze_popover.clone(); - window.connect_map(move |_| { - popover_to_open.set_autohide(false); - let popover_to_open = popover_to_open.clone(); - gtk4::glib::timeout_add_local_once(PRE_POPUP_DELAY, move || { - popover_to_open.popup(); - }); - }); - snooze_popover.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)); - }); - }); -} - -fn finish(result: anyhow::Result<()>) { - match result { - Ok(()) => std::process::exit(0), - Err(e) => { - eprintln!("breadpad: screenshot capture failed: {e}"); - std::process::exit(1); - } - } -} diff --git a/ci/bread-ecosystem.rev b/ci/bread-ecosystem.rev deleted file mode 100644 index 474f1fd..0000000 --- a/ci/bread-ecosystem.rev +++ /dev/null @@ -1 +0,0 @@ -620c5a1317a6b57276eabca961facdb78bf510db diff --git a/ci/build.sh b/ci/build.sh deleted file mode 100755 index 4056cce..0000000 --- a/ci/build.sh +++ /dev/null @@ -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" breadpad "$ROOT" "$@" diff --git a/packaging/arch/PKGBUILD b/packaging/arch/PKGBUILD new file mode 100644 index 0000000..22511b6 --- /dev/null +++ b/packaging/arch/PKGBUILD @@ -0,0 +1,38 @@ +# Maintainer: Breadway + +pkgname=breadpad +pkgver=0.3.1 +pkgrel=1 +pkgdesc="Quick-capture scratchpad and note viewer with AI classification" +arch=('x86_64') +url="https://github.com/Breadway/breadpad" +license=('MIT') +# Some Rust deps (ring/mlua) build vendored C/asm into static archives; makepkg's +# default -flto=auto emits GCC LTO bitcode the Rust (lld) link cannot read, +# causing undefined-symbol errors. Disable LTO. +options=(!lto !debug) +depends=('gtk4' 'gtk4-layer-shell') +optdepends=( + 'ollama: local AI note classification' + 'hyprland: scratchpad window integration' +) +makedepends=('rust' 'cargo') +source=("${pkgname}-${pkgver}.tar.gz") +sha256sums=('SKIP') + +build() { + cd "${srcdir}/${pkgname}-${pkgver}" + cargo build --release --locked +} + +check() { + cd "${srcdir}/${pkgname}-${pkgver}" + cargo test --release --locked --workspace +} + +package() { + cd "${srcdir}/${pkgname}-${pkgver}" + install -Dm755 target/release/breadpad "${pkgdir}/usr/bin/breadpad" + install -Dm755 target/release/breadman "${pkgdir}/usr/bin/breadman" + install -Dm644 LICENSE "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" +}