Compare commits
3 commits
3251e18a49
...
1b02cec0a6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b02cec0a6 | ||
|
|
e5ce91a9c9 | ||
|
|
e5ba33d810 |
10 changed files with 146 additions and 124 deletions
1
.github/README.md
vendored
Normal file
1
.github/README.md
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
Forgejo (`.forgejo/workflows`) is the canonical CI. This `.github` tree is unused.
|
||||
67
.github/workflows/release.yml
vendored
67
.github/workflows/release.yml
vendored
|
|
@ -1,67 +0,0 @@
|
|||
name: release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: ["v*"]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
env:
|
||||
DL_DIR: /srv/breadway-dl
|
||||
ECOSYSTEM_DIR: /home/breadway/Projects/bread-ecosystem
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: [self-hosted, hestia]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: install build deps
|
||||
run: sudo apt-get install -y libgtk-4-dev librsvg2-dev libdbus-1-dev pkg-config 2>/dev/null || true
|
||||
|
||||
- name: build
|
||||
run: cargo build --release --locked
|
||||
|
||||
- name: prepare artifacts
|
||||
run: |
|
||||
VERSION="${GITHUB_REF_NAME#v}"
|
||||
PKG_DIR="${DL_DIR}/breadbox/${VERSION}"
|
||||
mkdir -p "${PKG_DIR}"
|
||||
for bin in breadbox breadbox-sync; do
|
||||
cp "target/release/${bin}" "${PKG_DIR}/${bin}-x86_64"
|
||||
strip "${PKG_DIR}/${bin}-x86_64"
|
||||
sha256sum "${PKG_DIR}/${bin}-x86_64" | awk '{print $1}' \
|
||||
> "${PKG_DIR}/${bin}-x86_64.sha256"
|
||||
done
|
||||
cp packaging/breadbox-sync.service "${PKG_DIR}/"
|
||||
cp config.example.toml "${PKG_DIR}/"
|
||||
cp bakery.toml "${PKG_DIR}/bakery.toml"
|
||||
ln -sfn "${VERSION}" "${DL_DIR}/breadbox/latest"
|
||||
|
||||
- name: ensure bread-ecosystem
|
||||
run: |
|
||||
if [[ -d "${ECOSYSTEM_DIR}/.git" ]]; then
|
||||
git -C "${ECOSYSTEM_DIR}" pull --ff-only
|
||||
else
|
||||
mkdir -p "$(dirname "${ECOSYSTEM_DIR}")"
|
||||
git clone https://github.com/Breadway/bread-ecosystem.git "${ECOSYSTEM_DIR}"
|
||||
fi
|
||||
|
||||
- name: regenerate index.json
|
||||
run: bash "${ECOSYSTEM_DIR}/scripts/gen-index.sh"
|
||||
|
||||
- name: upload to GitHub Release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
VERSION="${GITHUB_REF_NAME#v}"
|
||||
PKG_DIR="${DL_DIR}/breadbox/${VERSION}"
|
||||
gh release create "${GITHUB_REF_NAME}" \
|
||||
--title "breadbox v${VERSION}" --generate-notes 2>/dev/null || true
|
||||
gh release upload "${GITHUB_REF_NAME}" \
|
||||
"${PKG_DIR}/breadbox-x86_64" \
|
||||
"${PKG_DIR}/breadbox-sync-x86_64" \
|
||||
"${PKG_DIR}/breadbox-x86_64.sha256" \
|
||||
"${PKG_DIR}/breadbox-sync-x86_64.sha256" \
|
||||
--clobber
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -21,4 +21,3 @@ Thumbs.db
|
|||
.claude/
|
||||
|
||||
# Local hygiene notes (not for commit)
|
||||
CLAUDE.md
|
||||
|
|
|
|||
13
AGENTS.md
Normal file
13
AGENTS.md
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# AGENTS.md — Repo hygiene
|
||||
|
||||
Follow [`CONTRIBUTING.md`](CONTRIBUTING.md). Single-trunk: `main` plus short-lived `feature/` / `fix/` branches. `dev`/`beta` are bakery tracks (tags / main), not git branches.
|
||||
|
||||
## Remotes
|
||||
- `origin` — Forgejo (`git.breadway.dev`) — authoritative.
|
||||
- `github` — mirror. Day-to-day push `origin` only.
|
||||
|
||||
## Product
|
||||
GTK4 app launcher + `breadbox-sync` icon cache. Theme via `bread-theme` (pin by tag on `git.breadway.dev`). Toggle uses `bread-utils::singleton`, not a homegrown PID file. `EVENTS.md` is the bread-event contract (app id `box`); emit `bread.box.launched` after a successful launch. No command verbs.
|
||||
|
||||
## Distribution
|
||||
Bakery (`bakery.toml`). Forgejo `.forgejo/workflows/` is canonical; do not re-add a GitHub Actions release workflow.
|
||||
35
Cargo.lock
generated
35
Cargo.lock
generated
|
|
@ -85,17 +85,28 @@ checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
|
|||
[[package]]
|
||||
name = "bread-screenshots"
|
||||
version = "0.3.1"
|
||||
source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?branch=main#f86e299f4a0ea73ff485cd84923b986ddcc8242e"
|
||||
source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?rev=69ce2d67a8de8a09c064aa9d7ff99b46656f0b1d#69ce2d67a8de8a09c064aa9d7ff99b46656f0b1d"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bread-utils",
|
||||
"bread-utils 0.3.1 (git+https://git.breadway.dev/Breadway/bread-ecosystem?rev=69ce2d67a8de8a09c064aa9d7ff99b46656f0b1d)",
|
||||
"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]]
|
||||
name = "bread-theme"
|
||||
version = "0.2.3"
|
||||
source = "git+https://github.com/Breadway/bread-ecosystem?tag=v0.2.8#77417d552130281ff787e07d52541eb25e9d533b"
|
||||
version = "0.3.1"
|
||||
source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.1#db2fa3c4b4c1e6933bc5cf62a236d05972fdc886"
|
||||
dependencies = [
|
||||
"dirs",
|
||||
"gtk4",
|
||||
|
|
@ -106,7 +117,18 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "bread-utils"
|
||||
version = "0.3.1"
|
||||
source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?branch=main#f86e299f4a0ea73ff485cd84923b986ddcc8242e"
|
||||
source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.1#db2fa3c4b4c1e6933bc5cf62a236d05972fdc886"
|
||||
dependencies = [
|
||||
"bread-shared",
|
||||
"dirs",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bread-utils"
|
||||
version = "0.3.1"
|
||||
source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?rev=69ce2d67a8de8a09c064aa9d7ff99b46656f0b1d#69ce2d67a8de8a09c064aa9d7ff99b46656f0b1d"
|
||||
dependencies = [
|
||||
"dirs",
|
||||
"serde",
|
||||
|
|
@ -120,6 +142,7 @@ dependencies = [
|
|||
"anyhow",
|
||||
"bread-screenshots",
|
||||
"bread-theme",
|
||||
"bread-utils 0.3.1 (git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.1)",
|
||||
"breadbox-shared",
|
||||
"clap",
|
||||
"gtk4",
|
||||
|
|
@ -480,7 +503,7 @@ dependencies = [
|
|||
"gobject-sys",
|
||||
"libc",
|
||||
"system-deps",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
41
EVENTS.md
Normal file
41
EVENTS.md
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# breadbox — bread event integration
|
||||
|
||||
breadbox is a standalone app launcher: it works exactly the same with or
|
||||
without `breadd` running. When breadd *is* present, the GTK launcher
|
||||
publishes a single event into the shared bread automation fabric after a
|
||||
successful launch. 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: **`box`**. Transport: `bread-utils`'s `bread_client` module
|
||||
(feature `bread-client`) — `breadbox` links it directly. breadbox is a
|
||||
short-lived process (it exits when the launcher closes), so each `emit`
|
||||
is its own short-lived connection. There is no long-running daemon and
|
||||
therefore no command subscription.
|
||||
|
||||
## Events published (`bread.box.*`)
|
||||
|
||||
| Event | Data | When |
|
||||
|-------|------|------|
|
||||
| `bread.box.launched` | `{ "id": "<desktop id or exec>", "name": "<display name>" }` | The user launched an app (Enter / keypad Enter on the selected row, or activating a row) **and** the spawn succeeded. Not emitted if `Command::spawn` fails (missing terminal, `exec` that cannot start). `id` is the desktop-file id (the `.desktop` filename, e.g. `firefox.desktop`), falling back to the stripped `Exec=` line when that id is empty. `name` is the desktop-entry display name. |
|
||||
|
||||
Launch history is local to breadbox (`~/.cache/breadbox/history.json`);
|
||||
the event bus is a notification that a launch happened, not a channel
|
||||
for the exec line's arguments or the resulting process.
|
||||
|
||||
## Commands honored (`bread.command.box.*`)
|
||||
|
||||
None. breadbox is not a daemon — it is not running (and not subscribed)
|
||||
except while the launcher overlay is open. There is no existing
|
||||
"launch this desktop id from the bus" product surface, and inventing
|
||||
`bread.command.box.launch` (or similar) without that surface would be a
|
||||
stub. If/when breadbox grows a long-running piece that can honor a
|
||||
verb, the corresponding `bread.command.box.*` command should be added
|
||||
at the same time, not stubbed out 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) — launching,
|
||||
history, theming, and the singleton toggle are entirely unaffected.
|
||||
- Closing the launcher without launching anything emits nothing.
|
||||
|
|
@ -14,7 +14,7 @@ breadbox GTK4 layer-shell launcher
|
|||
- Reads the active Hyprland workspace and sorts apps by context priority
|
||||
- Fuzzy filtering as you type; Enter launches, Escape closes
|
||||
- App icons loaded from the resolved icon cache (see `breadbox-sync`)
|
||||
- pywal palette auto-detected from `~/.cache/wal/colors.json`, falls back to Catppuccin Mocha
|
||||
- pywal accents from `~/.cache/wal/colors.json`; background/surface/overlay/foreground stay fixed BOS dark
|
||||
- User CSS override at `~/.config/breadbox/style.css`
|
||||
- Toggle/dismiss: running a second instance kills the first
|
||||
|
||||
|
|
|
|||
|
|
@ -54,6 +54,9 @@ pub fn app_dirs() -> Vec<PathBuf> {
|
|||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DesktopEntry {
|
||||
/// Desktop file id (the `.desktop` filename, e.g. `firefox.desktop`).
|
||||
/// Empty only if the path had no file name; callers fall back to `exec`.
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub exec: String,
|
||||
pub icon_name: String,
|
||||
|
|
@ -155,7 +158,14 @@ pub fn parse_desktop(path: &Path) -> Option<DesktopEntry> {
|
|||
.map(|s| s.to_string())
|
||||
.collect();
|
||||
|
||||
let id = path
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().into_owned())
|
||||
.filter(|s| !s.is_empty())
|
||||
.unwrap_or_default();
|
||||
|
||||
Some(DesktopEntry {
|
||||
id,
|
||||
name,
|
||||
exec,
|
||||
icon_name,
|
||||
|
|
|
|||
|
|
@ -9,9 +9,12 @@ name = "breadbox"
|
|||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
bread-theme = { git = "https://github.com/Breadway/bread-ecosystem", tag = "v0.2.8", features = ["gtk"] }
|
||||
bread-theme = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.1", features = ["gtk"] }
|
||||
bread-utils = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.1", features = ["bread-client"] }
|
||||
# Capture primitives for `--screenshot` mode — see src/screenshot.rs.
|
||||
bread-screenshots = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", branch = "main" }
|
||||
# Not on tag v0.7.1 (crate landed after that tag); rev-pin so this is not
|
||||
# `branch = "main"`.
|
||||
bread-screenshots = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", rev = "69ce2d67a8de8a09c064aa9d7ff99b46656f0b1d" }
|
||||
breadbox-shared = { path = "../breadbox-shared" }
|
||||
gtk4 = { version = "0.11", features = ["v4_12"] }
|
||||
gtk4-layer-shell = "0.8"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
use bread_theme::{hex_to_rgba, ink_on, load_palette, Palette};
|
||||
use bread_utils::bread_client::BreadClient;
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
collections::HashMap,
|
||||
|
|
@ -11,6 +12,10 @@ use std::{
|
|||
rc::Rc,
|
||||
};
|
||||
|
||||
/// This app's id in bread's sibling-app namespace registry
|
||||
/// (`bread_shared::apps::KNOWN_APPS`) — events publish as `bread.box.*`.
|
||||
const APP_ID: &str = "box";
|
||||
|
||||
use breadbox_shared::{
|
||||
config_dir, load_all_desktop_entries, Config, DesktopEntry, IconCache, LaunchHistory,
|
||||
};
|
||||
|
|
@ -206,24 +211,42 @@ fn pick_terminal() -> String {
|
|||
|
||||
fn do_launch(entry: &DesktopEntry) {
|
||||
let cmd = entry.exec.trim();
|
||||
if entry.terminal {
|
||||
let spawned = if entry.terminal {
|
||||
let term = pick_terminal();
|
||||
let _ = Command::new(&term)
|
||||
Command::new(&term)
|
||||
.args(["-e", "bash", "-c", cmd])
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.spawn();
|
||||
.spawn()
|
||||
} else {
|
||||
let _ = Command::new("bash")
|
||||
Command::new("bash")
|
||||
.args(["-c", cmd])
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.spawn();
|
||||
.spawn()
|
||||
};
|
||||
if spawned.is_ok() {
|
||||
emit_launched(entry);
|
||||
}
|
||||
}
|
||||
|
||||
/// Publishes `bread.box.launched` after a successful spawn. Fire-and-forget
|
||||
/// and non-fatal (`BreadClient::emit` never blocks or errors this caller) —
|
||||
/// breadd being absent must never affect launching itself.
|
||||
fn emit_launched(entry: &DesktopEntry) {
|
||||
let id = if entry.id.is_empty() {
|
||||
entry.exec.as_str()
|
||||
} else {
|
||||
entry.id.as_str()
|
||||
};
|
||||
BreadClient::connect(APP_ID).emit(
|
||||
"bread.box.launched",
|
||||
serde_json::json!({ "id": id, "name": entry.name }),
|
||||
);
|
||||
}
|
||||
|
||||
// ---- Fuzzy matching ---------------------------------------------------------
|
||||
|
||||
fn fuzzy_matches(pattern: &str, text: &str) -> bool {
|
||||
|
|
@ -254,40 +277,6 @@ fn fuzzy_score(query: &str, entry: &DesktopEntry) -> u32 {
|
|||
4 // subsequence match
|
||||
}
|
||||
|
||||
// ---- PID file toggle --------------------------------------------------------
|
||||
|
||||
fn pid_file() -> PathBuf {
|
||||
env::var("XDG_RUNTIME_DIR")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|_| PathBuf::from("/tmp"))
|
||||
.join("breadbox.pid")
|
||||
}
|
||||
|
||||
fn is_breadbox_pid(pid: u32) -> bool {
|
||||
fs::read_to_string(format!("/proc/{}/comm", pid))
|
||||
.map(|s| s.trim() == "breadbox")
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
// Returns false if an existing instance was killed (caller should exit).
|
||||
fn toggle_or_continue() -> bool {
|
||||
let pf = pid_file();
|
||||
if let Ok(content) = fs::read_to_string(&pf) {
|
||||
if let Ok(pid) = content.trim().parse::<u32>() {
|
||||
if is_breadbox_pid(pid) {
|
||||
let _ = Command::new("kill").arg(pid.to_string()).status();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
let _ = fs::write(&pf, std::process::id().to_string());
|
||||
true
|
||||
}
|
||||
|
||||
fn cleanup_pid() {
|
||||
let _ = fs::remove_file(pid_file());
|
||||
}
|
||||
|
||||
// ---- UI ---------------------------------------------------------------------
|
||||
|
||||
fn get_row_entry(row: >k4::ListBoxRow) -> Option<DesktopEntry> {
|
||||
|
|
@ -344,7 +333,6 @@ fn run_ui(
|
|||
let close_all: Rc<dyn Fn()> = Rc::new({
|
||||
let w = window.clone();
|
||||
move || {
|
||||
cleanup_pid();
|
||||
w.close();
|
||||
}
|
||||
});
|
||||
|
|
@ -551,8 +539,6 @@ fn run_ui(
|
|||
});
|
||||
window.add_controller(outside_click);
|
||||
|
||||
window.connect_destroy(|_| cleanup_pid());
|
||||
|
||||
if let Some(req) = screenshot_req.clone() {
|
||||
screenshot::dispatch(&window, req);
|
||||
}
|
||||
|
|
@ -578,14 +564,27 @@ fn main() {
|
|||
let cli = screenshot::Cli::parse();
|
||||
let screenshot_req = cli.screenshot_request();
|
||||
|
||||
// The PID-file toggle kills whatever's holding the file — a real,
|
||||
// already-running breadbox instance included. A screenshot run must
|
||||
// `toggle_or_kill` kills whatever's holding the single-instance lock —
|
||||
// a real, already-running breadbox included. A screenshot run must
|
||||
// never touch it: it's a separate, disposable instance by design (same
|
||||
// reasoning as breadbar's `allow_multiple_instances`), not a toggle of
|
||||
// the operator's real launcher.
|
||||
if screenshot_req.is_none() && !toggle_or_continue() {
|
||||
return;
|
||||
}
|
||||
//
|
||||
// Kept alive for the rest of `main` — dropping it releases the
|
||||
// single-instance lock and removes the pid file, which happens
|
||||
// naturally once `run_ui` returns (after the window closes).
|
||||
let _singleton_guard = if screenshot_req.is_some() {
|
||||
None
|
||||
} else {
|
||||
match bread_utils::singleton::toggle_or_kill("breadbox") {
|
||||
Ok(bread_utils::singleton::Toggle::Started(guard)) => Some(guard),
|
||||
Ok(bread_utils::singleton::Toggle::KilledExisting) => return,
|
||||
Err(e) => {
|
||||
eprintln!("breadbox: single-instance lock unavailable ({e}); continuing without it");
|
||||
None
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let config = Config::load();
|
||||
let workspace = get_active_workspace().unwrap_or_default();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue