Compare commits
No commits in common. "main" and "v0.2.1" have entirely different histories.
43 changed files with 276 additions and 2029 deletions
|
|
@ -1,24 +0,0 @@
|
||||||
name: check
|
|
||||||
|
|
||||||
# Fast-fail lint/test on short-lived work branches, before it ever reaches
|
|
||||||
# main and triggers a dev-track release build.
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: ['feature/**', 'fix/**']
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
check:
|
|
||||||
runs-on: [self-hosted, hestia]
|
|
||||||
steps:
|
|
||||||
- name: checkout
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
rm -rf src && mkdir src
|
|
||||||
git clone --branch "${GITHUB_REF_NAME}" --depth 1 \
|
|
||||||
"https://git.breadway.dev/${GITHUB_REPOSITORY}.git" src
|
|
||||||
|
|
||||||
- name: clippy
|
|
||||||
run: cd src && bash ci/build.sh cargo clippy --all-targets --locked -- -D warnings
|
|
||||||
|
|
||||||
- name: test
|
|
||||||
run: cd src && bash ci/build.sh cargo test --locked
|
|
||||||
|
|
@ -1,80 +0,0 @@
|
||||||
name: dev release
|
|
||||||
|
|
||||||
# Publishes a dev-track build on every push to `main` (the trunk
|
|
||||||
# branch — there is no separate `dev` branch). See bread-ecosystem's
|
|
||||||
# docs/release-channels.md for the release-track policy this is part of.
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: ['main']
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: [self-hosted, hestia]
|
|
||||||
steps:
|
|
||||||
- name: checkout
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
rm -rf src && mkdir src
|
|
||||||
git clone --branch main --depth 1 \
|
|
||||||
"https://git.breadway.dev/${GITHUB_REPOSITORY}.git" src
|
|
||||||
|
|
||||||
- name: build
|
|
||||||
run: cd src && bash ci/build.sh cargo build --release --locked
|
|
||||||
|
|
||||||
- name: test
|
|
||||||
run: cd src && bash ci/build.sh cargo test --release --locked
|
|
||||||
|
|
||||||
- name: compute dev version
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
cd src
|
|
||||||
# Base the dev version off the latest published stable tag,
|
|
||||||
# not Cargo.toml — Cargo.toml can go stale relative to the last
|
|
||||||
# real release, which would make a dev build sort as OLDER than
|
|
||||||
# what's already installed and bakery would correctly refuse it.
|
|
||||||
LATEST_TAG="$(git ls-remote --tags --refs \
|
|
||||||
"https://git.breadway.dev/${GITHUB_REPOSITORY}.git" 'v*' \
|
|
||||||
| awk -F/ '{print $NF}' | sed 's/^v//' | (grep -v -- '-' || true) | sort -V | tail -1)"
|
|
||||||
if [ -n "${LATEST_TAG}" ]; then
|
|
||||||
CUR="${LATEST_TAG}"
|
|
||||||
else
|
|
||||||
CUR="$(grep -m1 '^version' 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/breadhelp/${VERSION}"
|
|
||||||
mkdir -p "${PKG_DIR}"
|
|
||||||
cp "src/target/release/breadhelp" "${PKG_DIR}/breadhelp-x86_64"
|
|
||||||
strip "${PKG_DIR}/breadhelp-x86_64"
|
|
||||||
sha256sum "${PKG_DIR}/breadhelp-x86_64" | awk '{print $1}' \
|
|
||||||
> "${PKG_DIR}/breadhelp-x86_64.sha256"
|
|
||||||
cp src/packaging/breadhelp.desktop "${PKG_DIR}/"
|
|
||||||
cp src/LICENSE "${PKG_DIR}/"
|
|
||||||
tar czf "${PKG_DIR}/content.tar.gz" -C src content
|
|
||||||
cp src/bakery.toml "${PKG_DIR}/bakery.toml"
|
|
||||||
ln -sfn "${VERSION}" "/srv/breadway-dl/dev/breadhelp/latest"
|
|
||||||
|
|
||||||
# No GitHub Release upload — dev, like the other non-stable track,
|
|
||||||
# is only distributed via dl.breadway.dev/dev/.
|
|
||||||
- name: regenerate dev index.json
|
|
||||||
env:
|
|
||||||
MINISIGN_SEC_KEY: ${{ secrets.BAKERY_MINISIGN_SEC_KEY_PATH }}
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
if [ -z "${MINISIGN_SEC_KEY:-}" ]; then
|
|
||||||
echo "::error::BAKERY_MINISIGN_SEC_KEY_PATH secret not set — refusing to regenerate dev index.json unsigned (would leave a stale signature mismatched against fresh content and break bakery for everyone on the dev track)"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
# Do not wipe /tmp/bread-ecosystem-ci-* — other products' ci/build.sh
|
|
||||||
# caches live there and a wipe between their build and test steps
|
|
||||||
# is a 127 (missing ci/build.sh).
|
|
||||||
ECOSYSTEM_CI_DIR="$(mktemp -d /tmp/bread-ecosystem-index-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}"
|
|
||||||
40
.forgejo/workflows/package.yml
Normal file
40
.forgejo/workflows/package.yml
Normal file
|
|
@ -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 glib2 gtk4-layer-shell hicolor-icon-theme
|
||||||
|
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="breadhelp-${VERSION}/" HEAD \
|
||||||
|
> packaging/breadhelp-${VERSION}.tar.gz
|
||||||
|
SHA=$(sha256sum packaging/breadhelp-${VERSION}.tar.gz | awk '{print $1}')
|
||||||
|
sed -i "s/^pkgver=.*/pkgver=${VERSION}/" packaging/PKGBUILD
|
||||||
|
sed -i "s/^sha256sums=.*/sha256sums=('${SHA}')/" packaging/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 && makepkg -f --noconfirm --nocheck"
|
||||||
|
PKG=$(find /home/builder/src/packaging -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"
|
||||||
|
|
@ -1,62 +0,0 @@
|
||||||
name: beta (rc) release
|
|
||||||
|
|
||||||
# Publishes a beta-track build for any `vX.Y.Z-rc.N` prerelease tag
|
|
||||||
# pushed to `main` — there is no separate `beta` branch; "freezing" is
|
|
||||||
# just pausing pushes to main while an RC gets tested. See
|
|
||||||
# bread-ecosystem's docs/release-channels.md for the release-track policy.
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags: ['v*']
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
if: ${{ contains(github.ref_name, '-rc.') }}
|
|
||||||
runs-on: [self-hosted, hestia]
|
|
||||||
steps:
|
|
||||||
- name: checkout
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
rm -rf src && mkdir src
|
|
||||||
git clone --branch "${GITHUB_REF_NAME}" --depth 1 \
|
|
||||||
"https://git.breadway.dev/${GITHUB_REPOSITORY}.git" src
|
|
||||||
|
|
||||||
- name: build
|
|
||||||
run: cd src && bash ci/build.sh cargo build --release --locked
|
|
||||||
|
|
||||||
- name: test
|
|
||||||
run: cd src && cargo test --release --locked
|
|
||||||
|
|
||||||
- name: prepare artifacts
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
VERSION="${GITHUB_REF_NAME#v}"
|
|
||||||
PKG_DIR="/srv/breadway-dl/beta/breadhelp/${VERSION}"
|
|
||||||
mkdir -p "${PKG_DIR}"
|
|
||||||
cp "src/target/release/breadhelp" "${PKG_DIR}/breadhelp-x86_64"
|
|
||||||
strip "${PKG_DIR}/breadhelp-x86_64"
|
|
||||||
sha256sum "${PKG_DIR}/breadhelp-x86_64" | awk '{print $1}' \
|
|
||||||
> "${PKG_DIR}/breadhelp-x86_64.sha256"
|
|
||||||
cp src/packaging/breadhelp.desktop "${PKG_DIR}/"
|
|
||||||
cp src/LICENSE "${PKG_DIR}/"
|
|
||||||
tar czf "${PKG_DIR}/content.tar.gz" -C src content
|
|
||||||
cp src/bakery.toml "${PKG_DIR}/bakery.toml"
|
|
||||||
ln -sfn "${VERSION}" "/srv/breadway-dl/beta/breadhelp/latest"
|
|
||||||
|
|
||||||
# No GitHub Release upload — beta, like dev, is only distributed via
|
|
||||||
# dl.breadway.dev/beta/.
|
|
||||||
- name: regenerate beta index.json
|
|
||||||
env:
|
|
||||||
MINISIGN_SEC_KEY: ${{ secrets.BAKERY_MINISIGN_SEC_KEY_PATH }}
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
if [ -z "${MINISIGN_SEC_KEY:-}" ]; then
|
|
||||||
echo "::error::BAKERY_MINISIGN_SEC_KEY_PATH secret not set — refusing to regenerate beta index.json unsigned (would leave a stale signature mismatched against fresh content and break bakery for everyone on the beta track)"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
rm -rf /tmp/bread-ecosystem-ci-* 2>/dev/null || true
|
|
||||||
# mktemp: a fixed clone path races when multiple repos' dev/beta
|
|
||||||
# workflows run close together on the same self-hosted runner.
|
|
||||||
ECOSYSTEM_CI_DIR="$(mktemp -d /tmp/bread-ecosystem-ci-XXXXXX)"
|
|
||||||
git clone https://git.breadway.dev/Breadway/bread-ecosystem.git "${ECOSYSTEM_CI_DIR}"
|
|
||||||
TRACK=beta bash "${ECOSYSTEM_CI_DIR}/scripts/gen-index.sh"
|
|
||||||
rm -rf "${ECOSYSTEM_CI_DIR}"
|
|
||||||
|
|
@ -1,77 +0,0 @@
|
||||||
name: release
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags: ["v*"]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
if: ${{ !contains(github.ref_name, '-rc.') }}
|
|
||||||
runs-on: [self-hosted, hestia]
|
|
||||||
steps:
|
|
||||||
- name: checkout
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
rm -rf src && mkdir src
|
|
||||||
git clone --branch "${GITHUB_REF_NAME}" --depth 1 \
|
|
||||||
"https://git.breadway.dev/${GITHUB_REPOSITORY}.git" src
|
|
||||||
|
|
||||||
- name: build
|
|
||||||
run: |
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
- name: test
|
|
||||||
run: cd src && bash ci/build.sh cargo test --release --locked
|
|
||||||
|
|
||||||
- name: prepare artifacts
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
VERSION="${GITHUB_REF_NAME#v}"
|
|
||||||
PKG_DIR="/srv/breadway-dl/breadhelp/${VERSION}"
|
|
||||||
mkdir -p "${PKG_DIR}"
|
|
||||||
cp "src/target/release/breadhelp" "${PKG_DIR}/breadhelp-x86_64"
|
|
||||||
strip "${PKG_DIR}/breadhelp-x86_64"
|
|
||||||
sha256sum "${PKG_DIR}/breadhelp-x86_64" | awk '{print $1}' \
|
|
||||||
> "${PKG_DIR}/breadhelp-x86_64.sha256"
|
|
||||||
cp src/packaging/breadhelp.desktop "${PKG_DIR}/"
|
|
||||||
cp src/LICENSE "${PKG_DIR}/"
|
|
||||||
tar czf "${PKG_DIR}/content.tar.gz" -C src content
|
|
||||||
cp src/bakery.toml "${PKG_DIR}/bakery.toml"
|
|
||||||
ln -sfn "${VERSION}" "/srv/breadway-dl/breadhelp/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-* 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}"
|
|
||||||
bash "${ECOSYSTEM_CI_DIR}/scripts/gen-index.sh"
|
|
||||||
rm -rf "${ECOSYSTEM_CI_DIR}"
|
|
||||||
|
|
||||||
- name: upload to GitHub Release
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GH_RELEASE_TOKEN }}
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
VERSION="${GITHUB_REF_NAME#v}"
|
|
||||||
PKG_DIR="/srv/breadway-dl/breadhelp/${VERSION}"
|
|
||||||
gh release create "${GITHUB_REF_NAME}" --repo Breadway/breadhelp \
|
|
||||||
--title "breadhelp v${VERSION}" --generate-notes 2>/dev/null || true
|
|
||||||
gh release upload "${GITHUB_REF_NAME}" --repo Breadway/breadhelp \
|
|
||||||
"${PKG_DIR}/breadhelp-x86_64" \
|
|
||||||
"${PKG_DIR}/breadhelp-x86_64.sha256" \
|
|
||||||
--clobber
|
|
||||||
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -38,8 +38,3 @@ logs/
|
||||||
|
|
||||||
# Claude Code local agent state
|
# Claude Code local agent state
|
||||||
.claude/
|
.claude/
|
||||||
|
|
||||||
# Local hygiene notes (not for commit)
|
|
||||||
|
|
||||||
# graphify knowledge-graph output (local tool cache, not for commit)
|
|
||||||
graphify-out/
|
|
||||||
|
|
|
||||||
23
AGENTS.md
23
AGENTS.md
|
|
@ -1,23 +0,0 @@
|
||||||
# AGENTS.md — Repo hygiene
|
|
||||||
|
|
||||||
Scope: this file covers *repo hygiene* — branching, remotes, CI, cleanup. It is not project documentation.
|
|
||||||
|
|
||||||
## Identity
|
|
||||||
- breadhelp is a **bakery** product (`bakery.toml` + bakery CI + BOS ISO bake). It is not pacman-packaged.
|
|
||||||
|
|
||||||
## Branch model
|
|
||||||
- One long-lived branch: **`main`**. There is no separate `dev` or `beta` branch — those are bakery tracks, published from `main` and from tags. Follow `CONTRIBUTING.md`.
|
|
||||||
- Short-lived work: `feature/<name>` or `fix/<name>` off `main`.
|
|
||||||
|
|
||||||
## Remotes
|
|
||||||
- `origin` — Forgejo on Hestia only (`ssh://git@100.66.238.26:2222/Breadway/breadhelp.git`), via Tailscale. No GitHub mirror, unlike most bread-ecosystem repos.
|
|
||||||
|
|
||||||
## CI
|
|
||||||
- `.forgejo/workflows/dev-release.yml` — push to `main` (bakery dev track).
|
|
||||||
- `.forgejo/workflows/rc-release.yml` — `vX.Y.Z-rc.N` tags (bakery beta track).
|
|
||||||
- `.forgejo/workflows/release.yml` — other `v*` tags (signed bakery stable).
|
|
||||||
- There is no `package.yml` / pacman publish workflow.
|
|
||||||
|
|
||||||
## Don't
|
|
||||||
- Don't embed credentials in remote URLs — this repo already uses SSH, keep it that way.
|
|
||||||
- Don't start a Tauri port — this stays GTK4.
|
|
||||||
|
|
@ -1,84 +0,0 @@
|
||||||
# Contributing
|
|
||||||
|
|
||||||
`breadhelp` — Onboarding and help center for Bread OS.
|
|
||||||
|
|
||||||
Part of the bread ecosystem; this repo follows the same branch/release
|
|
||||||
workflow as every other ecosystem product.
|
|
||||||
|
|
||||||
## Branches
|
|
||||||
|
|
||||||
There is one long-lived branch: **`main`**. All day-to-day work lands here.
|
|
||||||
Every push to `main` automatically builds and publishes a **dev-track**
|
|
||||||
build (see Tracks below) — a real install you can test before cutting
|
|
||||||
anything more formal.
|
|
||||||
|
|
||||||
New work — features and bug fixes alike — goes on a short-lived branch:
|
|
||||||
|
|
||||||
```
|
|
||||||
feature/<short-name>
|
|
||||||
fix/<issue-number-or-short-name>
|
|
||||||
```
|
|
||||||
|
|
||||||
Branch off `main`, open a PR/push back into `main` when ready. Short-lived
|
|
||||||
branches get deleted on merge — they never accumulate the kind of drift a
|
|
||||||
second long-lived branch does.
|
|
||||||
|
|
||||||
## The release cycle
|
|
||||||
|
|
||||||
There's no separate `beta` or release branch — "stable" and "beta" are both
|
|
||||||
just **tags** on `main`, not branches that need to be kept in sync:
|
|
||||||
|
|
||||||
1. Work accumulates on `main` via `feature/x` / `fix/x` branches. Each push
|
|
||||||
auto-publishes a dev build — install it with `bakery track set dev` and
|
|
||||||
`bakery update --all`, then fix anything broken with another push.
|
|
||||||
2. When you want to stabilize before a real release, tag a release
|
|
||||||
candidate: `git tag vX.Y.Z-rc.1 && git push origin vX.Y.Z-rc.1` (push to
|
|
||||||
both remotes). That tag alone triggers a beta-track build —
|
|
||||||
"freezing" is just pausing pushes to `main` while you test it, not a
|
|
||||||
branch operation. Cut `-rc.2`, `-rc.3`, etc. for further fixes.
|
|
||||||
3. Once an RC has gone without issues, tag the real release:
|
|
||||||
`git tag vX.Y.Z && git push origin vX.Y.Z` — that's what triggers the
|
|
||||||
signed stable release build.
|
|
||||||
|
|
||||||
## Tracks, from a user's perspective
|
|
||||||
|
|
||||||
```
|
|
||||||
bakery track show # what you're currently on (defaults to stable)
|
|
||||||
bakery track set dev # or beta, or stable
|
|
||||||
bakery update --all # pull the latest build on your current track
|
|
||||||
```
|
|
||||||
|
|
||||||
| Track | What it is | Published from |
|
|
||||||
|--------|-----------|-----------------|
|
|
||||||
| `stable` | The last tagged release | a `vX.Y.Z` tag |
|
|
||||||
| `beta` | Latest release candidate | a `vX.Y.Z-rc.N` tag |
|
|
||||||
| `dev` | Bleeding edge | `main`, on every push |
|
|
||||||
|
|
||||||
Dev versions are auto-computed (`X.Y.Z-dev.<timestamp>+<sha>`) from the
|
|
||||||
latest published stable tag, so they always sort as newer than what you
|
|
||||||
have installed — no manual version bumping needed. Beta versions are just
|
|
||||||
the RC tag itself (already valid semver, already sorts below the real
|
|
||||||
release it's a candidate for).
|
|
||||||
|
|
||||||
## Local development
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cargo build --release
|
|
||||||
cargo test --release
|
|
||||||
```
|
|
||||||
|
|
||||||
## CI
|
|
||||||
|
|
||||||
- `dev-release.yml` — triggered on push to `main`.
|
|
||||||
- `rc-release.yml` — triggered on any `vX.Y.Z-rc.N` tag push.
|
|
||||||
- `release.yml` — triggered on any other `v*` tag push, cuts the actual
|
|
||||||
stable release.
|
|
||||||
|
|
||||||
All CI runs on a self-hosted runner; nothing runs automatically on plain
|
|
||||||
commits or PRs beyond the track builds above. See
|
|
||||||
[bread-ecosystem's docs/release-channels.md](https://git.breadway.dev/Breadway/bread-ecosystem/src/branch/main/docs/release-channels.md)
|
|
||||||
for the full policy, including how a new product gets wired onto these tracks.
|
|
||||||
|
|
||||||
## Questions
|
|
||||||
|
|
||||||
Open an issue on this repo's Forgejo tracker.
|
|
||||||
236
Cargo.lock
generated
236
Cargo.lock
generated
|
|
@ -2,12 +2,6 @@
|
||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 4
|
version = 4
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "anyhow"
|
|
||||||
version = "1.0.104"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-channel"
|
name = "async-channel"
|
||||||
version = "2.5.0"
|
version = "2.5.0"
|
||||||
|
|
@ -28,35 +22,14 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "2.13.1"
|
version = "2.13.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
|
checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
|
||||||
|
|
||||||
[[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",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"toml 0.8.23",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bread-theme"
|
name = "bread-theme"
|
||||||
version = "0.7.4"
|
version = "0.2.3"
|
||||||
source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.4#fcba3760387e2523edb71350f8efea3bc851b21e"
|
source = "git+https://github.com/Breadway/bread-ecosystem?tag=v0.2.10#17d1bb85801b9a8c195b64c02d288cd662c9c780"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dirs",
|
"dirs",
|
||||||
"gtk4",
|
"gtk4",
|
||||||
|
|
@ -64,27 +37,12 @@ dependencies = [
|
||||||
"serde_json",
|
"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",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"toml_edit 0.22.27",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "breadhelp"
|
name = "breadhelp"
|
||||||
version = "0.2.6"
|
version = "0.2.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
|
||||||
"async-channel",
|
"async-channel",
|
||||||
"bread-screenshots",
|
|
||||||
"bread-theme",
|
"bread-theme",
|
||||||
"bread-utils",
|
|
||||||
"gdk4",
|
"gdk4",
|
||||||
"glib",
|
"glib",
|
||||||
"gtk4",
|
"gtk4",
|
||||||
|
|
@ -115,7 +73,7 @@ checksum = "f8b4985713047f5faee02b8db6a6ef32bbb50269ff53c1aee716d1d195b76d54"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"glib-sys",
|
"glib-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"system-deps 7.0.8",
|
"system-deps",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -178,10 +136,11 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "event-listener"
|
name = "event-listener"
|
||||||
version = "5.4.2"
|
version = "5.4.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2"
|
checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"concurrent-queue",
|
||||||
"parking",
|
"parking",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
|
|
@ -208,24 +167,24 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-channel"
|
name = "futures-channel"
|
||||||
version = "0.3.34"
|
version = "0.3.32"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4"
|
checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-core"
|
name = "futures-core"
|
||||||
version = "0.3.34"
|
version = "0.3.32"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
|
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-executor"
|
name = "futures-executor"
|
||||||
version = "0.3.34"
|
version = "0.3.32"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432"
|
checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-task",
|
"futures-task",
|
||||||
|
|
@ -234,32 +193,32 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-io"
|
name = "futures-io"
|
||||||
version = "0.3.34"
|
version = "0.3.32"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed"
|
checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-macro"
|
name = "futures-macro"
|
||||||
version = "0.3.34"
|
version = "0.3.32"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44"
|
checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 3.0.3",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-task"
|
name = "futures-task"
|
||||||
version = "0.3.34"
|
version = "0.3.32"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
|
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-util"
|
name = "futures-util"
|
||||||
version = "0.3.34"
|
version = "0.3.32"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
|
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-macro",
|
"futures-macro",
|
||||||
|
|
@ -290,7 +249,7 @@ dependencies = [
|
||||||
"glib-sys",
|
"glib-sys",
|
||||||
"gobject-sys",
|
"gobject-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"system-deps 7.0.8",
|
"system-deps",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -323,7 +282,7 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"pango-sys",
|
"pango-sys",
|
||||||
"pkg-config",
|
"pkg-config",
|
||||||
"system-deps 7.0.8",
|
"system-deps",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -363,7 +322,7 @@ dependencies = [
|
||||||
"glib-sys",
|
"glib-sys",
|
||||||
"gobject-sys",
|
"gobject-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"system-deps 7.0.8",
|
"system-deps",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -417,7 +376,7 @@ dependencies = [
|
||||||
"heck",
|
"heck",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.119",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -427,7 +386,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "030967459f9f676851872c6304adea7825c6d462ec9b72554c733cf0c5952233"
|
checksum = "030967459f9f676851872c6304adea7825c6d462ec9b72554c733cf0c5952233"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"system-deps 7.0.8",
|
"system-deps",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -438,7 +397,7 @@ checksum = "22a861859b887a79cf461359c192c97a57d8fb0229dd291232e57aa11f6fa72c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"glib-sys",
|
"glib-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"system-deps 7.0.8",
|
"system-deps",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -459,7 +418,7 @@ checksum = "5c7ffdfde88f3570d3705e0d8a2433e036d387a1f2930bbf47eafcb5f569fd04"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"glib-sys",
|
"glib-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"system-deps 7.0.8",
|
"system-deps",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -490,7 +449,7 @@ dependencies = [
|
||||||
"graphene-sys",
|
"graphene-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"pango-sys",
|
"pango-sys",
|
||||||
"system-deps 7.0.8",
|
"system-deps",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -516,9 +475,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gtk4-layer-shell"
|
name = "gtk4-layer-shell"
|
||||||
version = "0.8.1"
|
version = "0.8.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "17c28ea0f4676fdaaae7ff2413a24d0d35c8657424f84856c1103c73454c9da4"
|
checksum = "a4069987ff4793699511a251028cc336b438e46565b463f111250148d574752a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"gdk4",
|
"gdk4",
|
||||||
|
|
@ -531,15 +490,15 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gtk4-layer-shell-sys"
|
name = "gtk4-layer-shell-sys"
|
||||||
version = "0.6.1"
|
version = "0.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bcf19bb884ef0ef55b9e6b2b369c39b4fcc0c41e3a0c1cbc8c267720338b690b"
|
checksum = "8f566a5ec5bcc454e7fcf2ab76930887ced5365afce12c1e5201bb296b95f1b9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gdk4-sys",
|
"gdk4-sys",
|
||||||
"glib-sys",
|
"glib-sys",
|
||||||
"gtk4-sys",
|
"gtk4-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"system-deps 8.0.0",
|
"system-deps",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -551,7 +510,7 @@ dependencies = [
|
||||||
"proc-macro-crate",
|
"proc-macro-crate",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.119",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -570,7 +529,7 @@ dependencies = [
|
||||||
"gsk4-sys",
|
"gsk4-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"pango-sys",
|
"pango-sys",
|
||||||
"system-deps 7.0.8",
|
"system-deps",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -609,15 +568,15 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.189"
|
version = "0.2.186"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
|
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libredox"
|
name = "libredox"
|
||||||
version = "0.1.20"
|
version = "0.1.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "28d0a00925a9f930d679b6789b721e3a7f9ed110f41b86d2497caa780c3a070a"
|
checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
@ -643,12 +602,6 @@ dependencies = [
|
||||||
"autocfg",
|
"autocfg",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "once_cell"
|
|
||||||
version = "1.21.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "option-ext"
|
name = "option-ext"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
|
@ -675,7 +628,7 @@ dependencies = [
|
||||||
"glib-sys",
|
"glib-sys",
|
||||||
"gobject-sys",
|
"gobject-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"system-deps 7.0.8",
|
"system-deps",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -692,9 +645,9 @@ checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pkg-config"
|
name = "pkg-config"
|
||||||
version = "0.3.34"
|
version = "0.3.33"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548"
|
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro-crate"
|
name = "proc-macro-crate"
|
||||||
|
|
@ -707,18 +660,18 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.107"
|
version = "1.0.106"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
|
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.47"
|
version = "1.0.46"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
|
checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
@ -751,9 +704,9 @@ checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.229"
|
version = "1.0.228"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
|
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_core",
|
"serde_core",
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
|
|
@ -761,29 +714,29 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_core"
|
name = "serde_core"
|
||||||
version = "1.0.229"
|
version = "1.0.228"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
|
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_derive"
|
name = "serde_derive"
|
||||||
version = "1.0.229"
|
version = "1.0.228"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 3.0.3",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_json"
|
name = "serde_json"
|
||||||
version = "1.0.151"
|
version = "1.0.150"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
|
checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"itoa",
|
"itoa",
|
||||||
"memchr",
|
"memchr",
|
||||||
|
|
@ -833,17 +786,6 @@ dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "syn"
|
|
||||||
version = "3.0.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"unicode-ident",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "system-deps"
|
name = "system-deps"
|
||||||
version = "7.0.8"
|
version = "7.0.8"
|
||||||
|
|
@ -853,20 +795,7 @@ dependencies = [
|
||||||
"cfg-expr",
|
"cfg-expr",
|
||||||
"heck",
|
"heck",
|
||||||
"pkg-config",
|
"pkg-config",
|
||||||
"toml 1.1.4+spec-1.1.0",
|
"toml 1.1.3+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",
|
|
||||||
"version-compare",
|
"version-compare",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -893,7 +822,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.119",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -910,9 +839,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml"
|
name = "toml"
|
||||||
version = "1.1.4+spec-1.1.0"
|
version = "1.1.3+spec-1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5"
|
checksum = "53c96ecdfa941c8fc4fcaed14f99ada8ebed502eef533015095a07e3301d4c3c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"serde_core",
|
"serde_core",
|
||||||
|
|
@ -969,9 +898,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_parser"
|
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"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56"
|
checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"winnow 1.0.4",
|
"winnow 1.0.4",
|
||||||
]
|
]
|
||||||
|
|
@ -988,37 +917,6 @@ version = "1.1.2+spec-1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2"
|
checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tracing"
|
|
||||||
version = "0.1.44"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
|
||||||
dependencies = [
|
|
||||||
"pin-project-lite",
|
|
||||||
"tracing-attributes",
|
|
||||||
"tracing-core",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tracing-attributes"
|
|
||||||
version = "0.1.31"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 2.0.119",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tracing-core"
|
|
||||||
version = "0.1.36"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
|
||||||
dependencies = [
|
|
||||||
"once_cell",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.24"
|
version = "1.0.24"
|
||||||
|
|
@ -1138,9 +1036,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "xml-rs"
|
name = "xml-rs"
|
||||||
version = "0.8.29"
|
version = "0.8.28"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7"
|
checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zmij"
|
name = "zmij"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "breadhelp"
|
name = "breadhelp"
|
||||||
version = "0.2.6"
|
version = "0.2.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|
@ -10,14 +10,10 @@ gtk4-layer-shell = "0.8"
|
||||||
glib = "0.22"
|
glib = "0.22"
|
||||||
# Shared ecosystem theming — same generated stylesheet bos-settings/breadbar/
|
# Shared ecosystem theming — same generated stylesheet bos-settings/breadbar/
|
||||||
# breadbox/breadpad load, so this looks like part of the same desktop.
|
# breadbox/breadpad load, so this looks like part of the same desktop.
|
||||||
bread-theme = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.4", features = ["gtk"] }
|
bread-theme = { git = "https://github.com/Breadway/bread-ecosystem", tag = "v0.2.10", features = ["gtk"] }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
toml = "0.8"
|
toml = "0.8"
|
||||||
# Non-destructive state editing (mirrors bos-settings/src/config/mod.rs).
|
# Non-destructive state editing (mirrors bos-settings/src/config/mod.rs).
|
||||||
toml_edit = "0.22"
|
toml_edit = "0.22"
|
||||||
async-channel = "2"
|
async-channel = "2"
|
||||||
bread-utils = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.2", features = ["toml", "bread-client"] }
|
|
||||||
# Capture primitives for `--screenshot` mode — see src/screenshot.rs.
|
|
||||||
bread-screenshots = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.2" }
|
|
||||||
anyhow = "1"
|
|
||||||
|
|
|
||||||
68
EVENTS.md
68
EVENTS.md
|
|
@ -1,68 +0,0 @@
|
||||||
# breadhelp — bread event integration
|
|
||||||
|
|
||||||
breadhelp is a standalone GTK help center: it works exactly the same with
|
|
||||||
or without `breadd` running. When breadd *is* present, it publishes one
|
|
||||||
event after the main window is actually shown. 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: **`help`**. Transport: `bread-utils`'s `bread_client` module
|
|
||||||
(feature `bread-client`) — breadhelp links it directly. Each `emit` is
|
|
||||||
its own short-lived connection. Command verbs are only received while
|
|
||||||
`breadhelp listen` is running — that process holds the
|
|
||||||
`bread.command.help.**` subscription open.
|
|
||||||
|
|
||||||
## Events published (`bread.help.*`)
|
|
||||||
|
|
||||||
| Event | Data | When |
|
|
||||||
|-------|------|------|
|
|
||||||
| `bread.help.opened` | `{ "autostart": bool }` | The main help window is presented (`ApplicationWindow::present`). `autostart` is `true` when that invocation was launched with `--autostart`. |
|
|
||||||
| `bread.help.open.done` | `{}` | `bread.command.help.open` was received and `breadhelp` was spawned. This is the command confirmation, not proof the window mapped — the spawned process is the same no-args invocation as SUPER+/. |
|
|
||||||
| `bread.help.open.failed` | `{ "error": "<message>" }` | `bread.command.help.open` was received but this binary could not be started. |
|
|
||||||
|
|
||||||
Not emitted when:
|
|
||||||
|
|
||||||
- every-login `--autostart` builds a hidden window because onboarding is
|
|
||||||
already done (silent autostart)
|
|
||||||
- first-run `--autostart` presents the main window on the day-zero
|
|
||||||
wizard; the tour overlay starts after that wizard finishes (or is
|
|
||||||
skipped). `bread.help.opened` **is** emitted for that first-run present.
|
|
||||||
- `--onboard` / `--tour-event` (tour only)
|
|
||||||
- `--screenshot` (capture, not a user-visible open)
|
|
||||||
|
|
||||||
## Commands honored (`bread.command.help.*`)
|
|
||||||
|
|
||||||
These are only received while `breadhelp listen` is running. Publishing a
|
|
||||||
command with no subscriber is a silent no-op — that is the documented
|
|
||||||
bread convention, not a breadhelp bug.
|
|
||||||
|
|
||||||
| Verb | Data | Effect |
|
|
||||||
|------|------|--------|
|
|
||||||
| `open` | none | Same as running `breadhelp` with no flags: present the main help window (GApplication forwards to an already-running primary instance). Emits `bread.help.open.done` / `.failed`. |
|
|
||||||
|
|
||||||
```lua
|
|
||||||
bread.spawn(function()
|
|
||||||
bread.emit("bread.command.help.open")
|
|
||||||
bread.wait("bread.help.open.done", { timeout = 5000 })
|
|
||||||
end)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Not implemented: extra verbs
|
|
||||||
|
|
||||||
There is no `onboard` / `tour` / `suggest` command verb. Those already
|
|
||||||
exist as local CLI flags (`--onboard`, `--tour-event`, `--suggest`).
|
|
||||||
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 — the help
|
|
||||||
center, tour, and screenshots are entirely unaffected.
|
|
||||||
- If breadd restarts, the command subscription reconnects automatically
|
|
||||||
(`BreadClient::subscribe`'s background thread has its own backoff
|
|
||||||
loop); no restart of `breadhelp listen` is needed.
|
|
||||||
- If `breadhelp listen` is not running, commands are a graceful no-op at
|
|
||||||
the bus (no subscriber). The CLI still works.
|
|
||||||
11
README.md
11
README.md
|
|
@ -1,8 +1,8 @@
|
||||||
# breadhelp
|
# breadhelp
|
||||||
|
|
||||||
Onboarding and help center for [BOS (Bread Operating System)](https://git.breadway.dev/Breadway/bos) — GTK4, a searchable guide library, an interactive keybind viewer, a troubleshooting wizard with one-click fixes, a short first-boot day-zero wizard (timezone, network hint, updates, snapper), and a live guided tour overlay that spotlights real on-screen bread\* apps (breadbar, breadbox, ...) instead of walking through a static wizard.
|
Onboarding and help center for [BOS (Bread Operating System)](https://git.breadway.dev/Breadway/bos) — GTK4, a searchable guide library, an interactive keybind viewer, a troubleshooting wizard with one-click fixes, and a live guided tour overlay that spotlights real on-screen bread\* apps (breadbar, breadbox, ...) instead of walking through a static wizard.
|
||||||
|
|
||||||
Split out of the `bos` repo into its own repo so a breadhelp release doesn't require a BOS ISO release, and vice versa. A [bakery](https://git.breadway.dev/Breadway/bread-ecosystem) product (`bakery.toml` + bakery CI) and baked into the BOS ISO — not pacman-packaged.
|
Split out of the `bos` repo into its own repo so a breadhelp release doesn't require a BOS ISO release, and vice versa. Pacman-packaged (not bakery-managed) — see `packaging/README.md`.
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
|
|
@ -12,9 +12,4 @@ cargo build --release
|
||||||
|
|
||||||
## Packaging / releasing
|
## Packaging / releasing
|
||||||
|
|
||||||
See `CONTRIBUTING.md` for the single-trunk + tag model. `main` publishes a bakery **dev** build; a `vX.Y.Z-rc.N` tag publishes **beta**; a `vX.Y.Z` tag publishes **stable**. The leftover `packaging/` tree is not a pacman source — bakery is canonical (see `packaging/README.md`).
|
See `packaging/README.md`. In short: bump `Cargo.toml`'s version, tag `vX.Y.Z`, push the tag to `forgejo` — `.forgejo/workflows/package.yml` builds and publishes to the `[breadway]` pacman repo automatically.
|
||||||
|
|
||||||
## bread event integration
|
|
||||||
|
|
||||||
Works the same with or without `breadd`. When the main window is shown,
|
|
||||||
breadhelp publishes `bread.help.opened`. See [EVENTS.md](EVENTS.md).
|
|
||||||
|
|
|
||||||
12
bakery.toml
12
bakery.toml
|
|
@ -1,12 +0,0 @@
|
||||||
name = "breadhelp"
|
|
||||||
description = "Onboarding and help center for Bread OS"
|
|
||||||
binaries = ["breadhelp"]
|
|
||||||
system_deps = ["gtk4", "glib2", "gtk4-layer-shell", "hicolor-icon-theme"]
|
|
||||||
optional_system_deps = ["snapper"]
|
|
||||||
bread_deps = []
|
|
||||||
license_file = "LICENSE"
|
|
||||||
desktop_file = "breadhelp.desktop"
|
|
||||||
data_archive = "content.tar.gz"
|
|
||||||
|
|
||||||
[install]
|
|
||||||
post_install = []
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
147cfbbf96ae4b171027defa1130d2caddb934b1
|
|
||||||
21
ci/build.sh
21
ci/build.sh
|
|
@ -1,21 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
# Delegates to bread-ecosystem's shared CI build image/script, pinned to
|
|
||||||
# the commit in ci/bread-ecosystem.rev — not `main`. bread-ecosystem's CI
|
|
||||||
# files now affect every product's release pipeline, so bumping the pin
|
|
||||||
# is a deliberate act instead of silent drift (see the bread-theme test
|
|
||||||
# that broke here for exactly that reason, before it was pinned by rev).
|
|
||||||
#
|
|
||||||
# Usage: ci/build.sh cargo build --release --locked
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
||||||
REV="$(cat "${ROOT}/ci/bread-ecosystem.rev")"
|
|
||||||
|
|
||||||
CACHE_DIR="/tmp/bread-ecosystem-ci-${REV}"
|
|
||||||
if [ ! -f "$CACHE_DIR/ci/build.sh" ]; then
|
|
||||||
rm -rf "$CACHE_DIR"
|
|
||||||
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" breadhelp "$ROOT" "$@"
|
|
||||||
|
|
@ -4,6 +4,6 @@ breadsearch indexes your files so you can find things instantly, without
|
||||||
digging through folders.
|
digging through folders.
|
||||||
|
|
||||||
- Open it from BOS Settings, or run it directly.
|
- Open it from BOS Settings, or run it directly.
|
||||||
- Describe what you are looking for in plain language — breadsearch ranks files by meaning, not by letters in a filename. Results update as you type.
|
- Type a few letters of a filename — results update as you type.
|
||||||
|
|
||||||
- [Run]breadsearch|Open File Search[/Run]
|
- [Run]breadsearch|Open File Search[/Run]
|
||||||
|
|
|
||||||
|
|
@ -13,17 +13,9 @@ nothing here is mandatory either, click around if you'd rather.
|
||||||
## Apps
|
## Apps
|
||||||
|
|
||||||
- [Show Keybind]super + space[/Show Keybind] opens the launcher (breadbox)
|
- [Show Keybind]super + space[/Show Keybind] opens the launcher (breadbox)
|
||||||
- [Show Keybind]super + e[/Show Keybind] opens files (nautilus)
|
- [Show Keybind]super + e[/Show Keybind] opens files
|
||||||
- [Show Keybind]super + b[/Show Keybind] opens the browser
|
- [Show Keybind]super + b[/Show Keybind] opens the browser
|
||||||
- [Show Keybind]super + u[/Show Keybind] opens notes (breadpad)
|
- [Show Keybind]super + u[/Show Keybind] opens notes
|
||||||
- [Show Keybind]super + ,[/Show Keybind] opens BOS Settings
|
|
||||||
- [Show Keybind]super + l[/Show Keybind] locks the screen
|
|
||||||
|
|
||||||
## Screenshots
|
|
||||||
|
|
||||||
- [Show Keybind]super + shift + s[/Show Keybind] captures a region to a file (breadshot)
|
|
||||||
- [Show Keybind]super + shift + c[/Show Keybind] captures a region to the clipboard (breadshot)
|
|
||||||
- [Show Keybind]super + shift + p[/Show Keybind] captures the active screen to a file (breadshot)
|
|
||||||
|
|
||||||
## The full list
|
## The full list
|
||||||
|
|
||||||
|
|
|
||||||
40
packaging/PKGBUILD
Normal file
40
packaging/PKGBUILD
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
# Maintainer: Breadway <plasticbread849@gmail.com>
|
||||||
|
|
||||||
|
pkgname=breadhelp
|
||||||
|
pkgver=0.1.0
|
||||||
|
pkgrel=1
|
||||||
|
pkgdesc="Onboarding and help center for Bread OS"
|
||||||
|
arch=('x86_64')
|
||||||
|
url="https://git.breadway.dev/Breadway/breadhelp"
|
||||||
|
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' 'glib2' 'gtk4-layer-shell' 'hicolor-icon-theme')
|
||||||
|
optdepends=(
|
||||||
|
'snapper: create-backup one-click fix'
|
||||||
|
)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
package() {
|
||||||
|
cd "${srcdir}/${pkgname}-${pkgver}"
|
||||||
|
install -Dm755 target/release/breadhelp "${pkgdir}/usr/bin/breadhelp"
|
||||||
|
install -Dm644 packaging/breadhelp.desktop \
|
||||||
|
"${pkgdir}/usr/share/applications/breadhelp.desktop"
|
||||||
|
install -d "${pkgdir}/usr/share/breadhelp"
|
||||||
|
cp -r content "${pkgdir}/usr/share/breadhelp/content"
|
||||||
|
install -Dm644 LICENSE "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE"
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,26 @@
|
||||||
# packaging/
|
Arch packaging
|
||||||
|
==============
|
||||||
|
|
||||||
Leftover from the pacman era. breadhelp is bakery-managed now
|
`PKGBUILD` builds and installs `breadhelp` from source.
|
||||||
(`bakery.toml` plus `.forgejo/workflows/{dev-release,rc-release,release}.yml`)
|
|
||||||
and is baked into the BOS ISO.
|
|
||||||
|
|
||||||
`breadhelp.desktop` is still live: bakery CI copies it into the published
|
## Local build
|
||||||
artifact. There is no PKGBUILD and no `package.yml` — do not treat this
|
|
||||||
directory as a pacman package source.
|
```bash
|
||||||
|
makepkg -si
|
||||||
|
```
|
||||||
|
|
||||||
|
## Before publishing to [breadway] repo
|
||||||
|
|
||||||
|
1. Tag a release on GitHub.
|
||||||
|
2. Update `pkgver` to match the tag.
|
||||||
|
3. Update `source` to the release tarball URL.
|
||||||
|
4. Run `updpkgsums` (or manually set `sha256sums`).
|
||||||
|
|
||||||
|
## Runtime dependencies
|
||||||
|
|
||||||
|
| Package | Required | Notes |
|
||||||
|
|---------|----------|-------|
|
||||||
|
| `gtk4` | yes | UI toolkit |
|
||||||
|
| `glib2` | yes | always |
|
||||||
|
| `gtk4-layer-shell` | yes | tour overlay (dim mask + spotlight callout) |
|
||||||
|
| `snapper` | optional | create-backup one-click fix |
|
||||||
|
|
|
||||||
66
src/cli.rs
66
src/cli.rs
|
|
@ -4,10 +4,7 @@
|
||||||
//! `connect_command_line`, so `--onboard` on a second launch re-triggers the
|
//! `connect_command_line`, so `--onboard` on a second launch re-triggers the
|
||||||
//! tour in the existing window instead of spawning a duplicate.
|
//! tour in the existing window instead of spawning a duplicate.
|
||||||
|
|
||||||
use bread_utils::screenshot_cli::{validate_pair, DEFAULT_HEIGHT, DEFAULT_WIDTH};
|
#[derive(Clone, Default)]
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Action {
|
pub struct Action {
|
||||||
/// Force-restart the onboarding tour from step 0, regardless of whether
|
/// Force-restart the onboarding tour from step 0, regardless of whether
|
||||||
/// it was already completed or in progress.
|
/// it was already completed or in progress.
|
||||||
|
|
@ -17,8 +14,7 @@ pub struct Action {
|
||||||
/// `present()` build the window (so the app is ready to respond the
|
/// `present()` build the window (so the app is ready to respond the
|
||||||
/// instant it's needed) without popping it open when onboarding is
|
/// instant it's needed) without popping it open when onboarding is
|
||||||
/// already done — autostart should only be visible on a genuine first
|
/// already done — autostart should only be visible on a genuine first
|
||||||
/// run (day-zero wizard, then the desktop tour), never on every
|
/// run, never on every subsequent login.
|
||||||
/// subsequent login.
|
|
||||||
pub autostart: bool,
|
pub autostart: bool,
|
||||||
/// From a breadd Lua module (e.g. `breadhelp-suggest.lua`) reacting to a
|
/// From a breadd Lua module (e.g. `breadhelp-suggest.lua`) reacting to a
|
||||||
/// system event. Resolved to banner text by `services::breadd::resolve`.
|
/// system event. Resolved to banner text by `services::breadd::resolve`.
|
||||||
|
|
@ -29,52 +25,6 @@ pub struct Action {
|
||||||
/// Only acted on if a tour is currently waiting for this exact id —
|
/// Only acted on if a tour is currently waiting for this exact id —
|
||||||
/// see the crash-safety note on `ui::tour`.
|
/// see the crash-safety note on `ui::tour`.
|
||||||
pub tour_event: Option<String>,
|
pub tour_event: Option<String>,
|
||||||
/// Render the named tab, capture it, then exit instead of running
|
|
||||||
/// normally. Known views: "home", "learn", "ask". See `crate::screenshot`.
|
|
||||||
pub screenshot: Option<String>,
|
|
||||||
/// PNG path to write the capture to. Required together with `screenshot`.
|
|
||||||
pub output: Option<String>,
|
|
||||||
/// Capture canvas width — matches the isolated compositor's output
|
|
||||||
/// width (`bread-capture --isolate-width`).
|
|
||||||
pub width: u32,
|
|
||||||
/// Capture canvas height — see `width`.
|
|
||||||
pub height: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Action {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
force_onboard: false,
|
|
||||||
autostart: false,
|
|
||||||
suggest: None,
|
|
||||||
tour_event: None,
|
|
||||||
screenshot: None,
|
|
||||||
output: None,
|
|
||||||
width: DEFAULT_WIDTH,
|
|
||||||
height: DEFAULT_HEIGHT,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Action {
|
|
||||||
/// `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<crate::screenshot::ScreenshotRequest> {
|
|
||||||
if let Err(e) = validate_pair(
|
|
||||||
self.screenshot.as_deref(),
|
|
||||||
self.output.as_deref().map(Path::new),
|
|
||||||
) {
|
|
||||||
eprintln!("breadhelp: {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: &[std::ffi::OsString]) -> Action {
|
pub fn parse(args: &[std::ffi::OsString]) -> Action {
|
||||||
|
|
@ -89,18 +39,6 @@ pub fn parse(args: &[std::ffi::OsString]) -> Action {
|
||||||
action.suggest = it.next().and_then(|s| s.to_str()).map(str::to_string);
|
action.suggest = it.next().and_then(|s| s.to_str()).map(str::to_string);
|
||||||
} else if arg == "--tour-event" {
|
} else if arg == "--tour-event" {
|
||||||
action.tour_event = it.next().and_then(|s| s.to_str()).map(str::to_string);
|
action.tour_event = it.next().and_then(|s| s.to_str()).map(str::to_string);
|
||||||
} else if arg == "--screenshot" {
|
|
||||||
action.screenshot = it.next().and_then(|s| s.to_str()).map(str::to_string);
|
|
||||||
} else if arg == "--output" {
|
|
||||||
action.output = it.next().and_then(|s| s.to_str()).map(str::to_string);
|
|
||||||
} else if arg == "--width" {
|
|
||||||
if let Some(v) = it.next().and_then(|s| s.to_str()).and_then(|s| s.parse().ok()) {
|
|
||||||
action.width = v;
|
|
||||||
}
|
|
||||||
} else if arg == "--height" {
|
|
||||||
if let Some(v) = it.next().and_then(|s| s.to_str()).and_then(|s| s.parse().ok()) {
|
|
||||||
action.height = v;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
action
|
action
|
||||||
|
|
|
||||||
221
src/config.rs
221
src/config.rs
|
|
@ -1,16 +1,20 @@
|
||||||
//! `~/.config/breadhelp/state.toml` — non-destructive TOML editing via
|
//! `~/.config/breadhelp/state.toml` — non-destructive TOML editing, same
|
||||||
//! `bread_utils::tomlcfg`, the same `load_doc`/`save_doc` discipline
|
//! `load_doc`/`save_doc` discipline as `bos-settings/src/config/mod.rs`: a
|
||||||
//! `bos-settings/src/config/mod.rs` uses: a missing file yields defaults, a
|
//! missing file yields defaults, a file that exists but fails to parse is
|
||||||
//! file that exists but fails to parse is backed up once before falling
|
//! backed up once before falling back, so a bad edit is always recoverable.
|
||||||
//! back, so a bad edit is always recoverable. This module used to carry its
|
|
||||||
//! own byte-for-byte copy of that logic (introduced in the same fix pass
|
|
||||||
//! that added it to bos-settings) — now both share one implementation.
|
|
||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use toml_edit::{value, DocumentMut};
|
use toml_edit::{value, DocumentMut};
|
||||||
|
|
||||||
pub fn config_dir() -> PathBuf {
|
pub fn config_dir() -> PathBuf {
|
||||||
bread_utils::xdg::config_home()
|
if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME") {
|
||||||
|
let p = PathBuf::from(xdg);
|
||||||
|
if p.is_absolute() {
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let home = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string());
|
||||||
|
PathBuf::from(home).join(".config")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn state_path() -> PathBuf {
|
fn state_path() -> PathBuf {
|
||||||
|
|
@ -18,11 +22,32 @@ fn state_path() -> PathBuf {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_doc(path: &Path) -> DocumentMut {
|
fn load_doc(path: &Path) -> DocumentMut {
|
||||||
bread_utils::tomlcfg::load_doc("breadhelp", path)
|
let Ok(text) = std::fs::read_to_string(path) else {
|
||||||
|
return DocumentMut::default();
|
||||||
|
};
|
||||||
|
match text.parse::<DocumentMut>() {
|
||||||
|
Ok(doc) => doc,
|
||||||
|
Err(e) => {
|
||||||
|
let backup = PathBuf::from(format!("{}.bak", path.display()));
|
||||||
|
eprintln!(
|
||||||
|
"breadhelp: {} failed to parse ({e}); backed up to {} before falling back to defaults",
|
||||||
|
path.display(),
|
||||||
|
backup.display()
|
||||||
|
);
|
||||||
|
let _ = std::fs::write(&backup, &text);
|
||||||
|
DocumentMut::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn save_doc(path: &Path, doc: &DocumentMut) {
|
fn save_doc(path: &Path, doc: &DocumentMut) {
|
||||||
if let Err(e) = bread_utils::tomlcfg::save_doc(path, doc) {
|
if let Some(parent) = path.parent() {
|
||||||
|
if let Err(e) = std::fs::create_dir_all(parent) {
|
||||||
|
eprintln!("breadhelp: couldn't create {}: {e}", parent.display());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Err(e) = std::fs::write(path, doc.to_string()) {
|
||||||
eprintln!("breadhelp: couldn't write {}: {e}", path.display());
|
eprintln!("breadhelp: couldn't write {}: {e}", path.display());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -86,11 +111,7 @@ impl State {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn onboarding_step(&self) -> i64 {
|
pub fn onboarding_step(&self) -> i64 {
|
||||||
self.doc
|
self.doc.get("onboarding").and_then(|t| t.get("step")).and_then(|v| v.as_integer()).unwrap_or(0)
|
||||||
.get("onboarding")
|
|
||||||
.and_then(|t| t.get("step"))
|
|
||||||
.and_then(|v| v.as_integer())
|
|
||||||
.unwrap_or(0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_onboarding_step(&mut self, step: i64) {
|
pub fn set_onboarding_step(&mut self, step: i64) {
|
||||||
|
|
@ -98,48 +119,6 @@ impl State {
|
||||||
self.save();
|
self.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// First-boot day-zero wizard (timezone / network hint / updates /
|
|
||||||
/// snapshot). Independent of the live desktop tour so an existing user
|
|
||||||
/// who already finished onboarding is never pulled back into setup.
|
|
||||||
pub fn day_zero_completed(&self) -> bool {
|
|
||||||
self.doc
|
|
||||||
.get("day_zero")
|
|
||||||
.and_then(|t| t.get("completed"))
|
|
||||||
.and_then(|v| v.as_bool())
|
|
||||||
.unwrap_or(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_day_zero_completed(&mut self, completed: bool) {
|
|
||||||
self.doc["day_zero"]["completed"] = value(completed);
|
|
||||||
self.save();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn day_zero_step(&self) -> i64 {
|
|
||||||
self.doc
|
|
||||||
.get("day_zero")
|
|
||||||
.and_then(|t| t.get("step"))
|
|
||||||
.and_then(|v| v.as_integer())
|
|
||||||
.unwrap_or(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_day_zero_step(&mut self, step: i64) {
|
|
||||||
self.doc["day_zero"]["step"] = value(step);
|
|
||||||
self.save();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn day_zero_snapshot_attempted(&self) -> bool {
|
|
||||||
self.doc
|
|
||||||
.get("day_zero")
|
|
||||||
.and_then(|t| t.get("snapshot_attempted"))
|
|
||||||
.and_then(|v| v.as_bool())
|
|
||||||
.unwrap_or(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_day_zero_snapshot_attempted(&mut self, attempted: bool) {
|
|
||||||
self.doc["day_zero"]["snapshot_attempted"] = value(attempted);
|
|
||||||
self.save();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the instant before `services::hyprland::rebind_temp` is called for
|
/// Set the instant before `services::hyprland::rebind_temp` is called for
|
||||||
/// a tour step with no compositor-observable signal (e.g. the screenshot
|
/// a tour step with no compositor-observable signal (e.g. the screenshot
|
||||||
/// step), cleared the instant after reverting. If breadhelp is killed
|
/// step), cleared the instant after reverting. If breadhelp is killed
|
||||||
|
|
@ -147,18 +126,8 @@ impl State {
|
||||||
/// pointing at a chained `--tour-event` ping and self-heals it before
|
/// pointing at a chained `--tour-event` ping and self-heals it before
|
||||||
/// the user can be surprised by a stray tour popup — see `ui::tour`.
|
/// the user can be surprised by a stray tour popup — see `ui::tour`.
|
||||||
pub fn pending_rebind(&self) -> Option<(String, String)> {
|
pub fn pending_rebind(&self) -> Option<(String, String)> {
|
||||||
let key = self
|
let key = self.doc.get("tour")?.get("pending_rebind_key")?.as_str()?.to_string();
|
||||||
.doc
|
let original = self.doc.get("tour")?.get("pending_rebind_original")?.as_str()?.to_string();
|
||||||
.get("tour")?
|
|
||||||
.get("pending_rebind_key")?
|
|
||||||
.as_str()?
|
|
||||||
.to_string();
|
|
||||||
let original = self
|
|
||||||
.doc
|
|
||||||
.get("tour")?
|
|
||||||
.get("pending_rebind_original")?
|
|
||||||
.as_str()?
|
|
||||||
.to_string();
|
|
||||||
Some((key, original))
|
Some((key, original))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -177,12 +146,7 @@ impl State {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn mode(&self) -> Mode {
|
pub fn mode(&self) -> Mode {
|
||||||
let s = self
|
let s = self.doc.get("general").and_then(|t| t.get("mode")).and_then(|v| v.as_str()).unwrap_or("normal");
|
||||||
.doc
|
|
||||||
.get("general")
|
|
||||||
.and_then(|t| t.get("mode"))
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.unwrap_or("normal");
|
|
||||||
Mode::from_str(s)
|
Mode::from_str(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -195,110 +159,3 @@ impl State {
|
||||||
save_doc(&self.path, &self.doc);
|
save_doc(&self.path, &self.doc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn state_toml_write_backs_up_previous_contents_and_no_tmp_file_left_behind() {
|
|
||||||
// Exercises the same bread_utils::atomic::write_atomic_backed_up
|
|
||||||
// path save_doc uses (via tomlcfg::save_doc), at the file-layout
|
|
||||||
// breadhelp actually writes to — the deeper backup/no-leftover-tmp
|
|
||||||
// behavior itself is covered by bread-utils' own test suite.
|
|
||||||
let dir = std::env::temp_dir().join(format!(
|
|
||||||
"breadhelp-atomic-write-test-{}",
|
|
||||||
std::process::id()
|
|
||||||
));
|
|
||||||
std::fs::create_dir_all(&dir).unwrap();
|
|
||||||
let path = dir.join("state.toml");
|
|
||||||
let backup = dir.join("state.toml.bak");
|
|
||||||
|
|
||||||
bread_utils::atomic::write_atomic_backed_up(&path, "first").unwrap();
|
|
||||||
assert_eq!(std::fs::read_to_string(&path).unwrap(), "first");
|
|
||||||
assert!(
|
|
||||||
!backup.exists(),
|
|
||||||
"no backup should be made when there's nothing to back up yet"
|
|
||||||
);
|
|
||||||
|
|
||||||
bread_utils::atomic::write_atomic_backed_up(&path, "second").unwrap();
|
|
||||||
assert_eq!(std::fs::read_to_string(&path).unwrap(), "second");
|
|
||||||
assert_eq!(std::fs::read_to_string(&backup).unwrap(), "first");
|
|
||||||
|
|
||||||
let leftover_tmp: Vec<_> = std::fs::read_dir(&dir)
|
|
||||||
.unwrap()
|
|
||||||
.filter_map(|e| e.ok())
|
|
||||||
.map(|e| e.file_name().to_string_lossy().into_owned())
|
|
||||||
.filter(|n| n.contains(".tmp."))
|
|
||||||
.collect();
|
|
||||||
assert!(
|
|
||||||
leftover_tmp.is_empty(),
|
|
||||||
"temp file should be renamed away, not left behind: {leftover_tmp:?}"
|
|
||||||
);
|
|
||||||
|
|
||||||
let _ = std::fs::remove_dir_all(&dir);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn save_doc_then_load_doc_round_trips_state() {
|
|
||||||
let dir = std::env::temp_dir().join(format!(
|
|
||||||
"breadhelp-state-roundtrip-test-{}",
|
|
||||||
std::process::id()
|
|
||||||
));
|
|
||||||
std::fs::create_dir_all(&dir).unwrap();
|
|
||||||
let path = dir.join("state.toml");
|
|
||||||
|
|
||||||
let mut doc = DocumentMut::default();
|
|
||||||
doc["onboarding"]["completed"] = value(true);
|
|
||||||
doc["general"]["mode"] = value("dad");
|
|
||||||
save_doc(&path, &doc);
|
|
||||||
|
|
||||||
let loaded = load_doc(&path);
|
|
||||||
assert_eq!(
|
|
||||||
loaded
|
|
||||||
.get("onboarding")
|
|
||||||
.and_then(|t| t.get("completed"))
|
|
||||||
.and_then(|v| v.as_bool()),
|
|
||||||
Some(true)
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
loaded
|
|
||||||
.get("general")
|
|
||||||
.and_then(|t| t.get("mode"))
|
|
||||||
.and_then(|v| v.as_str()),
|
|
||||||
Some("dad")
|
|
||||||
);
|
|
||||||
|
|
||||||
let _ = std::fs::remove_dir_all(&dir);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn day_zero_defaults_incomplete_and_round_trips() {
|
|
||||||
let dir =
|
|
||||||
std::env::temp_dir().join(format!("breadhelp-day-zero-state-{}", std::process::id()));
|
|
||||||
std::fs::create_dir_all(&dir).unwrap();
|
|
||||||
let path = dir.join("state.toml");
|
|
||||||
|
|
||||||
let mut state = State {
|
|
||||||
doc: DocumentMut::default(),
|
|
||||||
path: path.clone(),
|
|
||||||
};
|
|
||||||
assert!(!state.day_zero_completed());
|
|
||||||
assert_eq!(state.day_zero_step(), 0);
|
|
||||||
assert!(!state.day_zero_snapshot_attempted());
|
|
||||||
|
|
||||||
state.set_day_zero_step(3);
|
|
||||||
state.set_day_zero_snapshot_attempted(true);
|
|
||||||
state.set_day_zero_completed(true);
|
|
||||||
|
|
||||||
let loaded = State {
|
|
||||||
doc: load_doc(&path),
|
|
||||||
path,
|
|
||||||
};
|
|
||||||
assert!(loaded.day_zero_completed());
|
|
||||||
assert_eq!(loaded.day_zero_step(), 3);
|
|
||||||
assert!(loaded.day_zero_snapshot_attempted());
|
|
||||||
|
|
||||||
let _ = std::fs::remove_dir_all(&dir);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -12,23 +12,14 @@ pub enum Difficulty {
|
||||||
#[derive(serde::Deserialize, Clone, Default)]
|
#[derive(serde::Deserialize, Clone, Default)]
|
||||||
pub struct GuideMeta {
|
pub struct GuideMeta {
|
||||||
pub title: String,
|
pub title: String,
|
||||||
/// Not read by any UI yet — reserved for a future difficulty badge.
|
|
||||||
/// Real content already sets it, so kept rather than dropped.
|
|
||||||
#[allow(dead_code)]
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub difficulty: Difficulty,
|
pub difficulty: Difficulty,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub tags: Vec<String>,
|
pub tags: Vec<String>,
|
||||||
/// Guide dir-names in the same or another category. Unresolvable entries
|
/// Guide dir-names in the same or another category. Unresolvable entries
|
||||||
/// are silently dropped when rendering "related guides" — never a crash.
|
/// are silently dropped when rendering "related guides" — never a crash.
|
||||||
/// Not read by any UI yet — the "related guides" render path doesn't
|
|
||||||
/// exist. Real content already sets it, so kept rather than dropped.
|
|
||||||
#[allow(dead_code)]
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub related: Vec<String>,
|
pub related: Vec<String>,
|
||||||
/// Not read by any UI yet — reserved for a future "N min read" hint.
|
|
||||||
/// Real content already sets it, so kept rather than dropped.
|
|
||||||
#[allow(dead_code)]
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub estimated_minutes: u32,
|
pub estimated_minutes: u32,
|
||||||
/// Overrides the parent directory name as the guide's category, if set.
|
/// Overrides the parent directory name as the guide's category, if set.
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ pub struct ContentStore {
|
||||||
guides: Vec<Guide>,
|
guides: Vec<Guide>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn user_content_root() -> PathBuf {
|
fn user_content_root() -> PathBuf {
|
||||||
if let Ok(xdg) = std::env::var("XDG_DATA_HOME") {
|
if let Ok(xdg) = std::env::var("XDG_DATA_HOME") {
|
||||||
let p = PathBuf::from(xdg);
|
let p = PathBuf::from(xdg);
|
||||||
if p.is_absolute() {
|
if p.is_absolute() {
|
||||||
|
|
|
||||||
|
|
@ -29,10 +29,6 @@ pub enum Success {
|
||||||
|
|
||||||
#[derive(serde::Deserialize, Clone)]
|
#[derive(serde::Deserialize, Clone)]
|
||||||
pub struct Step {
|
pub struct Step {
|
||||||
/// Not read by any Rust code yet — steps are matched by index, not id.
|
|
||||||
/// Real tour content already sets it (content authors use it to keep
|
|
||||||
/// track of steps), so kept rather than dropped.
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub body: String,
|
pub body: String,
|
||||||
|
|
@ -90,26 +86,15 @@ struct TourFile {
|
||||||
}
|
}
|
||||||
|
|
||||||
const SYSTEM_TOUR_PATH: &str = "/usr/share/breadhelp/content/tours/onboarding.toml";
|
const SYSTEM_TOUR_PATH: &str = "/usr/share/breadhelp/content/tours/onboarding.toml";
|
||||||
const TOUR_SUBPATH: &str = "tours/onboarding.toml";
|
|
||||||
|
|
||||||
/// Checks the user content root first (where bakery installs content —
|
|
||||||
/// see `content::user_content_root`), falling back to the system path a
|
|
||||||
/// pacman package would have used. Same "user copy wins" precedent as
|
|
||||||
/// `ContentStore::load`.
|
|
||||||
pub fn load() -> Vec<Step> {
|
pub fn load() -> Vec<Step> {
|
||||||
let user_path = super::user_content_root().join(TOUR_SUBPATH);
|
let Ok(text) = std::fs::read_to_string(Path::new(SYSTEM_TOUR_PATH)) else {
|
||||||
let path = if user_path.exists() {
|
|
||||||
user_path
|
|
||||||
} else {
|
|
||||||
Path::new(SYSTEM_TOUR_PATH).to_path_buf()
|
|
||||||
};
|
|
||||||
let Ok(text) = std::fs::read_to_string(&path) else {
|
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
};
|
};
|
||||||
match toml::from_str::<TourFile>(&text) {
|
match toml::from_str::<TourFile>(&text) {
|
||||||
Ok(f) => f.steps,
|
Ok(f) => f.steps,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("breadhelp: {} failed to parse: {e}", path.display());
|
eprintln!("breadhelp: {SYSTEM_TOUR_PATH} failed to parse: {e}");
|
||||||
Vec::new()
|
Vec::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,6 @@ pub struct SymptomOption {
|
||||||
pub struct Fix {
|
pub struct Fix {
|
||||||
pub description: String,
|
pub description: String,
|
||||||
pub command: String,
|
pub command: String,
|
||||||
/// Not read by `ui::troubleshoot_wizard` yet — the "Run fix" button
|
|
||||||
/// currently executes unconditionally regardless of this flag. Real
|
|
||||||
/// symptom content already sets it, so kept rather than dropped; the
|
|
||||||
/// wizard should gate on it before this is treated as safe.
|
|
||||||
#[allow(dead_code)]
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub requires_confirm: bool,
|
pub requires_confirm: bool,
|
||||||
}
|
}
|
||||||
|
|
@ -41,11 +36,13 @@ struct SymptomFile {
|
||||||
}
|
}
|
||||||
|
|
||||||
const SYSTEM_SYMPTOMS_DIR: &str = "/usr/share/breadhelp/content/troubleshooting/_symptoms";
|
const SYSTEM_SYMPTOMS_DIR: &str = "/usr/share/breadhelp/content/troubleshooting/_symptoms";
|
||||||
const SYMPTOMS_SUBPATH: &str = "troubleshooting/_symptoms";
|
|
||||||
|
|
||||||
fn scan_symptoms_dir(dir: &Path, out: &mut std::collections::HashMap<String, Vec<SymptomNode>>) {
|
/// Loads every `*.toml` file in the symptoms directory, keyed by file stem so
|
||||||
let Ok(entries) = std::fs::read_dir(dir) else {
|
/// a `SymptomOption::goto` of `"no-sound:check-mute"` can be resolved.
|
||||||
return;
|
pub fn load_all() -> std::collections::HashMap<String, Vec<SymptomNode>> {
|
||||||
|
let mut out = std::collections::HashMap::new();
|
||||||
|
let Ok(entries) = std::fs::read_dir(Path::new(SYSTEM_SYMPTOMS_DIR)) else {
|
||||||
|
return out;
|
||||||
};
|
};
|
||||||
for entry in entries.flatten() {
|
for entry in entries.flatten() {
|
||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
|
|
@ -65,16 +62,5 @@ fn scan_symptoms_dir(dir: &Path, out: &mut std::collections::HashMap<String, Vec
|
||||||
Err(e) => eprintln!("breadhelp: {} failed to parse: {e}", path.display()),
|
Err(e) => eprintln!("breadhelp: {} failed to parse: {e}", path.display()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// Loads every `*.toml` file in the symptoms directory, keyed by file stem so
|
|
||||||
/// a `SymptomOption::goto` of `"no-sound:check-mute"` can be resolved. Scans
|
|
||||||
/// the system path (a pacman package's location) then the user content root
|
|
||||||
/// (where bakery installs content — see `content::user_content_root`) —
|
|
||||||
/// same "user copy wins on collision" precedent as `ContentStore::load`.
|
|
||||||
pub fn load_all() -> std::collections::HashMap<String, Vec<SymptomNode>> {
|
|
||||||
let mut out = std::collections::HashMap::new();
|
|
||||||
scan_symptoms_dir(Path::new(SYSTEM_SYMPTOMS_DIR), &mut out);
|
|
||||||
scan_symptoms_dir(&super::user_content_root().join(SYMPTOMS_SUBPATH), &mut out);
|
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,84 +0,0 @@
|
||||||
//! Long-running command subscription for `bread.command.help.*`.
|
|
||||||
//!
|
|
||||||
//! `breadhelp` is still a GTK help center by default. `breadhelp listen` is
|
|
||||||
//! the optional persistent process that can honor bus commands. See
|
|
||||||
//! `EVENTS.md`.
|
|
||||||
|
|
||||||
use bread_utils::bread_client::{BreadClient, BreadEvent};
|
|
||||||
|
|
||||||
use crate::services::breadd::APP_ID;
|
|
||||||
|
|
||||||
/// Subscribe to `bread.command.help.**` and block until the process is killed.
|
|
||||||
///
|
|
||||||
/// breadd being absent is not an error: [`BreadClient::subscribe`] reconnects
|
|
||||||
/// with backoff, and `on_event` simply isn't called until the daemon is up.
|
|
||||||
pub fn run() {
|
|
||||||
let client = BreadClient::connect(APP_ID);
|
|
||||||
if client.health().is_none() {
|
|
||||||
eprintln!(
|
|
||||||
"breadhelp: breadd unreachable; command subscription will connect when it comes back"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let _commands = client.subscribe("bread.command.help.**", |event| {
|
|
||||||
handle_command(&event);
|
|
||||||
});
|
|
||||||
|
|
||||||
eprintln!("breadhelp: listening for bread.command.help.**");
|
|
||||||
loop {
|
|
||||||
std::thread::park();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reacts to `bread.command.help.*` verbs. Only `open` is honored today —
|
|
||||||
/// other verbs are ignored, not stubbed as no-ops that pretend to succeed.
|
|
||||||
fn handle_command(event: &BreadEvent) {
|
|
||||||
let Some(verb) = command_verb(&event.event) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
match verb {
|
|
||||||
"open" => handle_open(),
|
|
||||||
other => {
|
|
||||||
eprintln!("breadhelp: ignoring unrecognized bread.command.help.{other}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_open() {
|
|
||||||
// Same as running `breadhelp` with no flags: GApplication presents the
|
|
||||||
// main window (or forwards to the already-running primary instance).
|
|
||||||
let result = spawn_self();
|
|
||||||
let client = BreadClient::connect(APP_ID);
|
|
||||||
match result {
|
|
||||||
Ok(_) => client.emit("bread.help.open.done", serde_json::json!({})),
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("breadhelp: bread.command.help.open failed: {e}");
|
|
||||||
client.emit(
|
|
||||||
"bread.help.open.failed",
|
|
||||||
serde_json::json!({ "error": e.to_string() }),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn spawn_self() -> std::io::Result<std::process::Child> {
|
|
||||||
let exe = std::env::current_exe().unwrap_or_else(|_| std::path::PathBuf::from("breadhelp"));
|
|
||||||
std::process::Command::new(exe).spawn()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn command_verb(event_name: &str) -> Option<&str> {
|
|
||||||
event_name.strip_prefix("bread.command.help.")
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn command_verb_strips_help_prefix() {
|
|
||||||
assert_eq!(command_verb("bread.command.help.open"), Some("open"));
|
|
||||||
assert_eq!(command_verb("bread.command.help.onboard"), Some("onboard"));
|
|
||||||
assert_eq!(command_verb("bread.command.box.open"), None);
|
|
||||||
assert_eq!(command_verb("bread.help.opened"), None);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
17
src/main.rs
17
src/main.rs
|
|
@ -1,8 +1,6 @@
|
||||||
mod cli;
|
mod cli;
|
||||||
mod config;
|
mod config;
|
||||||
mod content;
|
mod content;
|
||||||
mod listen;
|
|
||||||
mod screenshot;
|
|
||||||
mod services;
|
mod services;
|
||||||
mod theme;
|
mod theme;
|
||||||
mod ui;
|
mod ui;
|
||||||
|
|
@ -11,11 +9,6 @@ use gtk4::gio::ApplicationFlags;
|
||||||
use gtk4::prelude::*;
|
use gtk4::prelude::*;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
if std::env::args().nth(1).as_deref() == Some("listen") {
|
|
||||||
listen::run();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// GApplication requires a dotted id (g_application_id_is_valid rejects a
|
// GApplication requires a dotted id (g_application_id_is_valid rejects a
|
||||||
// bare "breadhelp" with a GLib-GIO-CRITICAL and silently skips setting
|
// bare "breadhelp" with a GLib-GIO-CRITICAL and silently skips setting
|
||||||
// it, which would break D-Bus single-instance activation). The Wayland
|
// it, which would break D-Bus single-instance activation). The Wayland
|
||||||
|
|
@ -26,17 +19,9 @@ fn main() {
|
||||||
// reach the running (primary) instance's argv, not just re-activate it
|
// reach the running (primary) instance's argv, not just re-activate it
|
||||||
// with no arguments — that's what lets a second launch re-trigger the
|
// with no arguments — that's what lets a second launch re-trigger the
|
||||||
// onboarding tour instead of only focusing the window.
|
// onboarding tour instead of only focusing the window.
|
||||||
let mut flags = ApplicationFlags::HANDLES_COMMAND_LINE;
|
|
||||||
if std::env::args().any(|a| a == "--screenshot") {
|
|
||||||
// Without this, a screenshot run would be forwarded over D-Bus to
|
|
||||||
// the real, already-running breadhelp instead of starting a fresh
|
|
||||||
// one — reusing (and mutating the tab of) the operator's actual
|
|
||||||
// help-center window instead of a disposable one.
|
|
||||||
flags |= ApplicationFlags::NON_UNIQUE;
|
|
||||||
}
|
|
||||||
let app = gtk4::Application::builder()
|
let app = gtk4::Application::builder()
|
||||||
.application_id("com.breadway.breadhelp")
|
.application_id("com.breadway.breadhelp")
|
||||||
.flags(flags)
|
.flags(ApplicationFlags::HANDLES_COMMAND_LINE)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
app.connect_command_line(|app, cmdline| {
|
app.connect_command_line(|app, cmdline| {
|
||||||
|
|
|
||||||
|
|
@ -1,102 +0,0 @@
|
||||||
//! `--screenshot` CLI mode: switch to the named tab, capture it via
|
|
||||||
//! `bread-screenshots`, then exit — driven by `bread-ecosystem`'s
|
|
||||||
//! `bread-capture` orchestrator, or run standalone for one-off captures.
|
|
||||||
//!
|
|
||||||
//! breadhelp has three tabs worth capturing (Home/Learn/Ask), switched via
|
|
||||||
//! the same `Stack::set_visible_child_name` the tab switcher itself uses —
|
|
||||||
//! see `ui::tabs`. The window is a plain top-level (not layer-shell), so a
|
|
||||||
//! full known-size canvas capture is enough, same reasoning as breadpad's
|
|
||||||
//! popup view.
|
|
||||||
//!
|
|
||||||
//! One more view isn't a tab: "troubleshoot-wizard" opens the modal wizard
|
|
||||||
//! window (`ui::troubleshoot_wizard`, normally reachable via Home's
|
|
||||||
//! "Something's wrong?" button) directly.
|
|
||||||
//!
|
|
||||||
//! Deliberately NOT covered here: the onboarding tour (`ui::tour`). It's a
|
|
||||||
//! live, multi-step walkthrough that overlays callouts on the *real*
|
|
||||||
//! desktop as the user does real things (opens real apps, switches real
|
|
||||||
//! workspaces) — there's no meaningful "screenshot" of a single step in an
|
|
||||||
//! isolated environment with no real desktop for it to point at.
|
|
||||||
//! `ui::guide_view` and `ui::keybind_viewer` also aren't separate views:
|
|
||||||
//! both render embedded *inside* the Learn/Home tabs respectively, so
|
|
||||||
//! they're already covered by those tabs' own captures.
|
|
||||||
|
|
||||||
use bread_utils::screenshot_cli::SETTLE_DELAY;
|
|
||||||
use gtk4::prelude::*;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
/// Delay before opening the wizard modal — 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;
|
|
||||||
|
|
||||||
const KNOWN_VIEWS: &[&str] = &["home", "learn", "ask", "troubleshoot-wizard"];
|
|
||||||
|
|
||||||
#[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
|
|
||||||
/// window and its tab `Stack`. Every path here ends by exiting the process
|
|
||||||
/// — it never returns control to the normal help-center UI.
|
|
||||||
pub fn dispatch(window: >k4::ApplicationWindow, stack: >k4::Stack, req: ScreenshotRequest) {
|
|
||||||
if !KNOWN_VIEWS.contains(&req.view.as_str()) {
|
|
||||||
eprintln!(
|
|
||||||
"breadhelp: unknown screenshot view '{}' (known: {})",
|
|
||||||
req.view,
|
|
||||||
KNOWN_VIEWS.join(", ")
|
|
||||||
);
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
let output = req.output;
|
|
||||||
let (width, height) = (req.width as i32, req.height as i32);
|
|
||||||
|
|
||||||
if req.view == "troubleshoot-wizard" {
|
|
||||||
window.connect_map(move |win| {
|
|
||||||
let output = output.clone();
|
|
||||||
let win = win.clone();
|
|
||||||
gtk4::glib::timeout_add_local_once(PRE_POPUP_DELAY, move || {
|
|
||||||
crate::ui::troubleshoot_wizard::open(&win, 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));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
// The caller (`ui::window::present`) returns immediately after this
|
|
||||||
// for the screenshot path, skipping its own normal `window.present()`
|
|
||||||
// call — trigger it here instead, so `connect_map` above actually has
|
|
||||||
// something to fire for.
|
|
||||||
window.present();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
stack.set_visible_child_name(&req.view);
|
|
||||||
|
|
||||||
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));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
window.present();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn finish(result: anyhow::Result<()>) {
|
|
||||||
match result {
|
|
||||||
Ok(()) => std::process::exit(0),
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("breadhelp: screenshot capture failed: {e}");
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,23 +1,6 @@
|
||||||
//! Bread bus integration. `--suggest` banners stay local; `bread.help.*`
|
//! Maps a `--suggest <id>` payload (sent by a breadd Lua module, e.g.
|
||||||
//! emits are optional and fail-silent. See `EVENTS.md`.
|
//! `breadhelp-suggest.lua` reacting to `bread.monitor.connected`) to Home
|
||||||
//!
|
//! tab banner text.
|
||||||
//! `BreadClient::emit` never blocks or errors this process — a missing or
|
|
||||||
//! restarting breadd must not affect the help center itself.
|
|
||||||
|
|
||||||
use bread_utils::bread_client::BreadClient;
|
|
||||||
|
|
||||||
/// Sibling-app id in `bread_shared::apps::KNOWN_APPS`. Events publish as
|
|
||||||
/// `bread.help.*`. Command verbs are handled by `breadhelp listen`.
|
|
||||||
pub const APP_ID: &str = "help";
|
|
||||||
|
|
||||||
/// Fire-and-forget `bread.help.opened` after the main window is actually
|
|
||||||
/// shown. Silent autostart (window built, not presented) does not call this.
|
|
||||||
pub fn emit_opened(autostart: bool) {
|
|
||||||
BreadClient::connect(APP_ID).emit(
|
|
||||||
"bread.help.opened",
|
|
||||||
serde_json::json!({ "autostart": autostart }),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Suggestion {
|
pub struct Suggestion {
|
||||||
pub text: String,
|
pub text: String,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
use std::path::Path;
|
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
/// Fire-and-forget: runs `command` via a shell, matching the idiom already
|
/// Fire-and-forget: runs `command` via a shell, matching the idiom already
|
||||||
|
|
@ -9,35 +8,6 @@ pub fn run(command: &str) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fire-and-forget argv form — use this when arguments come from the UI
|
|
||||||
/// (timezone names, page ids) so they never pass through a shell.
|
|
||||||
pub fn run_argv(prog: &str, args: &[&str]) {
|
|
||||||
if let Err(e) = Command::new(prog).args(args).spawn() {
|
|
||||||
eprintln!("breadhelp: failed to run `{prog}`: {e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// `true` when `name` resolves to an executable on `PATH`.
|
|
||||||
pub fn command_exists(name: &str) -> bool {
|
|
||||||
std::env::var_os("PATH")
|
|
||||||
.map(|paths| std::env::split_paths(&paths).any(|dir| is_executable(&dir.join(name))))
|
|
||||||
.unwrap_or(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_executable(path: &Path) -> bool {
|
|
||||||
#[cfg(unix)]
|
|
||||||
{
|
|
||||||
use std::os::unix::fs::PermissionsExt;
|
|
||||||
std::fs::metadata(path)
|
|
||||||
.map(|m| m.is_file() && m.permissions().mode() & 0o111 != 0)
|
|
||||||
.unwrap_or(false)
|
|
||||||
}
|
|
||||||
#[cfg(not(unix))]
|
|
||||||
{
|
|
||||||
path.is_file()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Runs `command` on a background thread and delivers success/failure back
|
/// Runs `command` on a background thread and delivers success/failure back
|
||||||
/// onto the GTK main loop via `on_done` — plain `spawn()` only confirms the
|
/// onto the GTK main loop via `on_done` — plain `spawn()` only confirms the
|
||||||
/// process *launched*, not how it exited, and one-click fixes need to report
|
/// process *launched*, not how it exited, and one-click fixes need to report
|
||||||
|
|
@ -56,19 +26,3 @@ pub fn run_reporting(command: &str, on_done: impl Fn(bool) + 'static) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Same as [`run_reporting`], but without a shell.
|
|
||||||
pub fn run_argv_reporting(prog: &str, args: &[&str], on_done: impl Fn(bool) + 'static) {
|
|
||||||
let prog = prog.to_string();
|
|
||||||
let args: Vec<String> = args.iter().map(|s| (*s).to_string()).collect();
|
|
||||||
let (tx, rx) = async_channel::bounded(1);
|
|
||||||
std::thread::spawn(move || {
|
|
||||||
let status = Command::new(&prog).args(&args).status();
|
|
||||||
let _ = tx.send_blocking(status.map(|s| s.success()).unwrap_or(false));
|
|
||||||
});
|
|
||||||
glib::MainContext::default().spawn_local(async move {
|
|
||||||
if let Ok(success) = rx.recv().await {
|
|
||||||
on_done(success);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,10 @@ pub struct Client {
|
||||||
pub struct Monitor {
|
pub struct Monitor {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
pub x: i32,
|
||||||
|
pub y: i32,
|
||||||
|
pub width: i32,
|
||||||
|
pub height: i32,
|
||||||
pub focused: bool,
|
pub focused: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -42,9 +46,8 @@ pub struct LayerClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn query_json(cmd: &str) -> Option<serde_json::Value> {
|
fn query_json(cmd: &str) -> Option<serde_json::Value> {
|
||||||
// Timeout-guarded: an unresponsive hyprctl (Hyprland wedged/reloading)
|
let output = Command::new("hyprctl").args(["-j", cmd]).output().ok()?;
|
||||||
// used to be able to block clients()/monitors()/layers() indefinitely.
|
output.status.success().then(|| serde_json::from_slice(&output.stdout).ok()).flatten()
|
||||||
bread_utils::proc::run_json("hyprctl", &["-j", cmd], std::time::Duration::from_secs(3))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clients() -> Vec<Client> {
|
pub fn clients() -> Vec<Client> {
|
||||||
|
|
|
||||||
27
src/theme.rs
27
src/theme.rs
|
|
@ -10,7 +10,7 @@ use std::cell::RefCell;
|
||||||
|
|
||||||
const APP_CSS: &str = "\
|
const APP_CSS: &str = "\
|
||||||
.view-content { padding: 24px; }\n\
|
.view-content { padding: 24px; }\n\
|
||||||
.view-content > label.page-title { margin-bottom: 16px; }\n\
|
.view-content > label.title { margin-bottom: 16px; }\n\
|
||||||
/* [Show Keybind] chip: reads as a keycap inline with guide/keybind-viewer text. */\n\
|
/* [Show Keybind] chip: reads as a keycap inline with guide/keybind-viewer text. */\n\
|
||||||
label.keybind-chip { \
|
label.keybind-chip { \
|
||||||
font-family: monospace; \
|
font-family: monospace; \
|
||||||
|
|
@ -44,7 +44,7 @@ window.tour-window { background-color: transparent; }\n\
|
||||||
border-radius: 12px; \
|
border-radius: 12px; \
|
||||||
padding: 16px 20px; \
|
padding: 16px 20px; \
|
||||||
}\n\
|
}\n\
|
||||||
.tour-callout label.page-title { font-weight: bold; font-size: 15px; }\n\
|
.tour-callout label.title { font-weight: bold; font-size: 15px; }\n\
|
||||||
/* The one payoff line in the whole tour — the user did the real thing and \
|
/* The one payoff line in the whole tour — the user did the real thing and \
|
||||||
the compositor proved it — shouldn't read as the same throwaway meta \
|
the compositor proved it — shouldn't read as the same throwaway meta \
|
||||||
text as the step counter. */\n\
|
text as the step counter. */\n\
|
||||||
|
|
@ -53,24 +53,6 @@ window.tour-window { background-color: transparent; }\n\
|
||||||
the \"Show me\" fallback as its own beat, not a continuation of the \
|
the \"Show me\" fallback as its own beat, not a continuation of the \
|
||||||
instructional paragraph above it. */\n\
|
instructional paragraph above it. */\n\
|
||||||
.tour-hint-row { margin-top: 8px; }\n\
|
.tour-hint-row { margin-top: 8px; }\n\
|
||||||
/* ui::troubleshoot_wizard — a bare gtk4::Window with no titlebar falls back \
|
|
||||||
to GTK's own minimal CSD (flat bar, plain system-font title, square \
|
|
||||||
corners), which read as a stray window from a different decade next to \
|
|
||||||
the rest of this (rounded, borderless) app. A real HeaderBar (set in \
|
|
||||||
troubleshoot_wizard::open) picks up the window's title automatically; \
|
|
||||||
this just makes it and the window match the shared dark theme instead \
|
|
||||||
of GTK's default light-grey CSD chrome. */\n\
|
|
||||||
window.wizard-dialog { background-color: @bg; color: @on-bg; border-radius: 10px; }\n\
|
|
||||||
window.wizard-dialog headerbar { \
|
|
||||||
background-color: @bg; \
|
|
||||||
color: @on-bg; \
|
|
||||||
border-top-left-radius: 10px; \
|
|
||||||
border-top-right-radius: 10px; \
|
|
||||||
border-bottom: 1px solid alpha(@on-bg, 0.08); \
|
|
||||||
box-shadow: none; \
|
|
||||||
}\n\
|
|
||||||
/* ui::day_zero — timezone list is a tall boxed list inside the Welcome tab. */\n\
|
|
||||||
.day-zero list { margin-top: 4px; }\n\
|
|
||||||
";
|
";
|
||||||
|
|
||||||
thread_local! {
|
thread_local! {
|
||||||
|
|
@ -81,8 +63,3 @@ pub fn load(_display: >k4::gdk::Display) {
|
||||||
bread_theme::gtk::apply_shared();
|
bread_theme::gtk::apply_shared();
|
||||||
APP_PROVIDER.with(|cell| bread_theme::gtk::apply_css(APP_CSS, cell));
|
APP_PROVIDER.with(|cell| bread_theme::gtk::apply_css(APP_CSS, cell));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn bind_window(window: &impl gtk4::prelude::IsA<gtk4::Native>) {
|
|
||||||
bread_theme::gtk::bind_window_auto_with_app_css(window, |_| APP_CSS.to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -148,7 +148,7 @@ pub fn build(store: Rc<ContentStore>, binds: Rc<Vec<Keybind>>, mode: Mode) -> Wi
|
||||||
}
|
}
|
||||||
Kind::Command { name, description } => {
|
Kind::Command { name, description } => {
|
||||||
let title = Label::new(Some(name));
|
let title = Label::new(Some(name));
|
||||||
title.add_css_class("page-title");
|
title.add_css_class("title");
|
||||||
title.set_xalign(0.0);
|
title.set_xalign(0.0);
|
||||||
detail.append(&title);
|
detail.append(&title);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,665 +0,0 @@
|
||||||
//! In-window day-zero wizard for first boot / `--autostart`.
|
|
||||||
//!
|
|
||||||
//! Not a second app: this is one extra tab on the existing help window so
|
|
||||||
//! Learn/Ask stay reachable when bakery `content.tar.gz` is installed.
|
|
||||||
//! Existing users who already finished the desktop tour never see it
|
|
||||||
//! (`needs_wizard` is false when `onboarding.completed` is set).
|
|
||||||
//!
|
|
||||||
//! Finish (or skip) hands off to the live tour — the previous first-run
|
|
||||||
//! destination — with the Home cheatsheet already showing underneath.
|
|
||||||
|
|
||||||
use std::cell::{Cell, RefCell};
|
|
||||||
use std::process::Command;
|
|
||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
use gtk4::prelude::*;
|
|
||||||
use gtk4::{
|
|
||||||
Align, Box as GBox, Button, Label, ListBox, ListBoxRow, Orientation, ScrolledWindow,
|
|
||||||
SearchEntry, SelectionMode, Stack,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::config::State;
|
|
||||||
use crate::content::keybinds::Keybind;
|
|
||||||
use crate::content::{markdown, ContentStore};
|
|
||||||
use crate::services::exec;
|
|
||||||
|
|
||||||
const PAGE_NAME: &str = "day-zero";
|
|
||||||
const STEP_COUNT: usize = 6;
|
|
||||||
const WELCOME_GUIDE_CATEGORY: &str = "getting-started";
|
|
||||||
const WELCOME_GUIDE_ID: &str = "01-what-is-bos";
|
|
||||||
const FALLBACK_WELCOME: &str = "\
|
|
||||||
# Welcome to BOS
|
|
||||||
|
|
||||||
BOS (the Bread Operating System) is a complete Hyprland desktop with the \
|
|
||||||
bread ecosystem preinstalled: a bar, a launcher, notes, Wi-Fi profiles, and a \
|
|
||||||
settings app that needs no config files.
|
|
||||||
|
|
||||||
This short setup gets the clock, network, and updates in place. Use the tabs \
|
|
||||||
above any time to browse the full guide.
|
|
||||||
";
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
|
||||||
enum Step {
|
|
||||||
Welcome,
|
|
||||||
Timezone,
|
|
||||||
Network,
|
|
||||||
Updates,
|
|
||||||
Snapshot,
|
|
||||||
Finish,
|
|
||||||
}
|
|
||||||
|
|
||||||
const STEPS: [Step; STEP_COUNT] = [
|
|
||||||
Step::Welcome,
|
|
||||||
Step::Timezone,
|
|
||||||
Step::Network,
|
|
||||||
Step::Updates,
|
|
||||||
Step::Snapshot,
|
|
||||||
Step::Finish,
|
|
||||||
];
|
|
||||||
|
|
||||||
struct Wizard {
|
|
||||||
content: GBox,
|
|
||||||
welcome_markdown: String,
|
|
||||||
binds: Vec<Keybind>,
|
|
||||||
step: RefCell<usize>,
|
|
||||||
finished: Cell<bool>,
|
|
||||||
on_finished: Box<dyn Fn()>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// First boot only: skip when the wizard already ran *or* the user already
|
|
||||||
/// finished the older tour-only onboarding (so upgrades don't re-prompt).
|
|
||||||
pub fn needs_wizard() -> bool {
|
|
||||||
needs_wizard_from(
|
|
||||||
State::load().day_zero_completed(),
|
|
||||||
State::load().onboarding_completed(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn needs_wizard_from(day_zero_done: bool, onboarding_done: bool) -> bool {
|
|
||||||
!day_zero_done && !onboarding_done
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add (or focus) the Welcome tab and show it. Idempotent across D-Bus
|
|
||||||
/// re-activation while the wizard is already up.
|
|
||||||
pub fn attach(
|
|
||||||
stack: &Stack,
|
|
||||||
store: &ContentStore,
|
|
||||||
binds: &[Keybind],
|
|
||||||
on_finished: impl Fn() + 'static,
|
|
||||||
) {
|
|
||||||
if stack.child_by_name(PAGE_NAME).is_some() {
|
|
||||||
stack.set_visible_child_name(PAGE_NAME);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let start = (State::load().day_zero_step() as usize).min(STEP_COUNT - 1);
|
|
||||||
let welcome_markdown = store
|
|
||||||
.guide(WELCOME_GUIDE_CATEGORY, WELCOME_GUIDE_ID)
|
|
||||||
.map(|g| store.body(g, State::load().mode().is_simplified()))
|
|
||||||
.filter(|body| !body.trim().is_empty())
|
|
||||||
.unwrap_or_else(|| FALLBACK_WELCOME.to_string());
|
|
||||||
|
|
||||||
let content = GBox::new(Orientation::Vertical, 16);
|
|
||||||
content.add_css_class("view-content");
|
|
||||||
content.add_css_class("day-zero");
|
|
||||||
|
|
||||||
let wizard = Rc::new(Wizard {
|
|
||||||
content: content.clone(),
|
|
||||||
welcome_markdown,
|
|
||||||
binds: binds.to_vec(),
|
|
||||||
step: RefCell::new(start),
|
|
||||||
finished: Cell::new(false),
|
|
||||||
on_finished: Box::new(on_finished),
|
|
||||||
});
|
|
||||||
wizard.render();
|
|
||||||
|
|
||||||
let page = ScrolledWindow::builder()
|
|
||||||
.child(&content)
|
|
||||||
.vexpand(true)
|
|
||||||
.hexpand(true)
|
|
||||||
.build();
|
|
||||||
stack.add_titled(&page, Some(PAGE_NAME), "Welcome");
|
|
||||||
stack.set_visible_child_name(PAGE_NAME);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn pick_network_tool(
|
|
||||||
has_settings: bool,
|
|
||||||
has_nm: bool,
|
|
||||||
) -> Option<(&'static str, &'static str, &'static [&'static str])> {
|
|
||||||
if has_settings {
|
|
||||||
Some((
|
|
||||||
"Open network settings",
|
|
||||||
"bos-settings",
|
|
||||||
&["--page", "network"],
|
|
||||||
))
|
|
||||||
} else if has_nm {
|
|
||||||
Some(("Open network editor", "nm-connection-editor", &[]))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn pick_updates_tool(
|
|
||||||
has_settings: bool,
|
|
||||||
) -> Option<(&'static str, &'static str, &'static [&'static str])> {
|
|
||||||
if has_settings {
|
|
||||||
Some(("Open BOS Settings", "bos-settings", &["--page", "packages"]))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn is_safe_timezone_name(tz: &str) -> bool {
|
|
||||||
!tz.is_empty()
|
|
||||||
&& tz.len() < 128
|
|
||||||
&& tz
|
|
||||||
.chars()
|
|
||||||
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '/' | '+' | '-' | '.'))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_timezone_property(stdout: &str) -> Option<String> {
|
|
||||||
stdout
|
|
||||||
.trim()
|
|
||||||
.strip_prefix("Timezone=")
|
|
||||||
.map(str::to_string)
|
|
||||||
.filter(|s| !s.is_empty())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_timezone_list(stdout: &str) -> Vec<String> {
|
|
||||||
stdout
|
|
||||||
.lines()
|
|
||||||
.map(str::trim)
|
|
||||||
.filter(|l| !l.is_empty())
|
|
||||||
.map(str::to_string)
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Wizard {
|
|
||||||
fn current_step(&self) -> Step {
|
|
||||||
STEPS[*self.step.borrow()]
|
|
||||||
}
|
|
||||||
|
|
||||||
fn go(self: &Rc<Self>, step: usize) {
|
|
||||||
let step = step.min(STEP_COUNT - 1);
|
|
||||||
*self.step.borrow_mut() = step;
|
|
||||||
let mut state = State::load();
|
|
||||||
state.set_day_zero_step(step as i64);
|
|
||||||
self.render();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn finish(self: &Rc<Self>) {
|
|
||||||
if self.finished.replace(true) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let mut state = State::load();
|
|
||||||
state.set_day_zero_completed(true);
|
|
||||||
(self.on_finished)();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn clear(&self) {
|
|
||||||
while let Some(child) = self.content.first_child() {
|
|
||||||
self.content.remove(&child);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render(self: &Rc<Self>) {
|
|
||||||
self.clear();
|
|
||||||
let index = *self.step.borrow();
|
|
||||||
let step = self.current_step();
|
|
||||||
|
|
||||||
let title = Label::new(Some(step_title(step)));
|
|
||||||
title.add_css_class("page-title");
|
|
||||||
title.set_xalign(0.0);
|
|
||||||
self.content.append(&title);
|
|
||||||
|
|
||||||
match step {
|
|
||||||
Step::Welcome => self.render_welcome(),
|
|
||||||
Step::Timezone => self.render_timezone(),
|
|
||||||
Step::Network => self.render_network(),
|
|
||||||
Step::Updates => self.render_updates(),
|
|
||||||
Step::Snapshot => self.render_snapshot(),
|
|
||||||
Step::Finish => self.render_finish(),
|
|
||||||
}
|
|
||||||
|
|
||||||
let counter = Label::new(Some(&format!("Step {} of {STEP_COUNT}", index + 1)));
|
|
||||||
counter.add_css_class("dim-label");
|
|
||||||
counter.set_xalign(0.0);
|
|
||||||
self.content.append(&counter);
|
|
||||||
|
|
||||||
let nav = GBox::new(Orientation::Horizontal, 8);
|
|
||||||
let skip = Button::with_label("Skip setup");
|
|
||||||
skip.add_css_class("flat");
|
|
||||||
let this = self.clone();
|
|
||||||
skip.connect_clicked(move |_| this.finish());
|
|
||||||
|
|
||||||
let back = Button::with_label("Back");
|
|
||||||
back.set_sensitive(index > 0);
|
|
||||||
let this = self.clone();
|
|
||||||
back.connect_clicked(move |_| {
|
|
||||||
let i = *this.step.borrow();
|
|
||||||
if i > 0 {
|
|
||||||
this.go(i - 1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let spacer = GBox::new(Orientation::Horizontal, 0);
|
|
||||||
spacer.set_hexpand(true);
|
|
||||||
|
|
||||||
let is_last = index + 1 >= STEP_COUNT;
|
|
||||||
let next = Button::with_label(if is_last { "Start tour" } else { "Next" });
|
|
||||||
next.add_css_class("suggested-action");
|
|
||||||
let this = self.clone();
|
|
||||||
next.connect_clicked(move |_| {
|
|
||||||
let i = *this.step.borrow();
|
|
||||||
if i + 1 >= STEP_COUNT {
|
|
||||||
this.finish();
|
|
||||||
} else {
|
|
||||||
this.go(i + 1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
nav.append(&skip);
|
|
||||||
nav.append(&spacer);
|
|
||||||
nav.append(&back);
|
|
||||||
nav.append(&next);
|
|
||||||
self.content.append(&nav);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_welcome(self: &Rc<Self>) {
|
|
||||||
let blurb = Label::new(Some(
|
|
||||||
"A few things before the desktop tour. The other tabs stay available if you want to look around.",
|
|
||||||
));
|
|
||||||
blurb.set_wrap(true);
|
|
||||||
blurb.set_xalign(0.0);
|
|
||||||
self.content.append(&blurb);
|
|
||||||
self.content.append(&markdown::render(
|
|
||||||
&markdown::parse(&self.welcome_markdown),
|
|
||||||
&self.binds,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_timezone(self: &Rc<Self>) {
|
|
||||||
let (current, zones) = load_timezones();
|
|
||||||
let intro = if zones.is_empty() {
|
|
||||||
"Couldn't list timezones (is timedatectl available?). You can set this later from BOS Settings."
|
|
||||||
} else {
|
|
||||||
"The clock should match where you are. Applying a change may ask for your password."
|
|
||||||
};
|
|
||||||
let blurb = Label::new(Some(intro));
|
|
||||||
blurb.set_wrap(true);
|
|
||||||
blurb.set_xalign(0.0);
|
|
||||||
self.content.append(&blurb);
|
|
||||||
|
|
||||||
let current_lbl = Label::new(Some(&if current.is_empty() {
|
|
||||||
"Current timezone: unknown".into()
|
|
||||||
} else {
|
|
||||||
format!("Current timezone: {current}")
|
|
||||||
}));
|
|
||||||
current_lbl.set_xalign(0.0);
|
|
||||||
current_lbl.add_css_class("dim-label");
|
|
||||||
self.content.append(¤t_lbl);
|
|
||||||
|
|
||||||
if zones.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let search = SearchEntry::new();
|
|
||||||
search.set_placeholder_text(Some("Search timezones"));
|
|
||||||
search.set_hexpand(true);
|
|
||||||
self.content.append(&search);
|
|
||||||
|
|
||||||
let list = ListBox::new();
|
|
||||||
list.set_selection_mode(SelectionMode::Single);
|
|
||||||
list.add_css_class("boxed-list");
|
|
||||||
for tz in &zones {
|
|
||||||
let row = ListBoxRow::new();
|
|
||||||
row.set_widget_name(tz);
|
|
||||||
let lbl = Label::new(Some(tz));
|
|
||||||
lbl.set_xalign(0.0);
|
|
||||||
lbl.set_margin_top(4);
|
|
||||||
lbl.set_margin_bottom(4);
|
|
||||||
lbl.set_margin_start(8);
|
|
||||||
row.set_child(Some(&lbl));
|
|
||||||
list.append(&row);
|
|
||||||
if tz == ¤t {
|
|
||||||
list.select_row(Some(&row));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
let list = list.clone();
|
|
||||||
search.connect_search_changed(move |entry| {
|
|
||||||
let q = entry.text().to_lowercase();
|
|
||||||
let mut i = 0;
|
|
||||||
let mut first_visible: Option<ListBoxRow> = None;
|
|
||||||
loop {
|
|
||||||
let Some(row) = list.row_at_index(i) else {
|
|
||||||
break;
|
|
||||||
};
|
|
||||||
let name = row.widget_name();
|
|
||||||
let visible = q.is_empty() || name.to_lowercase().contains(&q);
|
|
||||||
row.set_visible(visible);
|
|
||||||
if visible && first_visible.is_none() {
|
|
||||||
first_visible = Some(row);
|
|
||||||
}
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
if let Some(row) = first_visible {
|
|
||||||
list.select_row(Some(&row));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let scroller = ScrolledWindow::builder()
|
|
||||||
.child(&list)
|
|
||||||
.min_content_height(180)
|
|
||||||
.hexpand(true)
|
|
||||||
.vexpand(true)
|
|
||||||
.build();
|
|
||||||
self.content.append(&scroller);
|
|
||||||
|
|
||||||
let status = Label::new(None);
|
|
||||||
status.set_xalign(0.0);
|
|
||||||
status.add_css_class("dim-label");
|
|
||||||
|
|
||||||
let apply = Button::with_label("Apply timezone");
|
|
||||||
apply.set_halign(Align::Start);
|
|
||||||
apply.add_css_class("suggested-action");
|
|
||||||
let current_lbl = current_lbl.clone();
|
|
||||||
let status_for_click = status.clone();
|
|
||||||
apply.connect_clicked(move |_| {
|
|
||||||
let Some(row) = list.selected_row() else {
|
|
||||||
status_for_click.set_label("Pick a timezone first.");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let tz = row.widget_name().to_string();
|
|
||||||
if !is_safe_timezone_name(&tz) {
|
|
||||||
status_for_click.set_label("That timezone name doesn't look valid.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
status_for_click.set_label("Applying\u{2026}");
|
|
||||||
let status = status_for_click.clone();
|
|
||||||
let current_lbl = current_lbl.clone();
|
|
||||||
apply_timezone(tz, move |msg, ok, tz| {
|
|
||||||
status.set_label(&msg);
|
|
||||||
if ok {
|
|
||||||
current_lbl.set_label(&format!("Current timezone: {tz}"));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
self.content.append(&apply);
|
|
||||||
self.content.append(&status);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_network(self: &Rc<Self>) {
|
|
||||||
let blurb = Label::new(Some(
|
|
||||||
"Connect to Wi-Fi in BOS Settings (or the NetworkManager editor). This window does not manage networks itself.",
|
|
||||||
));
|
|
||||||
blurb.set_wrap(true);
|
|
||||||
blurb.set_xalign(0.0);
|
|
||||||
self.content.append(&blurb);
|
|
||||||
|
|
||||||
if let Some((label, prog, args)) = pick_network_tool(
|
|
||||||
exec::command_exists("bos-settings"),
|
|
||||||
exec::command_exists("nm-connection-editor"),
|
|
||||||
) {
|
|
||||||
let btn = Button::with_label(label);
|
|
||||||
btn.set_halign(Align::Start);
|
|
||||||
let prog = prog.to_string();
|
|
||||||
let args: Vec<String> = args.iter().map(|s| (*s).to_string()).collect();
|
|
||||||
btn.connect_clicked(move |_| {
|
|
||||||
let argv: Vec<&str> = args.iter().map(String::as_str).collect();
|
|
||||||
exec::run_argv(&prog, &argv);
|
|
||||||
});
|
|
||||||
self.content.append(&btn);
|
|
||||||
} else {
|
|
||||||
let hint = Label::new(Some(
|
|
||||||
"No network UI found. Run `breadcrumbs` or `nmcli` in a terminal to connect.",
|
|
||||||
));
|
|
||||||
hint.set_wrap(true);
|
|
||||||
hint.set_xalign(0.0);
|
|
||||||
hint.add_css_class("dim-label");
|
|
||||||
self.content.append(&hint);
|
|
||||||
}
|
|
||||||
|
|
||||||
if exec::command_exists("breadcrumbs") {
|
|
||||||
let crumbs = Label::new(Some(
|
|
||||||
"Location profiles (home / work / away) are handled by breadcrumbs once a network is saved.",
|
|
||||||
));
|
|
||||||
crumbs.set_wrap(true);
|
|
||||||
crumbs.set_xalign(0.0);
|
|
||||||
crumbs.add_css_class("dim-label");
|
|
||||||
self.content.append(&crumbs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_updates(self: &Rc<Self>) {
|
|
||||||
let blurb = Label::new(Some(
|
|
||||||
"When you're online, check for system and bread-ecosystem updates. Nothing is installed from this step.",
|
|
||||||
));
|
|
||||||
blurb.set_wrap(true);
|
|
||||||
blurb.set_xalign(0.0);
|
|
||||||
self.content.append(&blurb);
|
|
||||||
|
|
||||||
if let Some((label, prog, args)) = pick_updates_tool(exec::command_exists("bos-settings")) {
|
|
||||||
let btn = Button::with_label(label);
|
|
||||||
btn.set_halign(Align::Start);
|
|
||||||
let prog = prog.to_string();
|
|
||||||
let args: Vec<String> = args.iter().map(|s| (*s).to_string()).collect();
|
|
||||||
btn.connect_clicked(move |_| {
|
|
||||||
let argv: Vec<&str> = args.iter().map(String::as_str).collect();
|
|
||||||
exec::run_argv(&prog, &argv);
|
|
||||||
});
|
|
||||||
self.content.append(&btn);
|
|
||||||
}
|
|
||||||
|
|
||||||
let hint = Label::new(Some(if exec::command_exists("bos-settings") {
|
|
||||||
"Or run `bos-update` in a terminal."
|
|
||||||
} else {
|
|
||||||
"BOS Settings isn't installed. Run `bos-update` in a terminal when you're ready."
|
|
||||||
}));
|
|
||||||
hint.set_wrap(true);
|
|
||||||
hint.set_xalign(0.0);
|
|
||||||
hint.add_css_class("dim-label");
|
|
||||||
self.content.append(&hint);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_snapshot(self: &Rc<Self>) {
|
|
||||||
let available = exec::command_exists("snapper");
|
|
||||||
let blurb = Label::new(Some(if available {
|
|
||||||
"A snapshot is a restore point. We'll try to create one named day-zero — if this machine doesn't allow it, that's fine."
|
|
||||||
} else {
|
|
||||||
"snapper isn't available on this system, so no first-boot snapshot will be created. You can skip this."
|
|
||||||
}));
|
|
||||||
blurb.set_wrap(true);
|
|
||||||
blurb.set_xalign(0.0);
|
|
||||||
self.content.append(&blurb);
|
|
||||||
|
|
||||||
let status = Label::new(None);
|
|
||||||
status.set_xalign(0.0);
|
|
||||||
status.add_css_class("dim-label");
|
|
||||||
|
|
||||||
if available {
|
|
||||||
let retry = Button::with_label("Create snapshot");
|
|
||||||
retry.set_halign(Align::Start);
|
|
||||||
let status_click = status.clone();
|
|
||||||
retry.connect_clicked(move |_| {
|
|
||||||
status_click.set_label("Creating snapshot\u{2026}");
|
|
||||||
let status = status_click.clone();
|
|
||||||
create_day_zero_snapshot(move |ok| {
|
|
||||||
status.set_label(snapshot_status(ok));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
self.content.append(&retry);
|
|
||||||
|
|
||||||
if !State::load().day_zero_snapshot_attempted() {
|
|
||||||
let mut state = State::load();
|
|
||||||
state.set_day_zero_snapshot_attempted(true);
|
|
||||||
status.set_label("Creating snapshot\u{2026}");
|
|
||||||
let status = status.clone();
|
|
||||||
create_day_zero_snapshot(move |ok| {
|
|
||||||
status.set_label(snapshot_status(ok));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.content.append(&status);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_finish(self: &Rc<Self>) {
|
|
||||||
let blurb = Label::new(Some(
|
|
||||||
"You're set. Next is a short tour of the real desktop. Super+/ opens this help center any time — the Home tab is the keybind cheatsheet.",
|
|
||||||
));
|
|
||||||
blurb.set_wrap(true);
|
|
||||||
blurb.set_xalign(0.0);
|
|
||||||
self.content.append(&blurb);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn step_title(step: Step) -> &'static str {
|
|
||||||
match step {
|
|
||||||
Step::Welcome => "Welcome to BOS",
|
|
||||||
Step::Timezone => "Timezone",
|
|
||||||
Step::Network => "Network",
|
|
||||||
Step::Updates => "Updates",
|
|
||||||
Step::Snapshot => "Snapshot",
|
|
||||||
Step::Finish => "You're ready",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn snapshot_status(ok: bool) -> &'static str {
|
|
||||||
if ok {
|
|
||||||
"Created snapshot “day-zero”."
|
|
||||||
} else {
|
|
||||||
"Couldn't create a snapshot (that's OK — you can do this later from Settings)."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load_timezones() -> (String, Vec<String>) {
|
|
||||||
let current = Command::new("timedatectl")
|
|
||||||
.args(["show", "--property=Timezone"])
|
|
||||||
.output()
|
|
||||||
.ok()
|
|
||||||
.filter(|o| o.status.success())
|
|
||||||
.and_then(|o| parse_timezone_property(&String::from_utf8_lossy(&o.stdout)))
|
|
||||||
.unwrap_or_default();
|
|
||||||
let zones = Command::new("timedatectl")
|
|
||||||
.arg("list-timezones")
|
|
||||||
.output()
|
|
||||||
.ok()
|
|
||||||
.filter(|o| o.status.success())
|
|
||||||
.map(|o| parse_timezone_list(&String::from_utf8_lossy(&o.stdout)))
|
|
||||||
.unwrap_or_default();
|
|
||||||
(current, zones)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn apply_timezone(tz: String, on_done: impl Fn(String, bool, String) + 'static) {
|
|
||||||
let (tx, rx) = async_channel::bounded(1);
|
|
||||||
std::thread::spawn(move || {
|
|
||||||
let direct = Command::new("timedatectl")
|
|
||||||
.args(["set-timezone", &tz])
|
|
||||||
.status();
|
|
||||||
if direct.map(|s| s.success()).unwrap_or(false) {
|
|
||||||
let _ = tx.send_blocking((format!("Timezone set to {tz}."), true, tz));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if !exec::command_exists("pkexec") {
|
|
||||||
let _ = tx.send_blocking((
|
|
||||||
"Need permission to change the timezone, and pkexec isn't available.".into(),
|
|
||||||
false,
|
|
||||||
tz,
|
|
||||||
));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let elevated = Command::new("pkexec")
|
|
||||||
.args(["timedatectl", "set-timezone", &tz])
|
|
||||||
.status();
|
|
||||||
if elevated.map(|s| s.success()).unwrap_or(false) {
|
|
||||||
let _ = tx.send_blocking((format!("Timezone set to {tz}."), true, tz));
|
|
||||||
} else {
|
|
||||||
let _ = tx.send_blocking((
|
|
||||||
"Couldn't change the timezone. You can try again from BOS Settings.".into(),
|
|
||||||
false,
|
|
||||||
tz,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
glib::MainContext::default().spawn_local(async move {
|
|
||||||
if let Ok((msg, ok, tz)) = rx.recv().await {
|
|
||||||
on_done(msg, ok, tz);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_day_zero_snapshot(on_done: impl Fn(bool) + 'static) {
|
|
||||||
exec::run_argv_reporting(
|
|
||||||
"snapper",
|
|
||||||
&["-c", "root", "create", "-d", "day-zero"],
|
|
||||||
on_done,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Used by `window` after the wizard finishes: drop the Welcome tab so the
|
|
||||||
/// help center is just Home/Learn/Ask again.
|
|
||||||
pub fn detach(stack: &Stack) {
|
|
||||||
if let Some(child) = stack.child_by_name(PAGE_NAME) {
|
|
||||||
stack.remove(&child);
|
|
||||||
}
|
|
||||||
if stack.child_by_name("home").is_some() {
|
|
||||||
stack.set_visible_child_name("home");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn existing_onboarding_skips_wizard() {
|
|
||||||
assert!(!needs_wizard_from(false, true));
|
|
||||||
assert!(!needs_wizard_from(true, true));
|
|
||||||
assert!(!needs_wizard_from(true, false));
|
|
||||||
assert!(needs_wizard_from(false, false));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn network_prefers_bos_settings_then_nm() {
|
|
||||||
let (label, prog, args) = pick_network_tool(true, true).unwrap();
|
|
||||||
assert_eq!(prog, "bos-settings");
|
|
||||||
assert_eq!(args, &["--page", "network"]);
|
|
||||||
assert!(label.contains("network"));
|
|
||||||
let (_, prog, _) = pick_network_tool(false, true).unwrap();
|
|
||||||
assert_eq!(prog, "nm-connection-editor");
|
|
||||||
assert!(pick_network_tool(false, false).is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn updates_tool_only_when_settings_exists() {
|
|
||||||
assert!(pick_updates_tool(true).is_some());
|
|
||||||
assert!(pick_updates_tool(false).is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn timezone_names_reject_shell_metacharacters() {
|
|
||||||
assert!(is_safe_timezone_name("Australia/Perth"));
|
|
||||||
assert!(is_safe_timezone_name("Etc/GMT+8"));
|
|
||||||
assert!(is_safe_timezone_name("America/Argentina/ComodRivadavia"));
|
|
||||||
assert!(!is_safe_timezone_name(""));
|
|
||||||
assert!(!is_safe_timezone_name("America/New York"));
|
|
||||||
assert!(!is_safe_timezone_name("x; rm -rf /"));
|
|
||||||
assert!(!is_safe_timezone_name("$(reboot)"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parses_timedatectl_show_and_list() {
|
|
||||||
assert_eq!(
|
|
||||||
parse_timezone_property("Timezone=Australia/Perth\n").as_deref(),
|
|
||||||
Some("Australia/Perth")
|
|
||||||
);
|
|
||||||
assert_eq!(parse_timezone_property("Timezone=\n"), None);
|
|
||||||
assert_eq!(
|
|
||||||
parse_timezone_list("Africa/Abidjan\nAustralia/Perth\n\nUTC\n"),
|
|
||||||
vec!["Africa/Abidjan", "Australia/Perth", "UTC"]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -8,7 +8,7 @@ pub fn build(store: &ContentStore, guide: &Guide, simplified: bool, binds: &[Key
|
||||||
vbox.add_css_class("view-content");
|
vbox.add_css_class("view-content");
|
||||||
|
|
||||||
let title = Label::new(Some(&guide.meta.title));
|
let title = Label::new(Some(&guide.meta.title));
|
||||||
title.add_css_class("page-title");
|
title.add_css_class("title");
|
||||||
title.set_xalign(0.0);
|
title.set_xalign(0.0);
|
||||||
vbox.append(&title);
|
vbox.append(&title);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ pub fn build(
|
||||||
|
|
||||||
let header = GBox::new(Orientation::Horizontal, 12);
|
let header = GBox::new(Orientation::Horizontal, 12);
|
||||||
let title = Label::new(Some("Home"));
|
let title = Label::new(Some("Home"));
|
||||||
title.add_css_class("page-title");
|
title.add_css_class("title");
|
||||||
title.set_xalign(0.0);
|
title.set_xalign(0.0);
|
||||||
title.set_hexpand(true);
|
title.set_hexpand(true);
|
||||||
header.append(&title);
|
header.append(&title);
|
||||||
|
|
@ -83,7 +83,7 @@ pub fn build(
|
||||||
troubleshoot_btn.set_halign(Align::Start);
|
troubleshoot_btn.set_halign(Align::Start);
|
||||||
{
|
{
|
||||||
let parent = parent_window.clone();
|
let parent = parent_window.clone();
|
||||||
troubleshoot_btn.connect_clicked(move |_| troubleshoot_wizard::open(&parent, |_| {}));
|
troubleshoot_btn.connect_clicked(move |_| troubleshoot_wizard::open(&parent));
|
||||||
}
|
}
|
||||||
vbox.append(&troubleshoot_btn);
|
vbox.append(&troubleshoot_btn);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ pub fn build(binds: &[Keybind]) -> gtk4::Widget {
|
||||||
let vbox = GBox::new(Orientation::Vertical, 6);
|
let vbox = GBox::new(Orientation::Vertical, 6);
|
||||||
|
|
||||||
let header = Label::new(Some("Keybinds"));
|
let header = Label::new(Some("Keybinds"));
|
||||||
header.add_css_class("page-title");
|
header.add_css_class("title");
|
||||||
header.set_xalign(0.0);
|
header.set_xalign(0.0);
|
||||||
vbox.append(&header);
|
vbox.append(&header);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
pub mod ask;
|
pub mod ask;
|
||||||
pub mod day_zero;
|
|
||||||
pub mod guide_view;
|
pub mod guide_view;
|
||||||
pub mod home;
|
pub mod home;
|
||||||
pub mod keybind_viewer;
|
pub mod keybind_viewer;
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,6 @@ pub fn build(
|
||||||
window.set_layer(Layer::Overlay);
|
window.set_layer(Layer::Overlay);
|
||||||
window.set_keyboard_mode(KeyboardMode::OnDemand);
|
window.set_keyboard_mode(KeyboardMode::OnDemand);
|
||||||
window.set_monitor(Some(monitor));
|
window.set_monitor(Some(monitor));
|
||||||
crate::theme::bind_window(&window);
|
|
||||||
|
|
||||||
if let Some(rect) = rect {
|
if let Some(rect) = rect {
|
||||||
let (mon_w, mon_h) = monitor_size;
|
let (mon_w, mon_h) = monitor_size;
|
||||||
|
|
@ -73,7 +72,7 @@ pub fn build(
|
||||||
root.set_size_request(CALLOUT_WIDTH, -1);
|
root.set_size_request(CALLOUT_WIDTH, -1);
|
||||||
|
|
||||||
let title = Label::new(Some(&step.title));
|
let title = Label::new(Some(&step.title));
|
||||||
title.add_css_class("page-title");
|
title.add_css_class("title");
|
||||||
title.set_xalign(0.0);
|
title.set_xalign(0.0);
|
||||||
title.set_wrap(true);
|
title.set_wrap(true);
|
||||||
root.append(&title);
|
root.append(&title);
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@ fn new_mask_window(monitor: &gdk4::Monitor) -> Window {
|
||||||
window.set_layer(Layer::Overlay);
|
window.set_layer(Layer::Overlay);
|
||||||
window.set_keyboard_mode(KeyboardMode::None);
|
window.set_keyboard_mode(KeyboardMode::None);
|
||||||
window.set_monitor(Some(monitor));
|
window.set_monitor(Some(monitor));
|
||||||
crate::theme::bind_window(&window);
|
|
||||||
let dim = GBox::new(gtk4::Orientation::Vertical, 0);
|
let dim = GBox::new(gtk4::Orientation::Vertical, 0);
|
||||||
dim.add_css_class("tour-mask");
|
dim.add_css_class("tour-mask");
|
||||||
dim.set_hexpand(true);
|
dim.set_hexpand(true);
|
||||||
|
|
|
||||||
|
|
@ -110,7 +110,7 @@ pub fn self_heal() {
|
||||||
pub fn on_tour_event(id: &str) {
|
pub fn on_tour_event(id: &str) {
|
||||||
let hit = STATE.with(|cell| {
|
let hit = STATE.with(|cell| {
|
||||||
let mut borrow = cell.borrow_mut();
|
let mut borrow = cell.borrow_mut();
|
||||||
let state = borrow.as_mut()?;
|
let Some(state) = borrow.as_mut() else { return None };
|
||||||
// Only a *visibly* confirmed step blocks a re-fire. `confirmed` gets
|
// Only a *visibly* confirmed step blocks a re-fire. `confirmed` gets
|
||||||
// set below, before `render_step` runs, so that render actually
|
// set below, before `render_step` runs, so that render actually
|
||||||
// paints the "done" state instead of the stale pre-confirmation one
|
// paints the "done" state instead of the stale pre-confirmation one
|
||||||
|
|
|
||||||
|
|
@ -25,11 +25,7 @@ struct Wizard {
|
||||||
trees: HashMap<String, Vec<SymptomNode>>,
|
trees: HashMap<String, Vec<SymptomNode>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `on_build` runs on the freshly built window *before* it's presented —
|
pub fn open(parent: &impl IsA<gtk4::Window>) {
|
||||||
/// screenshot mode's only hook point, since `connect_map` registered any
|
|
||||||
/// later would miss a map that already happened. The real call site
|
|
||||||
/// (`ui::home`) passes a no-op.
|
|
||||||
pub fn open(parent: &impl IsA<gtk4::Window>, on_build: impl FnOnce(&Window)) {
|
|
||||||
let window = Window::builder()
|
let window = Window::builder()
|
||||||
.transient_for(parent)
|
.transient_for(parent)
|
||||||
.modal(true)
|
.modal(true)
|
||||||
|
|
@ -37,11 +33,6 @@ pub fn open(parent: &impl IsA<gtk4::Window>, on_build: impl FnOnce(&Window)) {
|
||||||
.default_width(480)
|
.default_width(480)
|
||||||
.default_height(360)
|
.default_height(360)
|
||||||
.build();
|
.build();
|
||||||
window.add_css_class("wizard-dialog");
|
|
||||||
crate::theme::bind_window(&window);
|
|
||||||
let header = gtk4::HeaderBar::new();
|
|
||||||
header.set_show_title_buttons(true);
|
|
||||||
window.set_titlebar(Some(&header));
|
|
||||||
|
|
||||||
let content = GBox::new(Orientation::Vertical, 12);
|
let content = GBox::new(Orientation::Vertical, 12);
|
||||||
content.add_css_class("view-content");
|
content.add_css_class("view-content");
|
||||||
|
|
@ -50,7 +41,6 @@ pub fn open(parent: &impl IsA<gtk4::Window>, on_build: impl FnOnce(&Window)) {
|
||||||
wizard.render_picker();
|
wizard.render_picker();
|
||||||
|
|
||||||
window.set_child(Some(&content));
|
window.set_child(Some(&content));
|
||||||
on_build(&window);
|
|
||||||
window.present();
|
window.present();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -65,7 +55,7 @@ impl Wizard {
|
||||||
self.clear();
|
self.clear();
|
||||||
|
|
||||||
let title = Label::new(Some("What's wrong?"));
|
let title = Label::new(Some("What's wrong?"));
|
||||||
title.add_css_class("page-title");
|
title.add_css_class("title");
|
||||||
title.set_xalign(0.0);
|
title.set_xalign(0.0);
|
||||||
self.content.append(&title);
|
self.content.append(&title);
|
||||||
|
|
||||||
|
|
|
||||||
121
src/ui/window.rs
121
src/ui/window.rs
|
|
@ -6,20 +6,16 @@ use gtk4::{Application, ApplicationWindow, Box as GBox, Orientation, Stack, Stac
|
||||||
|
|
||||||
use crate::cli::Action;
|
use crate::cli::Action;
|
||||||
use crate::config::State;
|
use crate::config::State;
|
||||||
use crate::content::keybinds::Keybind;
|
|
||||||
use crate::content::{keybinds, ContentStore};
|
use crate::content::{keybinds, ContentStore};
|
||||||
|
|
||||||
use super::home::Home;
|
use super::home::Home;
|
||||||
use super::{ask, day_zero, learn, modes, tabs, tour};
|
use super::{ask, learn, modes, tabs, tour};
|
||||||
|
|
||||||
const DEFAULT_TAB: &str = "home";
|
const DEFAULT_TAB: &str = "home";
|
||||||
|
|
||||||
struct Handle {
|
struct Handle {
|
||||||
window: ApplicationWindow,
|
window: ApplicationWindow,
|
||||||
home: Home,
|
home: Home,
|
||||||
stack: Stack,
|
|
||||||
store: Rc<ContentStore>,
|
|
||||||
binds: Rc<Vec<Keybind>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
thread_local! {
|
thread_local! {
|
||||||
|
|
@ -30,44 +26,19 @@ thread_local! {
|
||||||
/// over D-Bus to this primary instance by a second `breadhelp` launch — so
|
/// over D-Bus to this primary instance by a second `breadhelp` launch — so
|
||||||
/// it must build the window at most once and reuse it thereafter.
|
/// it must build the window at most once and reuse it thereafter.
|
||||||
pub fn present(app: &Application, action: Action) {
|
pub fn present(app: &Application, action: Action) {
|
||||||
// Crash-recover keybinds even if this invocation never builds a window
|
|
||||||
// (silent every-login autostart).
|
|
||||||
tour::self_heal();
|
|
||||||
|
|
||||||
// Once onboarding is done, autostart only needs a warm process so
|
|
||||||
// Super+/ is instant. GTK/theme/font load is most of that cost and is
|
|
||||||
// paid by staying resident (~14 MiB PSS, no window). Building tabs
|
|
||||||
// adds ~2 MiB of dead weight for a session that may never open help.
|
|
||||||
let is_bare_silent_autostart = action.autostart
|
|
||||||
&& action.screenshot.is_none()
|
|
||||||
&& !action.force_onboard
|
|
||||||
&& action.tour_event.is_none()
|
|
||||||
&& action.suggest.is_none()
|
|
||||||
&& State::load().onboarding_completed();
|
|
||||||
if is_bare_silent_autostart {
|
|
||||||
// No window means nothing holds GApplication open. Forget the hold
|
|
||||||
// guard so the primary instance lives for the session; logout is
|
|
||||||
// what ends it. Without this, the next Super+/ would cold-start.
|
|
||||||
std::mem::forget(app.hold());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
HANDLE.with(|cell| {
|
HANDLE.with(|cell| {
|
||||||
|
let mut just_built = false;
|
||||||
{
|
{
|
||||||
let mut cell_ref = cell.borrow_mut();
|
let mut cell_ref = cell.borrow_mut();
|
||||||
if cell_ref.is_none() {
|
if cell_ref.is_none() {
|
||||||
*cell_ref = Some(build(app));
|
*cell_ref = Some(build(app));
|
||||||
|
just_built = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let cell_ref = cell.borrow();
|
let cell_ref = cell.borrow();
|
||||||
let handle = cell_ref.as_ref().unwrap();
|
let handle = cell_ref.as_ref().unwrap();
|
||||||
let display = WidgetExt::display(&handle.window);
|
let display = WidgetExt::display(&handle.window);
|
||||||
|
|
||||||
if let Some(req) = action.screenshot_request() {
|
|
||||||
crate::screenshot::dispatch(&handle.window, &handle.stack, req);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if action.force_onboard {
|
if action.force_onboard {
|
||||||
tour::restart(&display);
|
tour::restart(&display);
|
||||||
return;
|
return;
|
||||||
|
|
@ -82,55 +53,29 @@ pub fn present(app: &Application, action: Action) {
|
||||||
if let Some(s) = crate::services::breadd::resolve(id) {
|
if let Some(s) = crate::services::breadd::resolve(id) {
|
||||||
handle.home.set_suggestion(Some(&s.text));
|
handle.home.set_suggestion(Some(&s.text));
|
||||||
}
|
}
|
||||||
// Background daemon event — never steal focus. A `--suggest`
|
// Only focus the window the first time this process builds it
|
||||||
// can now be the first invocation of the session because
|
// (i.e. breadhelp wasn't already running) — a background daemon
|
||||||
// silent autostart no longer builds a window.
|
// event shouldn't steal focus from whatever the user is doing.
|
||||||
|
if just_built {
|
||||||
|
handle.window.present();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bare silent autostart (onboarding done) already returned above.
|
// Every-login autostart builds the window (so the app is ready to
|
||||||
// Autostart that reaches here is a genuine first run.
|
// respond to SUPER+/ instantly) but only starts the tour / pops the
|
||||||
let onboarded = State::load().onboarding_completed();
|
// window open on a genuine first run — never on later logins.
|
||||||
if action.autostart && onboarded {
|
if action.autostart && !State::load().onboarding_completed() {
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if day_zero::needs_wizard() {
|
|
||||||
let stack = handle.stack.clone();
|
|
||||||
let display = display.clone();
|
|
||||||
day_zero::attach(&handle.stack, &handle.store, &handle.binds, move || {
|
|
||||||
// Drop the Welcome tab after the click handler returns —
|
|
||||||
// removing it here would destroy the Skip/Finish button
|
|
||||||
// mid-signal.
|
|
||||||
let stack = stack.clone();
|
|
||||||
let display = display.clone();
|
|
||||||
glib::idle_add_local_once(move || {
|
|
||||||
day_zero::detach(&stack);
|
|
||||||
if !State::load().onboarding_completed() {
|
|
||||||
tour::start(&display);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
show_window(&handle.window, action.autostart);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if action.autostart && !onboarded {
|
|
||||||
tour::start(&display);
|
tour::start(&display);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
show_window(&handle.window, action.autostart);
|
let silent_autostart = action.autostart && State::load().onboarding_completed();
|
||||||
|
if !silent_autostart {
|
||||||
|
handle.window.present();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Present the help window and publish `bread.help.opened`. Screenshot
|
|
||||||
/// mode calls `window.present()` itself and must not go through here —
|
|
||||||
/// that is capture, not a user-visible open.
|
|
||||||
fn show_window(window: &ApplicationWindow, autostart: bool) {
|
|
||||||
window.present();
|
|
||||||
crate::services::breadd::emit_opened(autostart);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build(app: &Application) -> Handle {
|
fn build(app: &Application) -> Handle {
|
||||||
// First thing on every cold start: revert any keybind a previous,
|
// First thing on every cold start: revert any keybind a previous,
|
||||||
// crashed run may have left temporarily rebound mid tour-step (see
|
// crashed run may have left temporarily rebound mid tour-step (see
|
||||||
|
|
@ -146,7 +91,6 @@ fn build(app: &Application) -> Handle {
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
crate::theme::load(&WidgetExt::display(&window));
|
crate::theme::load(&WidgetExt::display(&window));
|
||||||
crate::theme::bind_window(&window);
|
|
||||||
|
|
||||||
let store = Rc::new(ContentStore::load());
|
let store = Rc::new(ContentStore::load());
|
||||||
let binds = Rc::new(keybinds::load());
|
let binds = Rc::new(keybinds::load());
|
||||||
|
|
@ -168,16 +112,8 @@ fn build(app: &Application) -> Handle {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
stack.add_titled(&home.root, Some("home"), "Home");
|
stack.add_titled(&home.root, Some("home"), "Home");
|
||||||
stack.add_titled(
|
stack.add_titled(&learn::build(&store, &binds, state.mode()), Some("learn"), "Learn");
|
||||||
&learn::build(&store, &binds, state.mode()),
|
stack.add_titled(&ask::build(store.clone(), binds.clone(), state.mode()), Some("ask"), "Ask");
|
||||||
Some("learn"),
|
|
||||||
"Learn",
|
|
||||||
);
|
|
||||||
stack.add_titled(
|
|
||||||
&ask::build(store.clone(), binds.clone(), state.mode()),
|
|
||||||
Some("ask"),
|
|
||||||
"Ask",
|
|
||||||
);
|
|
||||||
stack.set_visible_child_name(DEFAULT_TAB);
|
stack.set_visible_child_name(DEFAULT_TAB);
|
||||||
|
|
||||||
let switcher = tabs::build(&stack);
|
let switcher = tabs::build(&stack);
|
||||||
|
|
@ -188,17 +124,12 @@ fn build(app: &Application) -> Handle {
|
||||||
|
|
||||||
window.set_child(Some(&content_vbox));
|
window.set_child(Some(&content_vbox));
|
||||||
// Deliberately not presented here — `present()` (the caller) decides
|
// Deliberately not presented here — `present()` (the caller) decides
|
||||||
// whether this initial build should actually be shown, so a from-cold
|
// whether this initial build should actually be shown (see
|
||||||
// every-login autostart with onboarding already complete builds a
|
// `silent_autostart` above), so a from-cold every-login autostart with
|
||||||
// ready-but-hidden window instead of flashing it open. First run
|
// onboarding already complete builds a ready-but-hidden window instead
|
||||||
// presents the window with the day-zero wizard; the tour overlay
|
// of flashing it open. On a genuine first run, the tour overlay runs
|
||||||
// starts after that finishes.
|
// independently of this window (see `tour::start`) — it never needs to
|
||||||
|
// be shown at all until the user explicitly opens it later.
|
||||||
|
|
||||||
Handle {
|
Handle { window, home }
|
||||||
window,
|
|
||||||
home,
|
|
||||||
stack,
|
|
||||||
store,
|
|
||||||
binds,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue