From 470e1a160c8a193d29515ac915150989d217b7be Mon Sep 17 00:00:00 2001 From: gooze Date: Fri, 10 Jul 2026 19:53:39 +0200 Subject: [PATCH 01/10] implemented hooks and custom scheme fixes --- README.md | 30 ++++++++++++ src/app.rs | 2 + src/config.rs | 13 +++++ src/pipeline/hooks.rs | 45 ++++++++++++++++++ src/pipeline/mod.rs | 27 +++++++++++ src/pipeline/slideshow.rs | 4 +- src/pipeline/tinty.rs | 84 +++++++++++++++++++++++++++++++++ src/pipeline/wallpaper_cache.rs | 14 ++++++ 8 files changed, 218 insertions(+), 1 deletion(-) create mode 100644 src/pipeline/hooks.rs diff --git a/README.md b/README.md index d327656..6934d62 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,8 @@ tinty sync See the [Tinty documentation](https://github.com/tinted-theming/tinty) for details. +For **custom schemes** (from `custom_schemes_dir`), gnomad automatically writes a copy into tinty's own custom-schemes directory (`~/.local/share/tinted-theming/tinty/custom-schemes//.yaml`) immediately before every `tinty apply` call — no manual registration or `tinty sync` needed. This file is regenerated on every apply, so edits to your source YAML in `custom_schemes_dir` take effect on the next scheme switch. + gnomad will warn on startup if User Themes is not detected or if Tinty/gowall are missing from `$PATH`. ### 4. Adwaita for Steam (optional) @@ -149,6 +151,11 @@ wallpaper_enabled = true # set to false to disable all wallpaper feature adwaita_steam_enabled = false # set to true to enable Adwaita for Steam integration slideshow_static_secs = 3600 # seconds each wallpaper is shown before crossfading slideshow_transition_secs = 1800 # seconds each crossfade lasts + +[hooks] +# on_scheme_apply = "..." +# on_wallpaper_apply = "..." +# on_wallpaper_batch_convert = "..." ``` | Key | Default | Description | @@ -169,6 +176,27 @@ The wallpaper directory can also be changed at runtime with `[d]` in the wallpap --- +## Hooks + +gnomad can run arbitrary shell commands after key events, with event-specific `GNOMAD_*` environment variables injected for the hook to reference. Hooks run via `sh -c ""` with a 30s timeout, and never block or fail the underlying gnomad operation — failures are logged to `~/.local/share/gnomad/gnomad.log` (run with `-v`) but otherwise silent. + +Configure them under a `[hooks]` table in `config.toml`: + +```toml +[hooks] +on_wallpaper_batch_convert = "/usr/bin/gsettings set org.gnome.shell.extensions.wallpaper-slideshow.picture-uri-list-source-directory \"file://$GNOMAD_CACHE_DIR\"" +``` + +> Use full paths like `/usr/bin/gsettings` rather than bare `gsettings` — avoids PATH issues with tools like Homebrew shadowing system binaries, and mirrors gnomad's own internal convention (see `gsettings_set`/`gsettings_get` in `src/pipeline/gnome.rs`). + +| Key | Fires | Env vars available | +|---|---|---| +| `on_scheme_apply` | After a full scheme switch completes successfully | `GNOMAD_SCHEME_SLUG`, `GNOMAD_SCHEME_NAME`, `GNOMAD_SCHEME_SYSTEM` (`base16`/`base24`), `GNOMAD_SCHEME_VARIANT` (empty if unset), `GNOMAD_WALLPAPER_PATH` (empty if wallpapers disabled) | +| `on_wallpaper_apply` | After a standalone wallpaper switch (no scheme change) | `GNOMAD_WALLPAPER_PATH`, `GNOMAD_SCHEME_SLUG` (empty if no active scheme) | +| `on_wallpaper_batch_convert` | After `[c]`/`[Shift+C]` batch-converts a wallpaper directory, and after `[s]` creates a slideshow (which batch-converts internally) | `GNOMAD_SCHEME_SLUG`, `GNOMAD_CACHE_DIR` (the per-scheme cache directory, now populated), `GNOMAD_WALLPAPER_COUNT` | + +--- + ## Key Bindings | Key | Action | @@ -222,6 +250,8 @@ Picking a wallpaper and pressing `Enter` runs only gowall + wallpaper set; no CS Place any base16/base24 YAML files in your configured `custom_schemes_dir`. They appear in the browser with a `[*]` tag and support everything the catalogue schemes do. Both the new format (with `palette:` key) and the legacy flat format are parsed. +Under the hood, gnomad mirrors each custom scheme into tinty's `custom-schemes//` directory before applying, so Tinty-themed apps (kitty, neovim, etc.) pick up custom schemes exactly like catalogue ones. + --- diff --git a/src/app.rs b/src/app.rs index 98a533b..efa9ca8 100644 --- a/src/app.rs +++ b/src/app.rs @@ -620,6 +620,7 @@ impl App { &config.wallpaper_dir, &config.wallpaper_cache_dir, force, + config.hooks.on_wallpaper_batch_convert.clone(), status_tx, ) .await; @@ -662,6 +663,7 @@ impl App { &scheme, config.slideshow_static_secs, config.slideshow_transition_secs, + config.hooks.on_wallpaper_batch_convert.clone(), status_tx, ) .await; diff --git a/src/config.rs b/src/config.rs index 04c7f17..eb600d9 100644 --- a/src/config.rs +++ b/src/config.rs @@ -33,6 +33,8 @@ pub struct Config { /// short transitions (≤60 s) produce visible 1fps stepping. #[serde(default = "default_slideshow_transition_secs")] pub slideshow_transition_secs: u64, + #[serde(default)] + pub hooks: HooksConfig, } fn default_theme_name() -> String { @@ -55,6 +57,16 @@ fn default_true() -> bool { true } +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct HooksConfig { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub on_scheme_apply: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub on_wallpaper_apply: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub on_wallpaper_batch_convert: Option, +} + fn default_slideshow_static_secs() -> u64 { 3600 } @@ -113,6 +125,7 @@ impl Config { adwaita_steam_enabled: false, slideshow_static_secs: default_slideshow_static_secs(), slideshow_transition_secs: default_slideshow_transition_secs(), + hooks: HooksConfig::default(), } } } diff --git a/src/pipeline/hooks.rs b/src/pipeline/hooks.rs new file mode 100644 index 0000000..92ca46e --- /dev/null +++ b/src/pipeline/hooks.rs @@ -0,0 +1,45 @@ +use std::process::Stdio; +use std::time::Duration; + +/// Run a user-configured hook command via `sh -c`, injecting `envs`. +/// No-ops silently if `hook_cmd` is None or blank. Never returns an error — +/// failures (spawn error, non-zero exit, timeout) are only logged, matching +/// gnomad's convention that non-critical side channels never fail the +/// main pipeline. +pub async fn run(hook_cmd: Option<&str>, envs: &[(&str, String)]) { + let Some(cmd) = hook_cmd else { return }; + let cmd = cmd.trim(); + if cmd.is_empty() { + return; + } + + tracing::debug!("running hook: {cmd}"); + + let mut command = tokio::process::Command::new("sh"); + command + .args(["-c", cmd]) + .envs(envs.iter().map(|(k, v)| (*k, v.as_str()))) + .stdout(Stdio::null()) + .stderr(Stdio::piped()) + .kill_on_drop(true); + + let child = match command.spawn() { + Ok(c) => c, + Err(e) => { + tracing::warn!("hook failed to spawn: {e:#} (cmd: {cmd})"); + return; + } + }; + + match tokio::time::timeout(Duration::from_secs(30), child.wait_with_output()).await { + Ok(Ok(output)) if output.status.success() => { + tracing::debug!("hook completed successfully"); + } + Ok(Ok(output)) => { + let stderr = String::from_utf8_lossy(&output.stderr); + tracing::warn!("hook exited with {} (cmd: {cmd}): {}", output.status, stderr.trim()); + } + Ok(Err(e)) => tracing::warn!("hook wait failed: {e:#} (cmd: {cmd})"), + Err(_elapsed) => tracing::warn!("hook timed out after 30s (cmd: {cmd})"), + } +} diff --git a/src/pipeline/mod.rs b/src/pipeline/mod.rs index fea4c32..cd49d90 100644 --- a/src/pipeline/mod.rs +++ b/src/pipeline/mod.rs @@ -1,6 +1,7 @@ pub mod gnome; pub mod gowall; pub mod gtk_css; +pub mod hooks; pub mod palette; pub mod shade; pub mod shell_css; @@ -65,6 +66,12 @@ pub async fn apply_scheme( } } + // Sync custom scheme into tinty's own custom-schemes dir so `tinty apply` + // can find it — tinty has no visibility into gnomad's custom_schemes_dir. + if scheme.is_custom { + tinty::sync_custom_scheme(scheme).await?; + } + // Step 2: Tinty let _ = status_tx.send("[ applying tinty scheme... ]".to_string()); let scheme_arg = format!("{}-{}", &scheme.system.tag(true), scheme.slug); @@ -118,6 +125,20 @@ pub async fn apply_scheme( // Reload GNOME Shell CSS via user-theme extension cycle (same mechanism as Rewaita) debug!("reloading shell theme extension"); gnome.reload_shell_theme().await; + + let envs = [ + ("GNOMAD_SCHEME_SLUG", scheme.slug.clone()), + ("GNOMAD_SCHEME_NAME", scheme.name.clone()), + ("GNOMAD_SCHEME_SYSTEM", scheme.system.tag(true).to_string()), + ("GNOMAD_SCHEME_VARIANT", scheme.variant.clone().unwrap_or_default()), + ("GNOMAD_WALLPAPER_PATH", if source_wallpaper.is_some() { + output_wall.display().to_string() + } else { + String::new() + }), + ]; + hooks::run(config.hooks.on_scheme_apply.as_deref(), &envs).await; + Ok(output_wall) } @@ -162,5 +183,11 @@ pub async fn apply_wallpaper( let _ = status_tx.send("[ setting wallpaper... ]".to_string()); gnome.set_wallpaper(&output).await?; + let envs = [ + ("GNOMAD_WALLPAPER_PATH", output.display().to_string()), + ("GNOMAD_SCHEME_SLUG", active_scheme.map(|s| s.slug.clone()).unwrap_or_default()), + ]; + hooks::run(config.hooks.on_wallpaper_apply.as_deref(), &envs).await; + Ok(output) } diff --git a/src/pipeline/slideshow.rs b/src/pipeline/slideshow.rs index e05e836..fefbb9d 100644 --- a/src/pipeline/slideshow.rs +++ b/src/pipeline/slideshow.rs @@ -4,6 +4,7 @@ use crate::config::data_dir; use anyhow::{Context, Result}; use std::path::{Path, PathBuf}; +#[allow(clippy::too_many_arguments)] pub async fn create_slideshow( wallpaper_dir: &Path, cache_dir: &Path, @@ -11,9 +12,10 @@ pub async fn create_slideshow( scheme: &Scheme, static_secs: u64, transition_secs: u64, + hook_cmd: Option, status_tx: tokio::sync::watch::Sender, ) -> Result { - wallpaper_cache::batch_convert(scheme, wallpaper_dir, cache_dir, false, status_tx.clone()) + wallpaper_cache::batch_convert(scheme, wallpaper_dir, cache_dir, false, hook_cmd, status_tx.clone()) .await?; let _ = status_tx.send("[ generating slideshow xml... ]".to_string()); diff --git a/src/pipeline/tinty.rs b/src/pipeline/tinty.rs index 14f277f..a3807be 100644 --- a/src/pipeline/tinty.rs +++ b/src/pipeline/tinty.rs @@ -1,4 +1,8 @@ +use crate::schemes::types::{Scheme, SchemeSystem}; use anyhow::{Context, Result}; +use serde::Serialize; +use std::collections::BTreeMap; +use std::path::PathBuf; use tokio::process::Command; pub async fn apply_scheme(slug: &str) -> Result<()> { @@ -14,3 +18,83 @@ pub async fn apply_scheme(slug: &str) -> Result<()> { } Ok(()) } + +/// Directory tinty scans for user-registered custom schemes — mirrors the +/// exact layout tinty's own `generate-scheme --save` writes to: +/// /custom-schemes//.yaml +pub fn tinty_custom_schemes_dir(system: &SchemeSystem) -> PathBuf { + dirs::data_local_dir() + .unwrap_or_else(|| PathBuf::from("~/.local/share")) + .join("tinted-theming") + .join("tinty") + .join("custom-schemes") + .join(system.tag(true)) +} + +#[derive(Serialize)] +struct TintyBuilderYaml { + system: &'static str, + name: String, + author: String, + variant: String, + palette: BTreeMap, +} + +/// Serialize `scheme` into the canonical tinted-builder YAML format and write +/// it into tinty's own custom-schemes directory so `tinty apply ` can +/// find it. Always overwrites, so edits to gnomad's custom scheme source YAML +/// propagate on next apply. No `tinty sync` needed — tinty's apply command +/// reads custom-schemes/ directly, bypassing its git-managed repos entirely. +pub async fn sync_custom_scheme(scheme: &Scheme) -> Result<()> { + let dir = tinty_custom_schemes_dir(&scheme.system); + tokio::fs::create_dir_all(&dir) + .await + .with_context(|| format!("creating {}", dir.display()))?; + + let variant = scheme.variant.clone().unwrap_or_else(|| { + tracing::debug!( + "custom scheme '{}' has no variant set; defaulting to \"dark\" for tinty sync", + scheme.slug + ); + "dark".to_string() + }); + + let mut palette = BTreeMap::new(); + for (k, v) in [ + ("base00", &scheme.base00), ("base01", &scheme.base01), ("base02", &scheme.base02), + ("base03", &scheme.base03), ("base04", &scheme.base04), ("base05", &scheme.base05), + ("base06", &scheme.base06), ("base07", &scheme.base07), ("base08", &scheme.base08), + ("base09", &scheme.base09), ("base0A", &scheme.base0a), ("base0B", &scheme.base0b), + ("base0C", &scheme.base0c), ("base0D", &scheme.base0d), ("base0E", &scheme.base0e), + ("base0F", &scheme.base0f), + ] { + palette.insert(k.to_string(), format!("#{v}")); + } + if scheme.system == SchemeSystem::Base24 { + for (k, v) in [ + ("base10", &scheme.base10), ("base11", &scheme.base11), ("base12", &scheme.base12), + ("base13", &scheme.base13), ("base14", &scheme.base14), ("base15", &scheme.base15), + ("base16", &scheme.base16), ("base17", &scheme.base17), + ] { + if let Some(hex) = v { + palette.insert(k.to_string(), format!("#{hex}")); + } + } + } + + let doc = TintyBuilderYaml { + system: scheme.system.tag(true), + name: scheme.name.clone(), + author: scheme.author.clone(), + variant, + palette, + }; + + let yaml = serde_yaml::to_string(&doc).context("serializing scheme for tinty sync")?; + let out_path = dir.join(format!("{}.yaml", scheme.slug)); + tokio::fs::write(&out_path, yaml) + .await + .with_context(|| format!("writing {}", out_path.display()))?; + tracing::debug!("synced custom scheme to tinty: {}", out_path.display()); + Ok(()) +} diff --git a/src/pipeline/wallpaper_cache.rs b/src/pipeline/wallpaper_cache.rs index 2a48101..37a8961 100644 --- a/src/pipeline/wallpaper_cache.rs +++ b/src/pipeline/wallpaper_cache.rs @@ -1,4 +1,5 @@ use crate::pipeline::gowall::write_palette_json; +use crate::pipeline::hooks; use crate::schemes::types::Scheme; use anyhow::{Context, Result}; use serde::{Deserialize, Serialize}; @@ -93,6 +94,7 @@ pub async fn batch_convert( wallpaper_dir: &Path, cache_dir: &Path, force: bool, + hook_cmd: Option, status_tx: tokio::sync::watch::Sender, ) -> Result<()> { let slug_cache_dir = scheme_cache_dir(cache_dir, &scheme.slug, wallpaper_dir); @@ -147,6 +149,12 @@ pub async fn batch_convert( if uncached_count == 0 { let _ = status_tx.send(format!("[ all {source_count} wallpapers already cached ]")); + let envs = [ + ("GNOMAD_SCHEME_SLUG", scheme.slug.clone()), + ("GNOMAD_CACHE_DIR", slug_cache_dir.display().to_string()), + ("GNOMAD_WALLPAPER_COUNT", source_count.to_string()), + ]; + hooks::run(hook_cmd.as_deref(), &envs).await; return Ok(()); } @@ -192,6 +200,12 @@ pub async fn batch_convert( if failed == 0 { let _ = status_tx.send(format!("[ converted {completed} wallpapers ]")); + let envs = [ + ("GNOMAD_SCHEME_SLUG", scheme.slug.clone()), + ("GNOMAD_CACHE_DIR", slug_cache_dir.display().to_string()), + ("GNOMAD_WALLPAPER_COUNT", completed.to_string()), + ]; + hooks::run(hook_cmd.as_deref(), &envs).await; Ok(()) } else { let _ = status_tx.send(format!("[ {completed} done, {failed} failed ]")); From a7599c665f60af71f80bc2c5d71c97857a097ee8 Mon Sep 17 00:00:00 2001 From: gooze Date: Fri, 10 Jul 2026 20:16:21 +0200 Subject: [PATCH 02/10] ci: convert release script to GitHub Action, add local test-deploy script Release now runs automatically in CI when a push to main bumps the Cargo.toml version, replacing the interactive scripts/release.sh. Also adds scripts/test-deploy.sh for installing/reverting a local dev build via Homebrew without touching the published tap. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/release.yml | 128 ++++++++++++++++++++++++++++++ scripts/release.sh | 144 ---------------------------------- scripts/test-deploy.sh | 91 +++++++++++++++++++++ 3 files changed, 219 insertions(+), 144 deletions(-) create mode 100644 .github/workflows/release.yml delete mode 100755 scripts/release.sh create mode 100755 scripts/test-deploy.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..0959de5 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,128 @@ +name: Release + +# Triggers whenever a push to main changes Cargo.toml. The `check-version` job +# then confirms the `version = "..."` line actually changed (vs. some other +# Cargo.toml edit landing on main) before the release job runs anything. +# +# To cut a release: bump `version` in Cargo.toml, commit with a message +# describing the release (used verbatim as the GitHub release notes), and +# push to main. +on: + push: + branches: [main] + paths: [Cargo.toml] + +permissions: + contents: write + +env: + CARGO_TERM_COLOR: always + +jobs: + check-version: + runs-on: ubuntu-latest + outputs: + changed: ${{ steps.check.outputs.changed }} + version: ${{ steps.check.outputs.version }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 2 + + - id: check + run: | + NEW="$(grep -m1 '^version = ' Cargo.toml | sed -E 's/version = "(.*)"/\1/')" + if git rev-parse HEAD~1 >/dev/null 2>&1; then + OLD="$(git show HEAD~1:Cargo.toml | grep -m1 '^version = ' | sed -E 's/version = "(.*)"/\1/' || true)" + else + OLD="" + fi + echo "old version: $OLD" + echo "new version: $NEW" + if [[ "$NEW" != "$OLD" ]]; then + echo "changed=true" >> "$GITHUB_OUTPUT" + echo "version=$NEW" >> "$GITHUB_OUTPUT" + else + echo "changed=false" >> "$GITHUB_OUTPUT" + fi + + release: + needs: check-version + if: needs.check-version.outputs.changed == 'true' + runs-on: ubuntu-latest + env: + VERSION: ${{ needs.check-version.outputs.version }} + steps: + - uses: actions/checkout@v4 + + - name: Install cross + run: cargo install cross --locked + + - name: Extract release note from commit message + id: note + run: | + { + echo "note<> "$GITHUB_OUTPUT" + + - name: Build x86_64 + run: cargo build --release + + - name: Package x86_64 + run: (cd target/release && tar czf "$GITHUB_WORKSPACE/gnomad-x86_64-unknown-linux-gnu.tar.gz" gnomad) + + - name: Cross-build aarch64 + run: cross build --release --target aarch64-unknown-linux-gnu + + - name: Package aarch64 + run: (cd target/aarch64-unknown-linux-gnu/release && tar czf "$GITHUB_WORKSPACE/gnomad-aarch64-unknown-linux-gnu.tar.gz" gnomad) + + - name: Create GitHub release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release create "v$VERSION" \ + --repo GooseRooster/gnomad \ + --title "v$VERSION" \ + --notes "${{ steps.note.outputs.note }}" \ + gnomad-x86_64-unknown-linux-gnu.tar.gz \ + gnomad-aarch64-unknown-linux-gnu.tar.gz + + - name: Compute checksums + id: sha + run: | + echo "x86=$(sha256sum gnomad-x86_64-unknown-linux-gnu.tar.gz | awk '{print $1}')" >> "$GITHUB_OUTPUT" + echo "arm=$(sha256sum gnomad-aarch64-unknown-linux-gnu.tar.gz | awk '{print $1}')" >> "$GITHUB_OUTPUT" + + - name: Publish to crates.io + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + run: cargo publish + + - name: Update homebrew tap + env: + X86_SHA: ${{ steps.sha.outputs.x86 }} + ARM_SHA: ${{ steps.sha.outputs.arm }} + TAP_REPO_TOKEN: ${{ secrets.TAP_REPO_TOKEN }} + run: | + git clone "https://x-access-token:${TAP_REPO_TOKEN}@github.com/GooseRooster/homebrew-gnomad.git" tap + FORMULA="tap/Formula/gnomad.rb" + + sed -i "s/version \".*\"/version \"$VERSION\"/" "$FORMULA" + sed -i \ + "s|releases/download/v[^/]*/gnomad-x86_64|releases/download/v$VERSION/gnomad-x86_64|" \ + "$FORMULA" + sed -i \ + "s|releases/download/v[^/]*/gnomad-aarch64|releases/download/v$VERSION/gnomad-aarch64|" \ + "$FORMULA" + sed -i "/gnomad-x86_64-unknown-linux-gnu.tar.gz\"/{n;s/sha256 \".*\"/sha256 \"$X86_SHA\"/}" "$FORMULA" + sed -i "/gnomad-aarch64-unknown-linux-gnu.tar.gz\"/{n;s/sha256 \".*\"/sha256 \"$ARM_SHA\"/}" "$FORMULA" + + cd tap + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add Formula/gnomad.rb + git commit -m "gnomad $VERSION" + git push diff --git a/scripts/release.sh b/scripts/release.sh deleted file mode 100755 index d8dfebc..0000000 --- a/scripts/release.sh +++ /dev/null @@ -1,144 +0,0 @@ -#!/usr/bin/env bash -# Release script for gnomad. -# Run from the repo root: ./scripts/release.sh -# Requires: cargo, cargo-cross, gh (GitHub CLI), git, sha256sum - -set -euo pipefail - -REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -TAP_DIR="$HOME/Development/homebrew-gnomad" -CARGO_TOML="$REPO_ROOT/Cargo.toml" - -# ── Preflight checks ────────────────────────────────────────────────────────── - -for cmd in cargo gh git sha256sum; do - if ! command -v "$cmd" &>/dev/null; then - echo "error: '$cmd' not found in PATH" >&2 - exit 1 - fi -done - -if ! cargo cross --version &>/dev/null 2>&1; then - echo "error: 'cargo-cross' not found — install with: cargo install cross" >&2 - exit 1 -fi - -if [[ ! -d "$TAP_DIR" ]]; then - echo "error: tap directory not found: $TAP_DIR" >&2 - exit 1 -fi - -# ── Prompt for release details ──────────────────────────────────────────────── - -read -rp "New version (e.g. 0.4.0): " VERSION -if [[ -z "$VERSION" ]]; then - echo "error: version cannot be empty" >&2 - exit 1 -fi - -read -rp "Release note (one-line summary): " RELEASE_NOTE -if [[ -z "$RELEASE_NOTE" ]]; then - echo "error: release note cannot be empty" >&2 - exit 1 -fi - -echo -echo "Releasing gnomad v$VERSION: $RELEASE_NOTE" -echo - -# ── Update Cargo.toml version ───────────────────────────────────────────────── - -echo "==> Updating Cargo.toml to version $VERSION" -sed -i "s/^version = \".*\"/version = \"$VERSION\"/" "$CARGO_TOML" - -# ── Build x86_64 ───────────────────────────────────────────────────────────── - -echo "==> Building x86_64 release binary" -cargo build --release --manifest-path "$CARGO_TOML" - -X86_BINARY="$REPO_ROOT/target/release/gnomad" -X86_TARBALL="gnomad-x86_64-unknown-linux-gnu.tar.gz" - -echo "==> Creating $X86_TARBALL" -(cd "$REPO_ROOT/target/release" && tar czf "$REPO_ROOT/$X86_TARBALL" gnomad) - -# ── Build aarch64 ──────────────────────────────────────────────────────────── - -echo "==> Cross-compiling aarch64 release binary" -cargo cross build --release --target aarch64-unknown-linux-gnu --manifest-path "$CARGO_TOML" - -ARM_TARBALL="gnomad-aarch64-unknown-linux-gnu.tar.gz" - -echo "==> Creating $ARM_TARBALL" -(cd "$REPO_ROOT/target/aarch64-unknown-linux-gnu/release" && tar czf "$REPO_ROOT/$ARM_TARBALL" gnomad) - -# ── Create GitHub release ───────────────────────────────────────────────────── - -echo "==> Creating GitHub release v$VERSION" -gh release create "v$VERSION" \ - --repo GooseRooster/gnomad \ - --title "v$VERSION" \ - --notes "$RELEASE_NOTE" \ - "$REPO_ROOT/$X86_TARBALL" \ - "$REPO_ROOT/$ARM_TARBALL" - -# ── Compute SHA256 hashes ──────────────────────────────────────────────────── - -echo "==> Computing SHA256 hashes" -X86_SHA=$(sha256sum "$REPO_ROOT/$X86_TARBALL" | awk '{print $1}') -ARM_SHA=$(sha256sum "$REPO_ROOT/$ARM_TARBALL" | awk '{print $1}') - -echo " x86_64: $X86_SHA" -echo " aarch64: $ARM_SHA" - -# ── Update the brew formula ─────────────────────────────────────────────────── - -FORMULA="$TAP_DIR/Formula/gnomad.rb" -echo "==> Updating $FORMULA" - -sed -i "s/version \".*\"/version \"$VERSION\"/" "$FORMULA" -sed -i "s|sha256 \"PLACEHOLDER_X86_64\"|sha256 \"$X86_SHA\"|" "$FORMULA" -sed -i "s|sha256 \"PLACEHOLDER_AARCH64\"|sha256 \"$ARM_SHA\"|" "$FORMULA" - -# Handle the case where placeholders were already replaced in a previous release -# by targeting the architecture-specific lines directly. -DOWNLOAD_BASE="https://github.com/GooseRooster/gnomad/releases/download/v$VERSION" -sed -i \ - "s|releases/download/v[^/]*/gnomad-x86_64|releases/download/v$VERSION/gnomad-x86_64|" \ - "$FORMULA" -sed -i \ - "s|releases/download/v[^/]*/gnomad-aarch64|releases/download/v$VERSION/gnomad-aarch64|" \ - "$FORMULA" -sed -i "/gnomad-x86_64-unknown-linux-gnu.tar.gz\"/{n;s/sha256 \".*\"/sha256 \"$X86_SHA\"/}" \ - "$FORMULA" -sed -i "/gnomad-aarch64-unknown-linux-gnu.tar.gz\"/{n;s/sha256 \".*\"/sha256 \"$ARM_SHA\"/}" \ - "$FORMULA" - -# ── Commit and push gnomad repo ─────────────────────────────────────────────── - -echo "==> Committing version bump in gnomad repo" -cd "$REPO_ROOT" -git add Cargo.toml Cargo.lock -git commit -m "chore: bump version to $VERSION" -git push - -# ── Publish to crates.io ───────────────────────────────────────────────────── - -echo "==> Publishing to crates.io" -cargo publish - -# ── Commit and push tap repo ───────────────────────────────────────────────── - -echo "==> Committing formula update in homebrew-gnomad tap" -cd "$TAP_DIR" -git add Formula/gnomad.rb -git commit -m "gnomad $VERSION" -git push - -# ── Clean up tarballs ───────────────────────────────────────────────────────── - -rm -f "$REPO_ROOT/$X86_TARBALL" "$REPO_ROOT/$ARM_TARBALL" - -echo -echo "Done! gnomad v$VERSION released." -echo "Install with: brew install GooseRooster/gnomad/gnomad" diff --git a/scripts/test-deploy.sh b/scripts/test-deploy.sh new file mode 100755 index 0000000..cb589de --- /dev/null +++ b/scripts/test-deploy.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +# Local test-deploy script for gnomad. +# +# Installs the current local checkout via Homebrew, building from source with +# cargo, so you can test changes as an actual installed binary without +# touching the published tap/formula. +# +# Homebrew builds from a git checkout of this repo (via a `url ..., using: +# :git` formula), so it only ever sees committed state — uncommitted changes +# are not picked up. +# +# Usage: +# ./scripts/test-deploy.sh # uninstall brew gnomad, reinstall from local repo +# ./scripts/test-deploy.sh --revert # uninstall local build, reinstall from GooseRooster/gnomad tap + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +TAP="GooseRooster/gnomad" + +for cmd in brew git; do + if ! command -v "$cmd" &>/dev/null; then + echo "error: '$cmd' not found in PATH" >&2 + exit 1 + fi +done + +uninstall_if_present() { + if brew list --formula 2>/dev/null | grep -qx gnomad; then + echo "==> Uninstalling existing gnomad" + brew uninstall --formula gnomad + fi +} + +if [[ "${1:-}" == "--revert" ]]; then + uninstall_if_present + echo "==> Ensuring tap $TAP is present" + brew tap "$TAP" >/dev/null + echo "==> Installing latest gnomad from $TAP" + brew install "$TAP/gnomad" + echo + echo "Done. $(gnomad --version)" + exit 0 +fi + +if [[ -n "${1:-}" ]]; then + echo "error: unknown argument '$1' (expected --revert or no arguments)" >&2 + exit 1 +fi + +if [[ -n "$(git -C "$REPO_ROOT" status --porcelain)" ]]; then + echo "error: you have uncommitted changes." >&2 + echo "Homebrew builds from a git checkout of the local repo, so it only sees committed state." >&2 + echo "Commit (or stash) your changes first, then re-run this script." >&2 + exit 1 +fi + +BRANCH="$(git -C "$REPO_ROOT" rev-parse --abbrev-ref HEAD)" +COMMIT="$(git -C "$REPO_ROOT" rev-parse --short HEAD)" +LOCAL_VERSION="$(grep -m1 '^version = ' "$REPO_ROOT/Cargo.toml" | sed -E 's/version = "(.*)"/\1/')-local" + +uninstall_if_present + +FORMULA_DIR="$(mktemp -d)" +trap 'rm -rf "$FORMULA_DIR"' EXIT +FORMULA="$FORMULA_DIR/gnomad.rb" + +cat > "$FORMULA" < Installing gnomad from local checkout (branch: $BRANCH, commit: $COMMIT)" +brew install --build-from-source "$FORMULA" + +echo +echo "Done. $(gnomad --version)" +echo "This is a local dev build — run '$(basename "$0") --revert' to go back to the released version." From 8e1ac73c1f0a3c5c21170cac7cdd21e1a0d8c972 Mon Sep 17 00:00:00 2001 From: gooze Date: Fri, 10 Jul 2026 20:18:24 +0200 Subject: [PATCH 03/10] fix: use a local dev tap for test-deploy.sh Modern Homebrew rejects installing a bare formula file that isn't part of a tap, so test-deploy.sh now maintains a small local tap (gnomad-dev/local) and writes the formula there instead. Co-Authored-By: Claude Sonnet 5 --- scripts/test-deploy.sh | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/scripts/test-deploy.sh b/scripts/test-deploy.sh index cb589de..3622bb7 100755 --- a/scripts/test-deploy.sh +++ b/scripts/test-deploy.sh @@ -17,6 +17,7 @@ set -euo pipefail REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" TAP="GooseRooster/gnomad" +DEV_TAP="gnomad-dev/local" for cmd in brew git; do if ! command -v "$cmd" &>/dev/null; then @@ -61,9 +62,23 @@ LOCAL_VERSION="$(grep -m1 '^version = ' "$REPO_ROOT/Cargo.toml" | sed -E 's/vers uninstall_if_present -FORMULA_DIR="$(mktemp -d)" -trap 'rm -rf "$FORMULA_DIR"' EXIT -FORMULA="$FORMULA_DIR/gnomad.rb" +# Homebrew (recent versions) refuses to install a bare formula file that +# isn't part of a tap, so we maintain a tiny local tap and drop the +# formula into it directly rather than re-tapping on every run. +DEV_TAP_DIR="$(brew --repository "$DEV_TAP")" +if [[ ! -d "$DEV_TAP_DIR" ]]; then + echo "==> Creating local dev tap ($DEV_TAP)" + SCAFFOLD="$(mktemp -d)" + mkdir -p "$SCAFFOLD/Formula" + git -C "$SCAFFOLD" init -q + git -C "$SCAFFOLD" add -A + git -C "$SCAFFOLD" -c user.name="gnomad-dev" -c user.email="dev@localhost" commit -q -m "init" --allow-empty + brew tap "$DEV_TAP" "file://$SCAFFOLD" + rm -rf "$SCAFFOLD" +fi + +FORMULA="$DEV_TAP_DIR/Formula/gnomad.rb" +mkdir -p "$(dirname "$FORMULA")" cat > "$FORMULA" < Installing gnomad from local checkout (branch: $BRANCH, commit: $COMMIT)" -brew install --build-from-source "$FORMULA" +brew install --build-from-source "$DEV_TAP/gnomad" echo echo "Done. $(gnomad --version)" From d67d443baa5928ca800581aecfae8b47aaa866cd Mon Sep 17 00:00:00 2001 From: gooze Date: Fri, 10 Jul 2026 20:19:36 +0200 Subject: [PATCH 04/10] fix: bake absolute cargo path into local dev formula Homebrew's build sandbox PATH doesn't include a rustup-managed cargo, so cargo install was failing with "command not found" inside the sandboxed build step. Co-Authored-By: Claude Sonnet 5 --- scripts/test-deploy.sh | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/scripts/test-deploy.sh b/scripts/test-deploy.sh index 3622bb7..6d9e34b 100755 --- a/scripts/test-deploy.sh +++ b/scripts/test-deploy.sh @@ -60,6 +60,13 @@ BRANCH="$(git -C "$REPO_ROOT" rev-parse --abbrev-ref HEAD)" COMMIT="$(git -C "$REPO_ROOT" rev-parse --short HEAD)" LOCAL_VERSION="$(grep -m1 '^version = ' "$REPO_ROOT/Cargo.toml" | sed -E 's/version = "(.*)"/\1/')-local" +# Homebrew's build sandbox uses a restricted PATH that doesn't include a +# rustup-managed cargo, so we bake in the absolute path to whatever cargo +# is already on PATH here rather than pulling in Homebrew's own "rust" +# formula as a build dependency (slow, and defeats the point of a fast +# local dev loop). +CARGO_BIN="$(command -v cargo)" + uninstall_if_present # Homebrew (recent versions) refuses to install a bare formula file that @@ -89,7 +96,7 @@ class Gnomad < Formula version "$LOCAL_VERSION" def install - system "cargo", "install", "--locked", "--root", prefix, "--path", "." + system "$CARGO_BIN", "install", "--locked", "--root", prefix, "--path", "." end test do From 194c31c83afd5c2d9c3460176fffc3979652fc09 Mon Sep 17 00:00:00 2001 From: gooze Date: Fri, 10 Jul 2026 20:20:37 +0200 Subject: [PATCH 05/10] fix: extend build sandbox PATH so cargo can find rustc cargo shells out to rustc by bare name; Homebrew's sandbox intercepts it via its own wrapper script which failed since rustc's directory wasn't on the restricted PATH. Co-Authored-By: Claude Sonnet 5 --- scripts/test-deploy.sh | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/scripts/test-deploy.sh b/scripts/test-deploy.sh index 6d9e34b..235cb4c 100755 --- a/scripts/test-deploy.sh +++ b/scripts/test-deploy.sh @@ -61,11 +61,13 @@ COMMIT="$(git -C "$REPO_ROOT" rev-parse --short HEAD)" LOCAL_VERSION="$(grep -m1 '^version = ' "$REPO_ROOT/Cargo.toml" | sed -E 's/version = "(.*)"/\1/')-local" # Homebrew's build sandbox uses a restricted PATH that doesn't include a -# rustup-managed cargo, so we bake in the absolute path to whatever cargo -# is already on PATH here rather than pulling in Homebrew's own "rust" -# formula as a build dependency (slow, and defeats the point of a fast -# local dev loop). +# rustup-managed cargo (and cargo in turn shells out to `rustc` by bare +# name), so we bake in the absolute path to whatever cargo is already on +# PATH here and prepend its directory to the sandbox PATH, rather than +# pulling in Homebrew's own "rust" formula as a build dependency (slow, +# and defeats the point of a fast local dev loop). CARGO_BIN="$(command -v cargo)" +CARGO_BIN_DIR="$(dirname "$CARGO_BIN")" uninstall_if_present @@ -96,6 +98,7 @@ class Gnomad < Formula version "$LOCAL_VERSION" def install + ENV.prepend_path "PATH", "$CARGO_BIN_DIR" system "$CARGO_BIN", "install", "--locked", "--root", prefix, "--path", "." end From 30071526878821ced4670ead1c0f338e12c55c5b Mon Sep 17 00:00:00 2001 From: gooze Date: Fri, 10 Jul 2026 20:22:10 +0200 Subject: [PATCH 06/10] fix: use Homebrew's own rust build dependency, not rustup The externally-managed rustup cargo can't cleanly drive Homebrew's sandboxed cc/linker shim (missing default libs at link time). Homebrew's own "rust" formula is built to work inside that sandbox, so depend on it as a build dependency instead of threading through the host cargo. Co-Authored-By: Claude Sonnet 5 --- scripts/test-deploy.sh | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/scripts/test-deploy.sh b/scripts/test-deploy.sh index 235cb4c..9471c2c 100755 --- a/scripts/test-deploy.sh +++ b/scripts/test-deploy.sh @@ -60,15 +60,6 @@ BRANCH="$(git -C "$REPO_ROOT" rev-parse --abbrev-ref HEAD)" COMMIT="$(git -C "$REPO_ROOT" rev-parse --short HEAD)" LOCAL_VERSION="$(grep -m1 '^version = ' "$REPO_ROOT/Cargo.toml" | sed -E 's/version = "(.*)"/\1/')-local" -# Homebrew's build sandbox uses a restricted PATH that doesn't include a -# rustup-managed cargo (and cargo in turn shells out to `rustc` by bare -# name), so we bake in the absolute path to whatever cargo is already on -# PATH here and prepend its directory to the sandbox PATH, rather than -# pulling in Homebrew's own "rust" formula as a build dependency (slow, -# and defeats the point of a fast local dev loop). -CARGO_BIN="$(command -v cargo)" -CARGO_BIN_DIR="$(dirname "$CARGO_BIN")" - uninstall_if_present # Homebrew (recent versions) refuses to install a bare formula file that @@ -97,9 +88,10 @@ class Gnomad < Formula url "file://$REPO_ROOT", using: :git, branch: "$BRANCH" version "$LOCAL_VERSION" + depends_on "rust" => :build + def install - ENV.prepend_path "PATH", "$CARGO_BIN_DIR" - system "$CARGO_BIN", "install", "--locked", "--root", prefix, "--path", "." + system "cargo", "install", "--locked", "--root", prefix, "--path", "." end test do From be014d8473035daa113216004be81e868db750b9 Mon Sep 17 00:00:00 2001 From: gooze Date: Fri, 10 Jul 2026 20:23:54 +0200 Subject: [PATCH 07/10] fix: drop non-existent --version flag from test-deploy.sh and formula test gnomad's CLI has no --version flag (only --help, --apply, --update-schemes, -v). Use --help for the formula's test block and print the resolved binary path instead of a fabricated version string. Co-Authored-By: Claude Sonnet 5 --- scripts/test-deploy.sh | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/scripts/test-deploy.sh b/scripts/test-deploy.sh index 9471c2c..505219a 100755 --- a/scripts/test-deploy.sh +++ b/scripts/test-deploy.sh @@ -40,7 +40,7 @@ if [[ "${1:-}" == "--revert" ]]; then echo "==> Installing latest gnomad from $TAP" brew install "$TAP/gnomad" echo - echo "Done. $(gnomad --version)" + echo "Done. Installed at: $(command -v gnomad)" exit 0 fi @@ -95,7 +95,7 @@ class Gnomad < Formula end test do - system "#{bin}/gnomad", "--version" + system "#{bin}/gnomad", "--help" end end EOF @@ -104,5 +104,6 @@ echo "==> Installing gnomad from local checkout (branch: $BRANCH, commit: $COMMI brew install --build-from-source "$DEV_TAP/gnomad" echo -echo "Done. $(gnomad --version)" -echo "This is a local dev build — run '$(basename "$0") --revert' to go back to the released version." +echo "Done. Installed at: $(command -v gnomad)" +echo "This is a local dev build (branch: $BRANCH, commit: $COMMIT)." +echo "Run '$(basename "$0") --revert' to go back to the released version." From 05526710d783013f5ead6c365802b0c1058ef111 Mon Sep 17 00:00:00 2001 From: gooze Date: Fri, 10 Jul 2026 20:46:27 +0200 Subject: [PATCH 08/10] feat: add --version flag to CLI Co-Authored-By: Claude Sonnet 5 --- src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 66967f5..c6c08c5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,7 +15,7 @@ use std::path::PathBuf; use tracing_subscriber::EnvFilter; #[derive(Parser)] -#[command(name = "gnomad", about = "GNOME colour scheme and wallpaper manager")] +#[command(name = "gnomad", about = "GNOME colour scheme and wallpaper manager", version)] struct Cli { #[arg(long, help = "Fetch/update schemes and exit")] update_schemes: bool, From db221b6de65bc9669fa5b29d4f13aac96beb343e Mon Sep 17 00:00:00 2001 From: gooze Date: Fri, 10 Jul 2026 20:46:27 +0200 Subject: [PATCH 09/10] chore: bump version to 0.4.0 Co-Authored-By: Claude Sonnet 5 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2253f2e..993bf38 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -672,7 +672,7 @@ dependencies = [ [[package]] name = "gnomad" -version = "0.3.7" +version = "0.4.0" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index d8cc4a2..5217c6c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "gnomad" -version = "0.3.7" +version = "0.4.0" edition = "2021" description = "A lightweight TUI for managing tinted color schemes in the GNOME shell" readme = "README.md" From 47489e2eceeb2e51d78d99aa348af752b6753f84 Mon Sep 17 00:00:00 2001 From: gooze Date: Fri, 10 Jul 2026 21:01:45 +0200 Subject: [PATCH 10/10] Fix Custom Schemes and add Hook support Fixed issues with custom schemes not applying correctly. Added support for hooks on gnomad events. --- README.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 6934d62..370f9bf 100644 --- a/README.md +++ b/README.md @@ -36,16 +36,17 @@ https://youtu.be/VyY0kjDfrCM - **Custom schemes** — drop your own YAML files into a configured directory and they appear alongside the catalogue - **Dark/light preference** — optionally filter schemes to match your GNOME colour scheme setting (prefer-dark / prefer-light) - **Adwaita for Steam** — optional integration with [Adwaita-for-Steam](https://github.com/tkashkin/Adwaita-for-Steam); writes scheme colours to its custom CSS override on every scheme switch, keeping Steam's UI in sync with your desktop +- **Hooks** — Support for custom hooks in config allowing you to run your own shell commands whenever gnomad changes the color scheme, wallpaper, or batch converts a directory. --- ## Requirements - GNOME 45+ on Wayland -- `rustup` installed from your distribution's package manager -- `tinty` in `$PATH` — `cargo install tinty` +- `rustup` installed from your distribution's package manager, or Homebrew +- `tinty` in `$PATH` — `cargo install tinty` or `brew install tinty` with the tinted-theming brew tap configured - `git` in `$PATH` -- `gowall` in `$PATH` — [installation](https://github.com/Achno/gowall#installation) *(optional — only required when `wallpaper_enabled = true`, which is the default)* +- `gowall` in `$PATH` — [installation](https://github.com/Achno/gowall#installation) *(optional — only required when `wallpaper_enabled = true`, which is the default. Also supports installation on linux via homebrew)* - A terminal with [Sixel](https://en.wikipedia.org/wiki/Sixel) or [Kitty Graphics Protocol](https://sw.kovidgoyal.net/kitty/graphics-protocol/) support for wallpaper preview (e.g. Kitty, foot, WezTerm, Ghostty) *(optional — wallpaper panel only)* --- @@ -113,9 +114,9 @@ tinty sync See the [Tinty documentation](https://github.com/tinted-theming/tinty) for details. -For **custom schemes** (from `custom_schemes_dir`), gnomad automatically writes a copy into tinty's own custom-schemes directory (`~/.local/share/tinted-theming/tinty/custom-schemes//.yaml`) immediately before every `tinty apply` call — no manual registration or `tinty sync` needed. This file is regenerated on every apply, so edits to your source YAML in `custom_schemes_dir` take effect on the next scheme switch. +For **custom schemes** (from `custom_schemes_dir`), gnomad automatically writes a copy into tinty's own custom-schemes directory (`~/.local/share/tinted-theming/tinty/custom-schemes//.yaml`) immediately before every `tinty apply` call, no manual registration or `tinty sync` needed. This file is regenerated on every apply, so edits to your source YAML in `custom_schemes_dir` take effect on the next scheme switch, effectively allowing you to manage your schemes entirely from gnomad. -gnomad will warn on startup if User Themes is not detected or if Tinty/gowall are missing from `$PATH`. +gnomad will warn on startup if dependencies are missing. ### 4. Adwaita for Steam (optional) @@ -187,7 +188,7 @@ Configure them under a `[hooks]` table in `config.toml`: on_wallpaper_batch_convert = "/usr/bin/gsettings set org.gnome.shell.extensions.wallpaper-slideshow.picture-uri-list-source-directory \"file://$GNOMAD_CACHE_DIR\"" ``` -> Use full paths like `/usr/bin/gsettings` rather than bare `gsettings` — avoids PATH issues with tools like Homebrew shadowing system binaries, and mirrors gnomad's own internal convention (see `gsettings_set`/`gsettings_get` in `src/pipeline/gnome.rs`). +> Use full paths like `/usr/bin/gsettings` rather than bare `gsettings`. This avoids PATH issues with tools like Homebrew shadowing system binaries, and mirrors gnomad's own internal convention (see `gsettings_set`/`gsettings_get` in `src/pipeline/gnome.rs`). | Key | Fires | Env vars available | |---|---|---|