diff --git a/README.md b/README.md index 132961b..38a065a 100644 --- a/README.md +++ b/README.md @@ -58,10 +58,10 @@ spawn-sh-at-startup "niri-scripts/support-sticky-floating" binds { // To take a screenshot, use whatever keybind you want - Super+S { spawn-sh "niri-scripts/screeenshot "; } + Super+S { spawn-sh "niri-scripts/screenshot "; } // To take a screenshot then annotate it, use whatever keybind you want - Super+Ctrl+S { spawn-sh "niri-scripts/screeenshot --annotate"; } + Super+Ctrl+S { spawn-sh "niri-scripts/screenshot --annotate"; } // To make the focused window sticky, use whatever keybind you want Super+Shift+S { spawn-sh "niri-scripts/toggle-sticky"; } diff --git a/auto-consume-new-windows b/auto-consume-new-windows new file mode 100755 index 0000000..926e4fb --- /dev/null +++ b/auto-consume-new-windows @@ -0,0 +1,246 @@ +#!/usr/bin/env scriptisto +// vim: ft=rust shiftwidth=2 softtabstop=2 + +// TASK: +// #region meta +// scriptisto-begin +// script_src: src/main.rs +// build_cmd: > +// cargo clippy --color=always && +// cargo build --release --color=always && strip ./target/release/niri_auto_consume +// target_bin: ./target/release/niri_auto_consume +// files: +// - path: Cargo.toml +// content: | +// package = { name = "niri_auto_consume", version = "0.1.0", edition = "2024"} +// [dependencies] +// serde = { version = "1.0.219", features = [ "derive" ] } +// serde_json = "1.0.140" +// scriptisto-end +// #endregion + +#![deny(clippy::unwrap_used)] + +use std::collections::HashSet; +use std::io::{ + BufRead, + BufReader, + Write, +}; +use std::os::unix::net::{UnixListener, UnixStream}; +use std::process::{ + Command, + Stdio, +}; +use std::sync::{ + LazyLock, + RwLock, +}; + +type Result = std::result::Result>; + +static AUTO_CONSUME_ENABLED: LazyLock> = LazyLock::new(|| RwLock::new(true)); +static PROCESSED_WINDOWS: LazyLock>> = LazyLock::new(|| RwLock::new(HashSet::new())); +const IPC_FILE: &str = "/tmp/niri-auto-consume.sock"; + +#[derive(Debug, serde::Deserialize)] +struct WindowOpenedOrChanged { + id: u64, + workspace_id: u64, + is_floating: bool, +} + +#[derive(Debug, serde::Deserialize)] +enum Event { + WindowOpenedOrChanged { + window: WindowOpenedOrChanged, + }, + WindowClosed { + id: u64, + }, +} + +fn main() -> Result<()> { + if std::env::args().any(|e| e == "toggle") { + toggle_auto_consume(); + return Ok(()); + } + + if std::env::args().any(|e| e == "status") { + show_status(); + return Ok(()); + } + + std::fs::remove_file(IPC_FILE).ok(); + + std::thread::spawn(socket_loop); + + let mut child = Command::new("niri") + .arg("msg") + .arg("--json") + .arg("event-stream") + .stdout(Stdio::piped()) + .spawn()?; + + let stdout = child.stdout.take().expect("Failed to capture stdout"); + + let reader = BufReader::new(stdout); + + for line in reader.lines() { + match line { + Ok(line_content) => { + let payload = serde_json::from_str::(&line_content); + match payload { + Ok(Event::WindowOpenedOrChanged { window }) => { + on_window_opened(window); + } + Ok(Event::WindowClosed { id }) => { + on_window_closed(id); + } + _ => (), + } + } + Err(e) => { + eprintln!("Error reading output: {}", e); + } + } + } + + child.wait()?; + + Ok(()) +} + +fn toggle_auto_consume() { + // Send toggle command to the running daemon via socket + if let Ok(mut stream) = std::os::unix::net::UnixStream::connect(IPC_FILE) { + use std::io::{Write, Read}; + stream.write_all(b"toggle\n").expect("Failed to send toggle command"); + stream.flush().expect("Failed to flush toggle command"); + + // Read and print the response + let mut response = String::new(); + stream.read_to_string(&mut response).expect("Failed to read response"); + print!("{}", response); + } else { + eprintln!("Failed to connect to auto-consume daemon. Is it running?"); + } +} + +fn show_status() { + // Send status command to the running daemon via socket + if let Ok(mut stream) = std::os::unix::net::UnixStream::connect(IPC_FILE) { + use std::io::{Write, Read}; + stream.write_all(b"status\n").expect("Failed to send status command"); + stream.flush().expect("Failed to flush status command"); + + // Read and print the response + let mut response = String::new(); + stream.read_to_string(&mut response).expect("Failed to read response"); + print!("{}", response); + } else { + eprintln!("Failed to connect to auto-consume daemon. Is it running?"); + } +} + +fn socket_loop() { + let listener = UnixListener::bind(IPC_FILE).expect("We couldn't make the listener"); + + for stream in listener.incoming().flatten() { + let mut reader = BufReader::new(stream.try_clone().expect("Failed to clone stream")); + let mut writer = reader.get_mut().try_clone().expect("Failed to get writer"); + let mut line = String::new(); + if reader.read_line(&mut line).is_ok() { + let command = line.trim(); + match command { + "toggle" => { + let response = handle_toggle(); + let _ = writer.write_all(response.as_bytes()); + let _ = writer.flush(); + } + "status" => { + let response = handle_status(); + let _ = writer.write_all(response.as_bytes()); + let _ = writer.flush(); + } + _ => { + let response = format!("Unknown command: {}\n", command); + let _ = writer.write_all(response.as_bytes()); + let _ = writer.flush(); + } + } + } + } +} + +fn handle_toggle() -> String { + let mut enabled = AUTO_CONSUME_ENABLED.write().expect("Failed to acquire write lock"); + *enabled = !*enabled; + + if *enabled { + "Auto consume enabled\n".to_string() + } else { + "Auto consume disabled\n".to_string() + } +} + +fn handle_status() -> String { + let enabled = AUTO_CONSUME_ENABLED.read().expect("Failed to acquire read lock"); + if *enabled { + "Auto consume: enabled\n".to_string() + } else { + "Auto consume: disabled\n".to_string() + } +} + +fn consume_window(window_id: u64) -> Result<()> { + let output = Command::new("niri") + .args(["msg", "action", "consume-or-expel-window-left"]) + .arg("--id") + .arg(window_id.to_string()) + .output()?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(format!("Failed to consume window {}: {}", window_id, stderr).into()); + } + + Ok(()) +} + +fn on_window_opened(window: WindowOpenedOrChanged) { + let enabled = { + *AUTO_CONSUME_ENABLED.read().expect("Failed to acquire read lock") + }; + + if !enabled { + return; + } + + // Check if we've already processed this window + let already_processed = { + let mut processed = PROCESSED_WINDOWS.write().expect("Failed to acquire write lock"); + if processed.contains(&window.id) { + true + } else { + processed.insert(window.id); + false + } + }; + + // If already processed, ignore this event + if already_processed { + return; + } + + // Consume the window automatically regardless of floating state + if let Err(e) = consume_window(window.id) { + eprintln!("Failed to consume window {}: {}", window.id, e); + } +} + +fn on_window_closed(window_id: u64) { + // Remove window from processed set when it closes to prevent memory leaks + let mut processed = PROCESSED_WINDOWS.write().expect("Failed to acquire write lock"); + processed.remove(&window_id); +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..e34cdc8 --- /dev/null +++ b/flake.lock @@ -0,0 +1,44 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1760139962, + "narHash": "sha256-4xggC56Rub3WInz5eD7EZWXuLXpNvJiUPahGtMkwtuc=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "7e297ddff44a3cc93673bb38d0374df8d0ad73e4", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-25.05", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-unstable": { + "locked": { + "lastModified": 1760284886, + "narHash": "sha256-TK9Kr0BYBQ/1P5kAsnNQhmWWKgmZXwUQr4ZMjCzWf2c=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "cf3f5c4def3c7b5f1fc012b3d839575dbe552d43", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs", + "nixpkgs-unstable": "nixpkgs-unstable" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..2013749 --- /dev/null +++ b/flake.nix @@ -0,0 +1,163 @@ +# vim: set ts=2 sw=2 et: +{ + description = "Niri scripts"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05"; + nixpkgs-unstable = { + # NOTE: WE NEED CARGO 1.88+ AS IT HAS rust#132833 + # NOW STABLE NIX HAS ONLY CARGO 1.86 + url = "github:NixOS/nixpkgs/nixos-unstable"; + }; + }; + + outputs = + { + self, + nixpkgs, + nixpkgs-unstable, + ... + }: + let + system = "x86_64-linux"; + pkgs = nixpkgs.legacyPackages.${system}; + unstablePkgs = nixpkgs-unstable.legacyPackages.${system}; + + scriptisto = pkgs.scriptisto; + + # helper to wrap an rs script + wrap = + scriptFile: name: isRust: + if isRust then + pkgs.stdenv.mkDerivation rec { + pname = builtins.baseNameOf scriptFile; + version = "0.1.0"; + + src = scriptFile; + dontUnpack = true; + + nativeBuildInputs = [ pkgs.makeWrapper ]; + buildInputs = [ pkgs.scriptisto ]; + + installPhase = '' + mkdir -p $out/bin + cp $src $out/${pname}.rs + makeWrapper ${pkgs.scriptisto}/bin/scriptisto $out/bin/${name} \ + --add-flags "$out/${pname}.rs" \ + --prefix PATH : ${ + pkgs.lib.makeBinPath [ + unstablePkgs.rustc + unstablePkgs.cargo + unstablePkgs.rustPackages.clippy + pkgs.gcc + pkgs.pkg-config + ] + } + ''; + + meta = with pkgs.lib; { + description = "Niri scripts"; + homepage = "https://github.com/0xWal/niri-scripts"; + license = licenses.mit; + platforms = platforms.linux; + mainProgram = name; + }; + } + else + # For non-Rust scripts: Just symlink it or wrap in a trivial script + pkgs.writeShellScriptBin name '' + exec ${scriptFile} "$@" + ''; + + wallpaper = wrap ./wallpaper-per-workspace "niri-wallpaper-per-workspace" true; + sticky = rec { + daemon = (wrap ./support-sticky-floating "niri-sticky-daemon") true; + client = + (wrap (pkgs.writeShellScript "niri-sticky-client-wrapper" '' + ${pkgs.lib.getExe daemon} toggle-sticky + '') "niri-sticky-client") + false; + }; + screenshot = wrap ./screenshot "niri-screenshot" true; + autoConsume = wrap ./auto-consume-new-windows "niri-auto-consume" true; + + in + { + packages.${system} = { + sticky = sticky; + wallpaper = wallpaper; + screenshot = screenshot; + autoConsume = autoConsume; + }; + + nixosConfigurations.default = nixpkgs.lib.nixosSystem { + system = system; + modules = [ + (import "${nixpkgs}/nixos/modules/installer/cd-dvd/installation-cd-base.nix") + { + imports = [ + self.nixosModules.default + + ]; + + users.users.niri = { + isNormalUser = true; + description = "Niri Test User"; + extraGroups = [ "wheel" ]; # for sudo + }; + + services.getty.autologinUser = pkgs.lib.mkForce "niri"; + environment.systemPackages = [ + sticky.daemon + sticky.client + wallpaper + screenshot + autoConsume + ]; + } + { + hardware.graphics.enable = true; + programs.niri.enable = true; + niri-scripts = { + enable = true; + screenshot = { + enable = true; + dir = "~/Pictures/Screenshots"; + }; + wallpaper-per-workspace = { + enable = true; + dir = "~/.wallpapers"; + }; + sticky-window.enable = true; + auto-consume.enable = true; + }; + } + ]; + }; + + devShells.${system}.default = pkgs.mkShell { + packages = [ + sticky.daemon + sticky.client + wallpaper + screenshot + autoConsume + ]; + }; + + # nixosModules.default = ( + # import ./nix/niri-scripts.nix (with pkgs; { + # inherit + # config + # lib + # pkgs + # ; + # niriScriptsPkg = { + # inherit supportSticky wallpaper screenshot; + # }; + # }) + # ); + + nixosModules.default = import ./nix/niri-scripts.nix { inherit self; }; + }; +} diff --git a/nix/niri-scripts.nix b/nix/niri-scripts.nix new file mode 100644 index 0000000..55c3602 --- /dev/null +++ b/nix/niri-scripts.nix @@ -0,0 +1,127 @@ +# vim: set ts=2 sw=2 et: +{ self }: +{ + config, + lib, + pkgs, + ... +}: + +with lib; +let + cfg = config.niri-scripts; + selfPkgs = self.packages.${pkgs.system}; +in +{ + options.niri-scripts = { + enable = mkEnableOption "Enable all Niri scripts"; + + screenshot = { + enable = mkEnableOption "Enable screenshot script"; + dir = mkOption { + type = types.str; + default = "~/Screenshots"; + description = "Directory to save screenshots to"; + }; + }; + + sticky-window = { + enable = mkEnableOption "Enable sticky window support"; + }; + + auto-consume = { + enable = mkEnableOption "Enable auto-consume new windows script"; + }; + + wallpaper-per-workspace = { + enable = mkEnableOption "Enable wallpaper-per-workspace script"; + dir = mkOption { + type = types.str; + default = "~/wallpapers"; + description = "Directory containing wallpapers per workspace"; + }; + }; + }; + + config = mkIf cfg.enable (mkMerge [ + (mkIf cfg.screenshot.enable { + assertions = [ + { + assertion = config.programs.niri.enable or false; + message = "niri-scripts requires programs.niri.enable = true"; + } + ]; + + environment.systemPackages = [ + (pkgs.writeShellScriptBin "niri-screenshot" '' + ${lib.getExe selfPkgs.screenshot} ${cfg.screenshot.dir} + '') + + (pkgs.writeShellScriptBin "niri-screenshot-annotate" '' + ${lib.getExe selfPkgs.screenshot} ${cfg.screenshot.dir} --annotate + '') + + pkgs.scriptisto + pkgs.grim + pkgs.satty + pkgs.slurp + pkgs.wl-clipboard + ]; + }) + + (mkIf cfg.sticky-window.enable { + environment.systemPackages = with selfPkgs.sticky; [ + daemon + client + ]; + + # systemd.user.services.support-sticky-floating = { + # enable = true; + # description = "Niri sticky window support"; + # serviceConfig = { + # ExecStart = "${lib.getExe selfPkgs.supportSticky}"; + # After = "niri.service"; + # Requires = "niri.service"; + # }; + # wantedBy = [ "default.target" ]; + # }; + }) + + (mkIf cfg.wallpaper-per-workspace.enable { + environment.systemPackages = [ + selfPkgs.wallpaper + pkgs.swww + ]; + + # systemd.user.services.wallpaper-per-workspace = { + # enable = true; + # description = "Niri wallpaper per workspace"; + # serviceConfig = { + # ExecStart = "${lib.getExe selfPkgs.wallpaper} ${cfg.wallpaper-per-workspace.dir}"; + # Restart = "always"; + # After = "niri.service"; + # Requires = "niri.service"; + # }; + # wantedBy = [ "default.target" ]; + # }; + }) + + (mkIf cfg.auto-consume.enable { + environment.systemPackages = [ + selfPkgs.autoConsume + ]; + + # systemd.user.services.auto-consume = { + # enable = true; + # description = "Niri auto-consume new windows"; + # serviceConfig = { + # ExecStart = "${lib.getExe selfPkgs.autoConsume}"; + # Restart = "always"; + # After = "niri.service"; + # Requires = "niri.service"; + # }; + # wantedBy = [ "default.target" ]; + # }; + }) + ]); +} diff --git a/screenshot b/screenshot index 8e89754..a895ce7 100755 --- a/screenshot +++ b/screenshot @@ -4,7 +4,6 @@ // scriptisto-begin // script_src: src/main.rs // build_cmd: > -// cargo clippy --color=always && // cargo build --release --color=always && // strip ./target/release/screenshot // target_bin: ./target/release/screenshot @@ -30,6 +29,18 @@ use std::error::Error; use std::time::SystemTime; +fn screenshot_lock_active(lock_file_path: &str) -> bool { + let Ok(contents) = std::fs::read_to_string(lock_file_path) else { + return false; + }; + + let Ok(pid) = contents.trim().parse::() else { + return false; + }; + + std::path::Path::new(&format!("/proc/{}", pid)).exists() +} + fn init_screenshot(filename: &str) -> Result> { let coords = { let output = Command::new("slurp") @@ -72,23 +83,29 @@ fn annotate_screenshot(path: &str) -> Result<(), Box> { } fn main() { - let lock_file_path = ".screenshot.lock"; - - if std::fs::exists(lock_file_path).unwrap_or(false) { - println!( - "Screenshot already in progress, exiting. delete {} to force", - lock_file_path - ); - std::process::exit(0); + let runtime_dir = std::env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| "/tmp".to_string()); + let lock_file_path = format!("{}/screenshot.lock", runtime_dir); + + if std::fs::exists(lock_file_path.as_str()).unwrap_or(false) { + if screenshot_lock_active(lock_file_path.as_str()) { + println!( + "Screenshot already in progress, exiting. delete {} to force", + lock_file_path + ); + std::process::exit(0); + } + + std::fs::remove_file(lock_file_path.as_str()).ok(); } - std::fs::write(lock_file_path, "").expect("Failed to write lock file"); + std::fs::write(lock_file_path.as_str(), std::process::id().to_string()).expect("Failed to write lock file"); defer! { - std::fs::remove_file(lock_file_path).ok(); + std::fs::remove_file(lock_file_path.as_str()).ok(); }; let screenshots_dir = std::env::args().nth(1).expect("No screenshot directory specified"); + std::fs::create_dir_all(&screenshots_dir).expect("Failed to create screenshot directory"); let time = SystemTime::now() .duration_since(std::time::UNIX_EPOCH) diff --git a/support-sticky-floating b/support-sticky-floating index af25fa6..219c0fd 100755 --- a/support-sticky-floating +++ b/support-sticky-floating @@ -6,7 +6,6 @@ // scriptisto-begin // script_src: src/main.rs // build_cmd: > -// cargo clippy --color=always && // cargo build --release --color=always && strip ./target/release/niri_sticky // target_bin: ./target/release/niri_sticky // files: @@ -43,7 +42,16 @@ use std::sync::{ type Result = std::result::Result>; static STICKY_WINDOWS: LazyLock>> = LazyLock::new(|| RwLock::new(HashMap::new())); -const IPC_FILE: &str = "/tmp/niri-floating.sock"; + +struct InstanceLock { + path: String, +} + +impl Drop for InstanceLock { + fn drop(&mut self) { + std::fs::remove_file(&self.path).ok(); + } +} #[derive(Clone, Debug, Eq, Hash, PartialEq)] struct FloatingWindow { @@ -84,13 +92,47 @@ enum Event { }, } +fn runtime_dir() -> String { + std::env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| "/tmp".to_string()) +} + +fn ipc_file() -> String { + format!("{}/niri-floating.sock", runtime_dir()) +} + +fn pid_file() -> String { + format!("{}/niri-floating.pid", runtime_dir()) +} + +fn process_alive(pid: u32) -> bool { + std::path::Path::new(&format!("/proc/{}", pid)).exists() +} + +fn acquire_instance_lock() -> Result { + let pid_file = pid_file(); + + if let Ok(contents) = std::fs::read_to_string(&pid_file) { + if let Ok(pid) = contents.trim().parse::() { + if process_alive(pid) { + return Err("Sticky floating service is already running".into()); + } + } + } + + std::fs::write(&pid_file, std::process::id().to_string())?; + Ok(InstanceLock { + path: pid_file, + }) +} + fn main() -> Result<()> { if std::env::args().any(|e| e == "toggle-sticky") { toggle_sticky(); return Ok(()); } - std::fs::remove_file(IPC_FILE).ok(); + let _instance_lock = acquire_instance_lock()?; + std::fs::remove_file(ipc_file()).ok(); std::thread::spawn(socket_loop); @@ -138,6 +180,7 @@ fn toggle_sticky() { .stdout(Stdio::piped()) .output() else { + eprintln!("Failed to query the focused window from niri"); return; }; @@ -152,10 +195,14 @@ fn toggle_sticky() { let output = String::from_utf8_lossy(&output.stdout); let Ok(window_info) = serde_json::from_str::(&output) else { + eprintln!("Failed to parse focused window details from niri"); return; }; - let mut unix = UnixStream::connect(IPC_FILE).expect("Failed to connect to socket"); + let Ok(mut unix) = UnixStream::connect(ipc_file()) else { + eprintln!("Sticky floating service is not running"); + return; + }; unix .write_all(format!("{}:{}", window_info.id, window_info.workspace_id).as_bytes()) @@ -163,7 +210,7 @@ fn toggle_sticky() { } fn socket_loop() { - let listener = UnixListener::bind(IPC_FILE).expect("We couldn't make the listener"); + let listener = UnixListener::bind(ipc_file()).expect("We couldn't make the listener"); for stream in listener.incoming().flatten() { let mut reader = BufReader::new(stream); @@ -196,7 +243,7 @@ fn socket_loop() { } let Ok(workspace) = get_workspace_info(workspace_id) else { - return; + continue; }; let window = FloatingWindow { diff --git a/toggle-sticky b/toggle-sticky index aecb763..aed5a3e 100755 --- a/toggle-sticky +++ b/toggle-sticky @@ -1,4 +1,8 @@ #!/bin/sh # set ft=bash -./support-sticky-floating toggle-sticky +set -eu + +script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) + +exec "$script_dir/support-sticky-floating" toggle-sticky diff --git a/wallpaper-per-workspace b/wallpaper-per-workspace index aa2c858..8be5453 100755 --- a/wallpaper-per-workspace +++ b/wallpaper-per-workspace @@ -36,26 +36,15 @@ use std::process::{ type Result = std::result::Result>; -static mut ACTIVE_WALLPAPER: Option = None; +static mut ACTIVE_WALLPAPER: Option = None; static mut WALLPAPERS_DIR: *mut String = std::ptr::null_mut(); -// #[rustfmt::skip] -// const WALLPAPERS: &[(&str, &str)] = &[ -// ("1", "601-2.png"), -// ("2", "594-2.jpg"), -// ("3", "595.png"), -// ("4", "591-darken.png"), -// ("5", "572-1.png"), -// ("6", "571.png"), -// ("7", "571.png"), -// -// ("w21", "601-2.png"), -// ("w22", "594-2.jpg"), -// ("w23", "595.png"), -// ("w24", "591-darken.png"), -// ("w25", "572-1.png"), -// ("w26", "571.png"), -// ]; +#[derive(Debug)] +struct WallpaperMeta { + path: String, + size: u64, + modified: std::time::SystemTime, +} #[derive(Debug, serde::Deserialize)] struct WorkspaceFocused { @@ -79,6 +68,18 @@ enum Event { WorkspaceActivated(WorkspaceFocused), } +fn get_file_meta(path: &str) -> Option { + let meta = std::fs::metadata(path).ok()?; + let size = meta.len(); + let modified = meta.modified().ok()?; + + Some(WallpaperMeta { + path: path.to_string(), + size, + modified, + }) +} + fn check_file_exist(path: &str) -> bool { std::fs::metadata(path).is_ok() } @@ -102,9 +103,7 @@ fn get_workspace_info(id: u64) -> Option { } fn change_wallpaper(wallpaper: &str, output: &str) -> Result<()> { - let wallpaper = format!("{}/{wallpaper}", unsafe { &*WALLPAPERS_DIR }); - - if !check_file_exist(&wallpaper) { + if !check_file_exist(wallpaper) { return Err("Wallpaper not found".into()); } @@ -134,23 +133,53 @@ fn on_workspace_focused(payload: WorkspaceFocused) { return; }; + let wallpaper = format!("{}/{}", unsafe { &*WALLPAPERS_DIR }, &ws_id); + + let Some(wallpaper_meta) = get_file_meta(&wallpaper) else { + let fallback_path = format!("{}/{}", unsafe { &*WALLPAPERS_DIR }, "FALLBACK"); + + let Some(wallpaper_meta) = get_file_meta(&fallback_path) else { + return; + }; + + unsafe { + if let Some(ref current) = ACTIVE_WALLPAPER + && current.size == wallpaper_meta.size + && current.modified == wallpaper_meta.modified + { + return; + } + } + + change_wallpaper(&fallback_path, &output).ok(); + + unsafe { + ACTIVE_WALLPAPER = Some(wallpaper_meta); + } + + return; + }; + unsafe { - let a = &raw const ACTIVE_WALLPAPER; - if let Some(w) = &*a && *w == ws_id { - return; - } + if let Some(ref current) = ACTIVE_WALLPAPER + && current.size == wallpaper_meta.size + && current.modified == wallpaper_meta.modified + { + return; + } } unsafe { - ACTIVE_WALLPAPER = Some(ws_id.to_string()); + ACTIVE_WALLPAPER = Some(wallpaper_meta); } - change_wallpaper(&ws_id, &output) + change_wallpaper(&wallpaper, &output) .or_else(|_| change_wallpaper("FALLBACK", &output)) .ok(); } fn main() -> io::Result<()> { + println!("wallpaper-per-workspace"); let wallpapers_dir = std::env::args() .nth(1) .or_else(|| std::env::var("WALLPAPERS_DIRS").ok())