From bfb78fd6f54b3feb7e5c6a7b1bae309912ea1be8 Mon Sep 17 00:00:00 2001 From: Breadway Date: Wed, 2 Sep 2026 09:31:20 +0800 Subject: [PATCH 1/2] appearance: per-monitor wallpaper + a shell-theme picker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wallpaper (Breadpaper.svelte) becomes monitor-aware: with more than one output connected it shows a 'use the same wallpaper on every monitor' switch; turning it off reveals per-output chips and routes 'Choose image' / library picks through 'breadpaper --output set', which regenerates only that monitor's pywal palette. Single-monitor behaviour is unchanged. Backend: get_wallpapers_by_output (reads breadpaper's current.json) + set_wallpaper_on. New 'Shell theme' tab: lists bread_theme::shell::list() (liquid-motion, glass-workbench, spotlight, daylight) and writes ~/.config/bread/shell.toml 'active =' non-destructively via toml_edit. This is a single global selector — per-monitor differences come from the wallpaper/palette, not the shell theme. breadbar/breadbox don't hot-reload the change today, so the tab is explicit that it applies on restart/login and offers a 'Restart bar & launcher' button (setsid -f, so it leaves no zombies). bread-theme bumped v0.7.4 -> v0.7.5 for shell::list(). --- frontend/src/lib/search.ts | 8 +- frontend/src/lib/views/AppearanceHub.svelte | 2 + frontend/src/lib/views/Breadpaper.svelte | 108 +++++++++-- frontend/src/lib/views/ShellTheme.svelte | 189 ++++++++++++++++++++ src/Cargo.lock | 7 +- src/Cargo.toml | 2 +- src/src/commands/breadpaper.rs | 58 ++++++ src/src/commands/mod.rs | 1 + src/src/commands/shell_theme.rs | 142 +++++++++++++++ src/src/lib.rs | 6 + 10 files changed, 508 insertions(+), 15 deletions(-) create mode 100644 frontend/src/lib/views/ShellTheme.svelte create mode 100644 src/src/commands/shell_theme.rs diff --git a/frontend/src/lib/search.ts b/frontend/src/lib/search.ts index 2b5f4e6..ab1545f 100644 --- a/frontend/src/lib/search.ts +++ b/frontend/src/lib/search.ts @@ -45,7 +45,13 @@ export const SEARCH_INDEX: SearchHit[] = [ { page: "appearance", label: "Wallpaper", - keywords: "wallpaper theme palette pywal breadpaper accent", + keywords: "wallpaper theme palette pywal breadpaper accent per monitor multi monitor background", + }, + { + page: "appearance", + tab: "shelltheme", + label: "Shell theme", + keywords: "shell theme breadbar breadbox bar launcher style liquid motion glass workbench spotlight daylight layout animation", }, { page: "appearance", diff --git a/frontend/src/lib/views/AppearanceHub.svelte b/frontend/src/lib/views/AppearanceHub.svelte index 692f3ea..e8e2e1f 100644 --- a/frontend/src/lib/views/AppearanceHub.svelte +++ b/frontend/src/lib/views/AppearanceHub.svelte @@ -1,6 +1,7 @@ @@ -9,6 +10,7 @@ lede="Wallpaper sets the colors. Windows follow." tabs={[ { id: "breadpaper", label: "Wallpaper", component: Breadpaper }, + { id: "shelltheme", label: "Shell theme", component: ShellTheme }, { id: "appearance", label: "Windows", component: Appearance }, ]} /> diff --git a/frontend/src/lib/views/Breadpaper.svelte b/frontend/src/lib/views/Breadpaper.svelte index d94cd39..b4abb31 100644 --- a/frontend/src/lib/views/Breadpaper.svelte +++ b/frontend/src/lib/views/Breadpaper.svelte @@ -4,25 +4,57 @@ import { open } from "@tauri-apps/plugin-dialog"; import ViewScaffold from "$lib/components/ViewScaffold.svelte"; import Group from "$lib/components/Group.svelte"; + import SwitchField from "$lib/components/SwitchField.svelte"; interface LibraryEntry { path: string; name: string; } + interface LiveMonitor { + name: string; + mode: string; + } + interface OutputWallpaper { + output: string; + path: string; + } + + let monitors = $state([]); + let byOutput = $state>({}); + let globalPath = $state(null); + let selected = $state(null); + let sameOnAll = $state(true); - let currentPath = $state(null); let status = $state(""); let libraryDir = $state(""); let library = $state(null); let scanning = $state(false); - async function refreshCurrent() { - currentPath = await invoke("get_current_wallpaper"); + // The wallpaper the current selection resolves to: the global one when + // "same on every monitor" is on, otherwise the selected monitor's own + // (falling back to the global if that monitor was never set explicitly). + let activePath = $derived( + sameOnAll ? globalPath : (selected && byOutput[selected]) || globalPath, + ); + + const perMonitor = $derived(monitors.length > 1); + + async function refresh() { + globalPath = await invoke("get_current_wallpaper"); + const list = await invoke("get_wallpapers_by_output"); + byOutput = Object.fromEntries(list.map((o) => [o.output, o.path])); + monitors = await invoke("get_live_monitors"); + if (!selected || !monitors.some((m) => m.name === selected)) { + selected = monitors[0]?.name ?? null; + } } onMount(async () => { libraryDir = await invoke("wallpaper_library_dir_display"); - await refreshCurrent(); + await refresh(); + // Start in per-monitor mode only if the monitors genuinely disagree. + const distinct = new Set(Object.values(byOutput)); + sameOnAll = distinct.size <= 1; scanning = true; try { library = await invoke("list_wallpaper_library"); @@ -34,8 +66,12 @@ async function apply(path: string) { status = "Setting…"; try { - await invoke("set_wallpaper", { path }); - await refreshCurrent(); + if (sameOnAll || !selected) { + await invoke("set_wallpaper", { path }); + } else { + await invoke("set_wallpaper_on", { output: selected, path }); + } + await refresh(); status = "Set"; } catch (e) { status = `${e}`; @@ -51,13 +87,39 @@ }); if (typeof path === "string") await apply(path); } + + function shortName(p: string | null): string { + return p ? (p.split("/").pop() ?? p) : ""; + } - - {#if currentPath} -
- {currentPath.split("/").pop()} + + {#if perMonitor} + + {/if} + + {#if perMonitor && !sameOnAll} +
+ {#each monitors as m (m.name)} + + {/each} +
+ {/if} + + {#if activePath} +
+ + {#if perMonitor && !sameOnAll}{selected} · {/if}{shortName(activePath)} +
{:else}
No wallpaper
@@ -78,7 +140,7 @@ {#each library as item (item.path)} +
{/if} diff --git a/src/src/commands/shell_theme.rs b/src/src/commands/shell_theme.rs index aaf3e8a..94267de 100644 --- a/src/src/commands/shell_theme.rs +++ b/src/src/commands/shell_theme.rs @@ -6,12 +6,14 @@ //! //! `bread_theme::shell::list()` enumerates every discoverable theme (user //! config → system dir → compiled-in builtins). Setting `active` is a -//! non-destructive `toml_edit` write; a running breadbar picks the change -//! up live via its own `bread_theme::shell::watch()` (colours and CSS -//! tokens re-resolve without a restart). Two things it *cannot* hot-apply: -//! window-spec changes (a bottom-anchored theme like `daylight` moves the -//! bar), and breadbox — which reads `shell::load()` once at startup and has -//! no watch. `restart_shell_apps` covers both. +//! non-destructive `toml_edit` write. +//! +//! A running breadbar watches `shell.toml` (`bread_theme::shell::watch()`) +//! and, on an `active` change, re-execs itself so the full theme applies — +//! geometry and widget structure, not just CSS tokens. breadbox re-reads +//! the theme on each launcher open. So a plain write here is normally +//! enough; `restart_shell_apps` stays as a manual fallback (an old breadbar +//! without self-restart, or a failed re-exec). use serde::Serialize;