wallswitch/backends/
awww.rs1use 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 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 ensure_daemon_running(config)?;
30
31 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
63fn 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}