Skip to main content

wallswitch/backends/
awww.rs

1use crate::{
2    Config, FileInfo, WallSwitchError, WallSwitchResult, WallpaperBackend, detect_monitors,
3    exec_cmd, get_random_integer,
4};
5use std::{
6    env, fs,
7    io::{self, Write},
8    process::{Command, Stdio},
9    thread::sleep,
10    time::Duration,
11};
12
13pub struct AwwwBackend;
14
15impl WallpaperBackend for AwwwBackend {
16    fn build_commands(_images: &[FileInfo], _config: &Config) -> WallSwitchResult<Vec<Command>> {
17        // Overrides `apply` below directly since we need daemon state management.
18        Ok(vec![])
19    }
20
21    fn apply(images: &[FileInfo], config: &Config) -> WallSwitchResult<()> {
22        let monitors = detect_monitors(config)?;
23
24        if config.verbose {
25            println!("monitors:\n{monitors:#?}\n");
26        }
27
28        // Starts or restarts daemon if necessary
29        ensure_daemon_running(config)?;
30
31        // Cycle through images to ensure all monitors receive a command,
32        // avoiding issues when detected monitors > configured monitors.
33        for (image, monitor) in images.iter().cycle().zip(monitors.iter()) {
34            let effect = get_transition_effect(config);
35
36            let mut cmd = Command::new("awww");
37            cmd.args(["img", "-o", monitor])
38                .arg(&image.path)
39                .args(["--transition-type", &effect])
40                .args([
41                    "--transition-duration",
42                    &config.transition_duration.to_string(),
43                ])
44                .args(["--transition-fps", &config.transition_fps.to_string()])
45                .args(["--transition-angle", &config.transition_angle.to_string()])
46                .args(["--transition-pos", &config.transition_pos]);
47
48            if config.dry_run {
49                println!("[DRY-RUN] Would execute: {:?}", cmd);
50            } else {
51                exec_cmd(
52                    &mut cmd,
53                    config.verbose,
54                    &format!("Apply awww on {}", monitor),
55                )?;
56            }
57        }
58
59        Ok(())
60    }
61}
62
63// ==============================================================================
64// INTERNAL HELPERS
65// ==============================================================================
66
67fn get_transition_effect(config: &Config) -> String {
68    if config.transition_type.to_lowercase() == "random" {
69        let effects = ["wipe", "fade", "center", "outer", "wave", "left", "right"];
70        let idx: usize = get_random_integer(0, effects.len() - 1);
71        effects[idx].to_string()
72    } else {
73        config.transition_type.clone()
74    }
75}
76
77fn ensure_daemon_running(config: &Config) -> WallSwitchResult<()> {
78    if is_daemon_alive() {
79        return Ok(());
80    }
81
82    if config.dry_run {
83        println!("[DRY-RUN] awww-daemon is down; would perform clean start.");
84        return Ok(());
85    }
86
87    if config.verbose {
88        println!("awww-daemon is down. Performing clean start...");
89    }
90
91    let _ = Command::new("killall").arg("awww-daemon").output();
92    clean_stale_sockets();
93
94    Command::new("awww-daemon")
95        .stdout(Stdio::null())
96        .stderr(Stdio::null())
97        .spawn()
98        .map_err(|e| WallSwitchError::AwwwDaemonError(e.to_string()))?;
99
100    let mut elapsed = 0.0;
101    let step = 0.2;
102    let max_wait = 5.0;
103
104    while elapsed < max_wait {
105        if is_daemon_alive() {
106            if config.verbose {
107                println!("\nawww-daemon successfully initialized.");
108            }
109            return Ok(());
110        }
111
112        if config.verbose {
113            print!(
114                "\rWait to initialize awww-daemon. Time: {:0.1}/{:0.1}",
115                elapsed, max_wait
116            );
117            io::stdout().flush().ok();
118        }
119
120        sleep(Duration::from_secs_f32(step));
121        elapsed += step;
122    }
123
124    if config.verbose {
125        println!();
126    }
127
128    Err(WallSwitchError::AwwwDaemonError(
129        "Daemon failed to initialize.".into(),
130    ))
131}
132
133fn is_daemon_alive() -> bool {
134    Command::new("awww")
135        .arg("query")
136        .stdout(Stdio::null())
137        .stderr(Stdio::null())
138        .status()
139        .map(|s| s.success())
140        .unwrap_or(false)
141}
142
143fn clean_stale_sockets() {
144    let runtime_dir = env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| "/tmp".to_string());
145    if let Ok(entries) = fs::read_dir(&runtime_dir) {
146        for entry in entries.flatten() {
147            let name = entry.file_name().to_string_lossy().to_string();
148            if name.contains("awww") && name.ends_with(".sock") {
149                let _ = fs::remove_file(entry.path());
150            }
151        }
152    }
153}