Merge pull request 'appearance: per-monitor wallpaper + a shell-theme picker' (#5) from feature/per-monitor-wallpaper-shell-theme into main
This commit is contained in:
commit
418068d186
10 changed files with 508 additions and 15 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts">
|
||||
import Hub from "$lib/components/Hub.svelte";
|
||||
import Breadpaper from "./Breadpaper.svelte";
|
||||
import ShellTheme from "./ShellTheme.svelte";
|
||||
import Appearance from "./Appearance.svelte";
|
||||
</script>
|
||||
|
||||
|
|
@ -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 },
|
||||
]}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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<LiveMonitor[]>([]);
|
||||
let byOutput = $state<Record<string, string>>({});
|
||||
let globalPath = $state<string | null>(null);
|
||||
let selected = $state<string | null>(null);
|
||||
let sameOnAll = $state(true);
|
||||
|
||||
let currentPath = $state<string | null>(null);
|
||||
let status = $state("");
|
||||
let libraryDir = $state("");
|
||||
let library = $state<LibraryEntry[] | null>(null);
|
||||
let scanning = $state(false);
|
||||
|
||||
async function refreshCurrent() {
|
||||
currentPath = await invoke<string | null>("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<string | null>("get_current_wallpaper");
|
||||
const list = await invoke<OutputWallpaper[]>("get_wallpapers_by_output");
|
||||
byOutput = Object.fromEntries(list.map((o) => [o.output, o.path]));
|
||||
monitors = await invoke<LiveMonitor[]>("get_live_monitors");
|
||||
if (!selected || !monitors.some((m) => m.name === selected)) {
|
||||
selected = monitors[0]?.name ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
libraryDir = await invoke<string>("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<LibraryEntry[]>("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) : "";
|
||||
}
|
||||
</script>
|
||||
|
||||
<ViewScaffold title="Wallpaper">
|
||||
<Group title="Wallpaper" hint="This also updates desktop colors.">
|
||||
{#if currentPath}
|
||||
<div class="hero" style="background-image: url('{convertFileSrc(currentPath)}')">
|
||||
<span class="cap">{currentPath.split("/").pop()}</span>
|
||||
<Group title="Wallpaper" hint="This also sets the desktop colors — each monitor's accent comes from its own wallpaper.">
|
||||
{#if perMonitor}
|
||||
<SwitchField label="Use the same wallpaper on every monitor" bind:value={sameOnAll} />
|
||||
{/if}
|
||||
|
||||
{#if perMonitor && !sameOnAll}
|
||||
<div class="mons">
|
||||
{#each monitors as m (m.name)}
|
||||
<button
|
||||
type="button"
|
||||
class="mon"
|
||||
class:on={selected === m.name}
|
||||
title={m.mode}
|
||||
onclick={() => (selected = m.name)}
|
||||
>
|
||||
{m.name}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if activePath}
|
||||
<div class="hero" style="background-image: url('{convertFileSrc(activePath)}')">
|
||||
<span class="cap">
|
||||
{#if perMonitor && !sameOnAll}{selected} · {/if}{shortName(activePath)}
|
||||
</span>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="hero placeholder">No wallpaper</div>
|
||||
|
|
@ -78,7 +140,7 @@
|
|||
{#each library as item (item.path)}
|
||||
<button
|
||||
class="thumb"
|
||||
class:on={item.path === currentPath}
|
||||
class:on={item.path === activePath}
|
||||
onclick={() => apply(item.path)}
|
||||
title={item.path}
|
||||
>
|
||||
|
|
@ -91,6 +153,30 @@
|
|||
</ViewScaffold>
|
||||
|
||||
<style>
|
||||
.mons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin: 4px 0 12px;
|
||||
}
|
||||
|
||||
.mon {
|
||||
padding: 6px 11px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
background: color-mix(in srgb, var(--fg) 6%, transparent);
|
||||
border: 1px solid var(--line, color-mix(in srgb, var(--fg) 8%, transparent));
|
||||
color: var(--fg);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mon.on {
|
||||
background: var(--accent);
|
||||
color: var(--on-accent);
|
||||
border-color: transparent;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.hero {
|
||||
height: 180px;
|
||||
border-radius: 12px;
|
||||
|
|
|
|||
187
frontend/src/lib/views/ShellTheme.svelte
Normal file
187
frontend/src/lib/views/ShellTheme.svelte
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import ViewScaffold from "$lib/components/ViewScaffold.svelte";
|
||||
import Group from "$lib/components/Group.svelte";
|
||||
import Hint from "$lib/components/Hint.svelte";
|
||||
|
||||
interface ShellThemeInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
source: string; // "builtin" | "system" | "user"
|
||||
}
|
||||
|
||||
let themes = $state<ShellThemeInfo[]>([]);
|
||||
let active = $state<string>("");
|
||||
let loaded = $state(false);
|
||||
let busy = $state(false);
|
||||
let status = $state("");
|
||||
// Set once the user switches, so the restart prompt gains emphasis.
|
||||
// breadbar/breadbox don't hot-reload a shell-theme change today — it
|
||||
// takes effect when they restart or on the next login.
|
||||
let switched = $state(false);
|
||||
|
||||
onMount(async () => {
|
||||
[themes, active] = await Promise.all([
|
||||
invoke<ShellThemeInfo[]>("list_shell_themes"),
|
||||
invoke<string>("get_active_shell_theme"),
|
||||
]);
|
||||
loaded = true;
|
||||
});
|
||||
|
||||
async function select(id: string) {
|
||||
if (id === active || busy) return;
|
||||
const prev = active;
|
||||
active = id;
|
||||
busy = true;
|
||||
status = "Applying…";
|
||||
try {
|
||||
await invoke("set_active_shell_theme", { id });
|
||||
switched = true;
|
||||
status = "Applied";
|
||||
setTimeout(() => (status = ""), 3000);
|
||||
} catch (e) {
|
||||
active = prev;
|
||||
status = `${e}`;
|
||||
} finally {
|
||||
busy = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function restart() {
|
||||
busy = true;
|
||||
status = "Restarting…";
|
||||
try {
|
||||
await invoke("restart_shell_apps");
|
||||
switched = false;
|
||||
status = "Restarted";
|
||||
setTimeout(() => (status = ""), 3000);
|
||||
} catch (e) {
|
||||
status = `${e}`;
|
||||
} finally {
|
||||
busy = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<ViewScaffold
|
||||
title="Shell theme"
|
||||
lede="The layout and motion style for the bar and launcher. Colors come from the wallpaper."
|
||||
>
|
||||
<Group title="Theme" wide>
|
||||
{#if !loaded}
|
||||
<Hint text="Loading…" />
|
||||
{:else}
|
||||
<div class="cards">
|
||||
{#each themes as t (t.id)}
|
||||
<button
|
||||
type="button"
|
||||
class="card"
|
||||
class:on={active === t.id}
|
||||
disabled={busy}
|
||||
onclick={() => select(t.id)}
|
||||
>
|
||||
<span class="dot" aria-hidden="true"></span>
|
||||
<span class="meta">
|
||||
<span class="name">{t.name}</span>
|
||||
<span class="sub">
|
||||
<span class="id">{t.id}</span>{#if t.source !== "builtin"} · {t.source}{/if}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="foot">
|
||||
<span class="status">{status}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</Group>
|
||||
|
||||
{#if loaded}
|
||||
<Group title="Apply">
|
||||
<Hint
|
||||
text={switched
|
||||
? "The bar restarts itself to pick up the new theme; the launcher applies it next time you open it."
|
||||
: "The bar restarts itself on a theme change; the launcher applies it on its next open. Use the button below if something looks stuck."}
|
||||
/>
|
||||
<div class="foot">
|
||||
<button class="btn" disabled={busy} onclick={restart}>Restart bar & launcher</button>
|
||||
</div>
|
||||
</Group>
|
||||
{/if}
|
||||
</ViewScaffold>
|
||||
|
||||
<style>
|
||||
.cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 10px;
|
||||
text-align: left;
|
||||
background: var(--bg);
|
||||
border: 2px solid transparent;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.card:disabled {
|
||||
cursor: default;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.card.on {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 999px;
|
||||
flex-shrink: 0;
|
||||
background: color-mix(in srgb, var(--fg) 25%, transparent);
|
||||
}
|
||||
|
||||
.card.on .dot {
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
.meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.sub {
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.sub .id {
|
||||
text-transform: none;
|
||||
font-family: var(--font-mono, ui-monospace, monospace);
|
||||
}
|
||||
|
||||
.foot {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.status {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
7
src/Cargo.lock
generated
7
src/Cargo.lock
generated
|
|
@ -307,12 +307,15 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "bread-theme"
|
||||
version = "0.7.4"
|
||||
source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.4#fcba3760387e2523edb71350f8efea3bc851b21e"
|
||||
version = "0.7.5"
|
||||
source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.5#46b886b7bc168e73945269e143b18ef8a0d987f8"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"dirs 5.0.1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"toml 0.8.23",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ toml_edit = "0.22"
|
|||
tokio = { version = "1", features = ["process", "io-util", "time", "macros"] }
|
||||
notify = "7"
|
||||
regex = "1"
|
||||
bread-theme = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.4" }
|
||||
bread-theme = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.5" }
|
||||
bread-utils = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.2", features = ["toml"] }
|
||||
anyhow = "1"
|
||||
|
||||
|
|
|
|||
|
|
@ -84,6 +84,64 @@ pub async fn set_wallpaper(path: String) -> Result<(), String> {
|
|||
}
|
||||
}
|
||||
|
||||
/// One compositor output's persisted wallpaper, from breadpaper's own
|
||||
/// `~/.config/breadpaper/current.json` (`{ "outputs": { "<name>": "<path>" } }`).
|
||||
#[derive(Serialize)]
|
||||
pub struct OutputWallpaper {
|
||||
output: String,
|
||||
path: String,
|
||||
}
|
||||
|
||||
fn current_json_path() -> PathBuf {
|
||||
super::config::config_dir().join("breadpaper/current.json")
|
||||
}
|
||||
|
||||
/// The wallpaper breadpaper last set on each output. Read straight from
|
||||
/// `current.json` rather than shelling `breadpaper --output <n> get` once
|
||||
/// per monitor — it's the same file breadpaper's `get_on` reads, and a
|
||||
/// settings window can have several monitors to report.
|
||||
#[tauri::command]
|
||||
pub fn get_wallpapers_by_output() -> Vec<OutputWallpaper> {
|
||||
let Ok(text) = std::fs::read_to_string(current_json_path()) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let Ok(value) = serde_json::from_str::<serde_json::Value>(&text) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let Some(outputs) = value.get("outputs").and_then(|v| v.as_object()) else {
|
||||
return Vec::new();
|
||||
};
|
||||
outputs
|
||||
.iter()
|
||||
.filter_map(|(output, path)| {
|
||||
Some(OutputWallpaper {
|
||||
output: output.clone(),
|
||||
path: path.as_str()?.to_string(),
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Set the wallpaper (and regenerate that output's pywal palette) on a
|
||||
/// single compositor output. `breadpaper --output <NAME> set <path>`
|
||||
/// leaves every other output's wallpaper untouched (see breadpaper's
|
||||
/// `current.json` `set_one_output_does_not_drop_others` test).
|
||||
#[tauri::command]
|
||||
pub async fn set_wallpaper_on(output: String, path: String) -> Result<(), String> {
|
||||
let ok = Command::new("breadpaper")
|
||||
.args(["--output", &output, "set"])
|
||||
.arg(&path)
|
||||
.status()
|
||||
.await
|
||||
.map(|s| s.success())
|
||||
.unwrap_or(false);
|
||||
if ok {
|
||||
Ok(())
|
||||
} else {
|
||||
Err("breadpaper failed — see terminal/journal".into())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct LibraryEntry {
|
||||
path: String,
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ pub mod packages;
|
|||
pub mod power;
|
||||
pub mod printing;
|
||||
pub mod service;
|
||||
pub mod shell_theme;
|
||||
pub mod snapshots;
|
||||
pub mod sound;
|
||||
pub mod streaming;
|
||||
|
|
|
|||
144
src/src/commands/shell_theme.rs
Normal file
144
src/src/commands/shell_theme.rs
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
//! `~/.config/bread/shell.toml` — the `active = "<id>"` key selects the
|
||||
//! **shell theme**: breadbar/breadbox chrome, layout, and animation style
|
||||
//! (`liquid-motion`, `glass-workbench`, `spotlight`, `daylight`). This is a
|
||||
//! single global selector; per-monitor differences come from the wallpaper
|
||||
//! and its pywal palette, not from the shell theme.
|
||||
//!
|
||||
//! `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 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;
|
||||
|
||||
use super::config;
|
||||
|
||||
fn shell_toml_path() -> std::path::PathBuf {
|
||||
config::config_dir().join("bread/shell.toml")
|
||||
}
|
||||
|
||||
/// Default when `shell.toml` is missing or has no `active` — matches
|
||||
/// `bread_theme::shell`'s own hardcoded fallback.
|
||||
const DEFAULT_THEME_ID: &str = "liquid-motion";
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ShellThemeInfo {
|
||||
id: String,
|
||||
name: String,
|
||||
/// `"builtin"`, `"system"`, or `"user"` — for a source badge in the picker.
|
||||
source: String,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn list_shell_themes() -> Vec<ShellThemeInfo> {
|
||||
bread_theme::shell::list()
|
||||
.into_iter()
|
||||
.map(|t| ShellThemeInfo {
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
source: match t.source {
|
||||
bread_theme::shell::ThemeSource::User => "user",
|
||||
bread_theme::shell::ThemeSource::System => "system",
|
||||
bread_theme::shell::ThemeSource::Builtin => "builtin",
|
||||
}
|
||||
.to_string(),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_active_shell_theme() -> String {
|
||||
let doc = config::load_doc(&shell_toml_path());
|
||||
doc.get("active")
|
||||
.and_then(|i| i.as_str())
|
||||
.map(str::trim)
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(str::to_string)
|
||||
.unwrap_or_else(|| DEFAULT_THEME_ID.to_string())
|
||||
}
|
||||
|
||||
/// Writes `active = "<id>"` into `shell.toml`, preserving any other keys and
|
||||
/// comments. Rejects an id that isn't a discoverable theme — a typo would
|
||||
/// silently fall breadbar/breadbox back to the builtin with no error.
|
||||
#[tauri::command]
|
||||
pub fn set_active_shell_theme(id: String) -> Result<(), String> {
|
||||
if !bread_theme::shell::list().iter().any(|t| t.id == id) {
|
||||
return Err(format!("no shell theme with id '{id}'"));
|
||||
}
|
||||
let path = shell_toml_path();
|
||||
let mut doc = config::load_doc(&path);
|
||||
doc["active"] = toml_edit::value(id);
|
||||
config::save_doc(&path, &doc).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Restart breadbar and breadbox so a shell-theme change takes full effect:
|
||||
/// breadbar's live watch handles colours/tokens but not window geometry
|
||||
/// (anchors, exclusive zone), and breadbox has no watch at all. Detached so
|
||||
/// the new processes outlive this settings process. Best-effort — a missing
|
||||
/// binary or a compositor without them running is not an error.
|
||||
#[tauri::command]
|
||||
pub fn restart_shell_apps() -> Result<(), String> {
|
||||
for app in ["breadbar", "breadbox"] {
|
||||
let _ = std::process::Command::new("pkill").args(["-x", app]).status();
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(250));
|
||||
spawn_detached("breadbar", &[]);
|
||||
// Matches `~/.config/hypr/autostart.json` — breadbox runs as a command
|
||||
// bus (`breadbox listen`); the launcher window is spawned on demand.
|
||||
spawn_detached("breadbox", &["listen"]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn spawn_detached(prog: &str, args: &[&str]) {
|
||||
use std::process::{Command, Stdio};
|
||||
// `setsid -f` forks the target into its own session and exits at once;
|
||||
// the target is reparented to init, so it never becomes a zombie of
|
||||
// this settings process. `.status()` reaps `setsid` itself (it returns
|
||||
// immediately) so that doesn't linger either — a plain `.spawn()` here
|
||||
// leaves a `<defunct>` entry behind for every restart.
|
||||
let _ = Command::new("setsid")
|
||||
.arg("-f")
|
||||
.arg(prog)
|
||||
.args(args)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.status();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::io::Write;
|
||||
|
||||
/// `set`-style write must not drop sibling keys or comments in shell.toml.
|
||||
#[test]
|
||||
fn writing_active_preserves_other_content() {
|
||||
let dir = std::env::temp_dir().join(format!("bos-shelltoml-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let path = dir.join("shell.toml");
|
||||
let mut f = std::fs::File::create(&path).unwrap();
|
||||
write!(
|
||||
f,
|
||||
"# my shell config\nactive = \"spotlight\"\n\n[experimental]\nfoo = true\n"
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut doc = config::load_doc(&path);
|
||||
doc["active"] = toml_edit::value("daylight");
|
||||
config::save_doc(&path, &doc).unwrap();
|
||||
|
||||
let out = std::fs::read_to_string(&path).unwrap();
|
||||
assert!(out.contains("active = \"daylight\""));
|
||||
assert!(out.contains("# my shell config"));
|
||||
assert!(out.contains("[experimental]"));
|
||||
assert!(out.contains("foo = true"));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
}
|
||||
|
|
@ -41,8 +41,14 @@ pub fn run() {
|
|||
commands::breadcrumbs::save_breadcrumbs_config,
|
||||
commands::breadpaper::get_current_wallpaper,
|
||||
commands::breadpaper::set_wallpaper,
|
||||
commands::breadpaper::get_wallpapers_by_output,
|
||||
commands::breadpaper::set_wallpaper_on,
|
||||
commands::breadpaper::list_wallpaper_library,
|
||||
commands::breadpaper::wallpaper_library_dir_display,
|
||||
commands::shell_theme::list_shell_themes,
|
||||
commands::shell_theme::get_active_shell_theme,
|
||||
commands::shell_theme::set_active_shell_theme,
|
||||
commands::shell_theme::restart_shell_apps,
|
||||
commands::appearance::get_appearance,
|
||||
commands::appearance::save_appearance,
|
||||
commands::autostart::get_autostart_entries,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue