1use crate::config::WallrConfig;
2use crate::ipc::{IpcCommand, IpcResponse, start_ipc_server};
3use crate::renderer::Renderer;
4use crate::wallpaper::{SetOptions, WallpaperEngine};
5use notify::{Event, EventKind, RecursiveMode, Watcher};
6use std::path::PathBuf;
7use std::sync::Arc;
8use std::sync::atomic::{AtomicBool, Ordering};
9use tokio::sync::Mutex;
10
11use raw_window_handle::{
12 DisplayHandle, HasDisplayHandle, HasWindowHandle, RawDisplayHandle, RawWindowHandle,
13 WaylandDisplayHandle, WaylandWindowHandle, WindowHandle,
14};
15use smithay_client_toolkit::{
16 compositor::{CompositorHandler, CompositorState},
17 delegate_compositor, delegate_layer, delegate_output, delegate_registry, delegate_shm,
18 output::{OutputHandler, OutputState},
19 registry::{ProvidesRegistryState, RegistryState},
20 registry_handlers,
21 shell::WaylandSurface,
22 shell::wlr_layer::{
23 Anchor, KeyboardInteractivity, Layer, LayerShell, LayerShellHandler, LayerSurface,
24 LayerSurfaceConfigure,
25 },
26 shm::{Shm, ShmHandler},
27};
28use wayland_client::{
29 Connection, Proxy, QueueHandle,
30 globals::registry_queue_init,
31 protocol::{wl_compositor, wl_output, wl_surface},
32};
33#[derive(Debug, thiserror::Error)]
34pub enum DaemonError {
35 #[error("daemon already running: {0}")]
36 AlreadyRunning(String),
37 #[error("failed to start daemon: {0}")]
38 StartError(String),
39 #[error("I/O error: {0}")]
40 Io(#[from] std::io::Error),
41 #[error("IPC error: {0}")]
42 Ipc(#[from] crate::ipc::IpcError),
43 #[error("Config error: {0}")]
44 Config(#[from] crate::config::ConfigError),
45 #[error("Wallpaper error: {0}")]
46 Wallpaper(#[from] crate::wallpaper::WallpaperError),
47}
48
49pub struct WaylandWindow {
50 pub display: *mut std::ffi::c_void,
51 pub surface: *mut std::ffi::c_void,
52}
53
54unsafe impl Send for WaylandWindow {}
55unsafe impl Sync for WaylandWindow {}
56
57impl HasWindowHandle for WaylandWindow {
58 fn window_handle(&self) -> Result<WindowHandle<'_>, raw_window_handle::HandleError> {
59 let surface = std::ptr::NonNull::new(self.surface)
60 .ok_or(raw_window_handle::HandleError::Unavailable)?;
61 let handle = WaylandWindowHandle::new(surface);
62 unsafe { Ok(WindowHandle::borrow_raw(RawWindowHandle::Wayland(handle))) }
63 }
64}
65
66impl HasDisplayHandle for WaylandWindow {
67 fn display_handle(&self) -> Result<DisplayHandle<'_>, raw_window_handle::HandleError> {
68 let display = std::ptr::NonNull::new(self.display)
69 .ok_or(raw_window_handle::HandleError::Unavailable)?;
70 let handle = WaylandDisplayHandle::new(display);
71 unsafe { Ok(DisplayHandle::borrow_raw(RawDisplayHandle::Wayland(handle))) }
72 }
73}
74
75#[derive(Clone)]
76struct OutputInfo {
77 name: String,
78 width: u32,
79 height: u32,
80 scale_factor: i32,
81 wl_output: wl_output::WlOutput,
82}
83
84struct WaylandState {
85 registry_state: RegistryState,
86 output_state: OutputState,
87 compositor_state: CompositorState,
88 shm: Shm,
89 outputs: std::collections::HashMap<u32, OutputInfo>,
90 surfaces: Vec<(u32, LayerSurface)>,
91 layer_shell: LayerShell,
93 compositor: wl_compositor::WlCompositor,
95 hotplug: Option<DaemonHotplug>,
98}
99
100struct SendDisplayPtr(*mut std::ffi::c_void);
103unsafe impl Send for SendDisplayPtr {}
104
105struct DaemonHotplug {
109 renderer: std::sync::Arc<Renderer>,
110 config: crate::config::WallrConfig,
111 display_ptr: SendDisplayPtr,
112 render_states: std::sync::Arc<
117 tokio::sync::Mutex<
118 std::collections::HashMap<String, std::sync::Arc<tokio::sync::Mutex<RenderState>>>,
119 >,
120 >,
121}
122
123impl ProvidesRegistryState for WaylandState {
124 fn registry(&mut self) -> &mut RegistryState {
125 &mut self.registry_state
126 }
127
128 registry_handlers![OutputState,];
129}
130
131impl CompositorHandler for WaylandState {
132 fn scale_factor_changed(
133 &mut self,
134 _conn: &Connection,
135 _qh: &QueueHandle<Self>,
136 _surface: &wl_surface::WlSurface,
137 _new_factor: i32,
138 ) {
139 }
140 fn transform_changed(
141 &mut self,
142 _conn: &Connection,
143 _qh: &QueueHandle<Self>,
144 _surface: &wl_surface::WlSurface,
145 _new_transform: wl_output::Transform,
146 ) {
147 }
148 fn frame(
149 &mut self,
150 _conn: &Connection,
151 _qh: &QueueHandle<Self>,
152 _surface: &wl_surface::WlSurface,
153 _time: u32,
154 ) {
155 }
156 fn surface_enter(
157 &mut self,
158 _conn: &Connection,
159 _qh: &QueueHandle<Self>,
160 _surface: &wl_surface::WlSurface,
161 _output: &wl_output::WlOutput,
162 ) {
163 }
164 fn surface_leave(
165 &mut self,
166 _conn: &Connection,
167 _qh: &QueueHandle<Self>,
168 _surface: &wl_surface::WlSurface,
169 _output: &wl_output::WlOutput,
170 ) {
171 }
172}
173
174impl wayland_client::Dispatch<wayland_client::protocol::wl_region::WlRegion, ()> for WaylandState {
175 fn event(
176 _state: &mut WaylandState,
177 _region: &wayland_client::protocol::wl_region::WlRegion,
178 _event: wayland_client::protocol::wl_region::Event,
179 _data: &(),
180 _conn: &Connection,
181 _qh: &QueueHandle<WaylandState>,
182 ) {
183 }
184}
185
186impl LayerShellHandler for WaylandState {
187 fn configure(
188 &mut self,
189 _conn: &Connection,
190 _qh: &QueueHandle<Self>,
191 layer: &LayerSurface,
192 _configure: LayerSurfaceConfigure,
193 _serial: u32,
194 ) {
195 layer.commit();
196 }
197
198 fn closed(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _layer: &LayerSurface) {}
199}
200
201impl ShmHandler for WaylandState {
202 fn shm_state(&mut self) -> &mut Shm {
203 &mut self.shm
204 }
205}
206
207impl OutputHandler for WaylandState {
208 fn output_state(&mut self) -> &mut OutputState {
209 &mut self.output_state
210 }
211 fn new_output(
212 &mut self,
213 _conn: &Connection,
214 _qh: &QueueHandle<Self>,
215 output: wl_output::WlOutput,
216 ) {
217 let id = output.id().protocol_id();
218 tracing::info!("Output detected: protocol_id={id}");
219
220 let mut info = OutputInfo {
221 name: format!("output-{id}"),
222 width: 1920,
223 height: 1080,
224 scale_factor: 1,
225 wl_output: output,
226 };
227
228 if let Some(info_data) = self.output_state.info(&info.wl_output) {
230 if let Some(mode) = info_data.modes.iter().find(|m| m.current) {
231 info.width = mode.dimensions.0 as u32;
232 info.height = mode.dimensions.1 as u32;
233 }
234 info.scale_factor = info_data.scale_factor;
235 let resolved = info_data
236 .name
237 .as_deref()
238 .filter(|n| !n.is_empty())
239 .or(info_data.description.as_deref().filter(|n| !n.is_empty()));
240 if let Some(real_name) = resolved {
241 info.name = real_name.to_string();
242 } else if !info_data.make.is_empty() || !info_data.model.is_empty() {
243 let fallback = format!("{} {}", info_data.make, info_data.model)
244 .trim()
245 .to_string();
246 if !fallback.is_empty() {
247 info.name = fallback;
248 }
249 }
250 }
251
252 self.outputs.insert(id, info.clone());
253
254 if self.hotplug.is_some() {
258 let name = info.name.clone();
259 let renderer = self.hotplug.as_ref().unwrap().renderer.clone();
260 let display_ptr = self.hotplug.as_ref().unwrap().display_ptr.0;
261 let config = self.hotplug.as_ref().unwrap().config.clone();
262 let render_states = self.hotplug.as_ref().unwrap().render_states.clone();
263 match create_render_state_for_output_sync(
264 &renderer,
265 display_ptr,
266 self,
267 _qh,
268 &info,
269 &config,
270 ) {
271 Ok(rs) => {
272 let rs = std::sync::Arc::new(tokio::sync::Mutex::new(rs));
273 tokio::spawn(async move {
274 render_states.lock().await.insert(name.clone(), rs.clone());
275
276 let state_path = dirs::cache_dir()
278 .unwrap_or_else(|| std::path::PathBuf::from("/tmp"))
279 .join(format!("wallr/last_wallpaper/{name}"));
280 if let Ok(path_str) = std::fs::read_to_string(&state_path) {
281 let p = std::path::Path::new(path_str.trim());
282 if p.exists() {
283 let mut lock = rs.lock().await;
284 let effect = crate::animation::Effect::Fade(
285 crate::animation::FadeParams::default(),
286 );
287 let _ = lock.set_wallpaper(p, &effect, 1000, 0).await;
288 }
289 }
290 tracing::info!("Hotplug: output {name} ready");
291 });
292 }
293 Err(e) => {
294 tracing::error!("Hotplug: failed to create render state for {name}: {e}");
295 }
296 }
297 }
298 }
299 fn update_output(
300 &mut self,
301 _conn: &Connection,
302 _qh: &QueueHandle<Self>,
303 output: wl_output::WlOutput,
304 ) {
305 let id = output.id().protocol_id();
306 if let Some(info) = self.outputs.get_mut(&id) {
307 let mut new_width = info.width;
308 let mut new_height = info.height;
309 let mut new_scale = info.scale_factor;
310
311 if let Some(mode) = self
312 .output_state
313 .info(&output)
314 .and_then(|i| i.modes.iter().find(|m| m.current).cloned())
315 {
316 new_width = mode.dimensions.0 as u32;
317 new_height = mode.dimensions.1 as u32;
318 }
319 if let Some(info_data) = self.output_state.info(&output) {
320 new_scale = info_data.scale_factor;
321 let resolved = info_data
322 .name
323 .as_deref()
324 .filter(|n| !n.is_empty())
325 .or(info_data.description.as_deref().filter(|n| !n.is_empty()));
326 if let Some(real_name) = resolved {
327 if real_name != info.name {
328 tracing::info!(
329 "Output {id}: resolved name '{}' -> '{}'",
330 info.name,
331 real_name
332 );
333 info.name = real_name.to_string();
334 }
335 } else if info.name.starts_with("output-")
336 && (!info_data.make.is_empty() || !info_data.model.is_empty())
337 {
338 let fallback = format!("{} {}", info_data.make, info_data.model)
339 .trim()
340 .to_string();
341 if !fallback.is_empty() {
342 tracing::info!(
343 "Output {id}: fallback name '{}' -> '{}'",
344 info.name,
345 fallback
346 );
347 info.name = fallback;
348 }
349 }
350 }
351
352 let changed = new_width != info.width
353 || new_height != info.height
354 || new_scale != info.scale_factor;
355
356 info.width = new_width;
357 info.height = new_height;
358 info.scale_factor = new_scale;
359
360 if changed {
362 let name = info.name.clone();
363 if let Some(ref hotplug) = self.hotplug {
364 let render_states = hotplug.render_states.clone();
365 let renderer = hotplug.renderer.clone();
366 tokio::spawn(async move {
367 let states = render_states.lock().await;
368 if let Some(rs) = states.get(&name) {
369 let mut lock = rs.lock().await;
370 lock.width = new_width;
371 lock.height = new_height;
372 let surf_config = wgpu::SurfaceConfiguration {
373 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
374 format: lock.format,
375 width: new_width,
376 height: new_height,
377 present_mode: wgpu::PresentMode::Fifo,
378 alpha_mode: wgpu::CompositeAlphaMode::Opaque,
379 view_formats: vec![],
380 desired_maximum_frame_latency: 2,
381 };
382 lock.surface.configure(&renderer.device, &surf_config);
383 tracing::info!(
384 "Hotplug: reconfigured {name} to {new_width}x{new_height}"
385 );
386 }
387 });
388 }
389 }
390 }
391 }
392 fn output_destroyed(
393 &mut self,
394 _conn: &Connection,
395 _qh: &QueueHandle<Self>,
396 output: wl_output::WlOutput,
397 ) {
398 let id = output.id().protocol_id();
399 if let Some(info) = self.outputs.remove(&id) {
400 tracing::info!("Output disconnected: {} (protocol_id={})", info.name, id);
401 self.surfaces.retain(|(pid, _)| *pid != id);
403 if let Some(ref hotplug) = self.hotplug {
405 let name = info.name.clone();
406 let render_states = hotplug.render_states.clone();
407 tokio::spawn(async move {
408 if let Some(rs) = render_states.lock().await.remove(&name) {
409 let lock = rs.lock().await;
410 lock.video_playback.stop();
411 tracing::info!("Hotplug: cleaned up render state for {name}");
412 }
413 });
414 }
415 }
416 }
417}
418
419delegate_compositor!(WaylandState);
420delegate_layer!(WaylandState);
421delegate_output!(WaylandState);
422delegate_registry!(WaylandState);
423delegate_shm!(WaylandState);
424
425struct LivePacer {
427 lock: std::sync::Mutex<()>,
428 cond: std::sync::Condvar,
429}
430
431impl LivePacer {
432 fn new() -> Self {
433 Self {
434 lock: std::sync::Mutex::new(()),
435 cond: std::sync::Condvar::new(),
436 }
437 }
438
439 fn notify(&self) {
440 let _guard = self.lock.lock().unwrap();
441 self.cond.notify_all();
442 }
443
444 fn wait_until(&self, deadline: std::time::Instant) {
447 let guard = self.lock.lock().unwrap();
448 let now = std::time::Instant::now();
449 if deadline <= now {
450 return;
451 }
452 let _ = self
453 .cond
454 .wait_timeout_while(guard, deadline - now, |_| true);
455 }
456}
457
458struct RenderState {
459 renderer: std::sync::Arc<Renderer>,
460 surface: &'static wgpu::Surface<'static>,
461 render_lock: std::sync::Arc<std::sync::Mutex<()>>,
465 playback_gen: std::sync::Arc<std::sync::atomic::AtomicU64>,
468 pacer: std::sync::Arc<LivePacer>,
471 current_bind: Option<wgpu::BindGroup>,
472 current_tex: Option<wgpu::Texture>,
473 width: u32,
474 height: u32,
475 current_width: u32,
476 current_height: u32,
477 format: wgpu::TextureFormat,
478 video_playback: std::sync::Arc<crate::video::VideoPlayback>,
480 hw_accel: crate::video::HwAccel,
482 scaling_mode: u32,
484 per_output_uniforms: std::sync::Arc<crate::renderer::PerOutputUniforms>,
486 last_wallpaper: Option<std::path::PathBuf>,
488 pre_blank: Option<(std::path::PathBuf, u32)>,
490 blanked: bool,
492 gif_paused: std::sync::Arc<std::sync::atomic::AtomicBool>,
494}
495
496struct CommitData {
499 bg_bind: wgpu::BindGroup,
500 new_bind: wgpu::BindGroup,
501 img_width: u32,
502 img_height: u32,
503 old_img_width: u32,
504 old_img_height: u32,
505 format: wgpu::TextureFormat,
506 width: u32,
507 height: u32,
508 animated: Option<crate::animated::AnimatedImage>,
511 is_video: bool,
513 generation: u64,
516 scaling_mode: u32,
518}
519
520impl RenderState {
521 async fn set_wallpaper(
522 &mut self,
523 path: &std::path::Path,
524 effect: &crate::animation::Effect,
525 duration_ms: u32,
526 scaling_mode: u32,
527 ) -> anyhow::Result<()> {
528 self.scaling_mode = scaling_mode;
529 let commit = self.commit_wallpaper(path, scaling_mode)?;
530 self.spawn_transition(commit, effect, duration_ms);
531 self.last_wallpaper = Some(path.to_path_buf());
533 Ok(())
534 }
535
536 fn commit_wallpaper(
540 &mut self,
541 path: &std::path::Path,
542 scaling_mode: u32,
543 ) -> anyhow::Result<CommitData> {
544 use image::ImageReader;
545
546 if crate::video::VideoDecoder::is_video_file(path) {
548 tracing::info!("Video file detected: {:?}", path);
549
550 let generation = self.playback_gen.fetch_add(1, Ordering::SeqCst) + 1;
556 self.pacer.notify();
557
558 let metadata = self.video_playback.start(path, self.hw_accel)?;
561
562 let first_frame = self
565 .video_playback
566 .wait_first_frame(std::time::Duration::from_millis(1000));
567
568 let (new_tex, new_bind, img_width, img_height) = if let Some(frame) = first_frame {
569 let (tex, bind) = self.renderer.create_texture(frame.width, frame.height);
570 self.renderer
571 .update_texture(&tex, &frame.data, frame.width, frame.height);
572 (tex, bind, frame.width, frame.height)
573 } else {
574 tracing::warn!("No first frame available, using black texture");
576 let (tex, bind) = self
577 .renderer
578 .create_texture(metadata.width, metadata.height);
579 let black = vec![0u8; (metadata.width * metadata.height * 4) as usize];
580 self.renderer
581 .update_texture(&tex, &black, metadata.width, metadata.height);
582 (tex, bind, metadata.width, metadata.height)
583 };
584
585 let old_bind = self.current_bind.take();
586 let (old_img_width, old_img_height) = if old_bind.is_some() {
587 (self.current_width.max(1), self.current_height.max(1))
588 } else {
589 (img_width, img_height)
590 };
591 let bg_bind = old_bind.unwrap_or_else(|| new_bind.clone());
592
593 drop(self.current_tex.take());
594 self.current_tex = Some(new_tex);
595 self.current_bind = Some(new_bind.clone());
596 self.current_width = img_width;
597 self.current_height = img_height;
598
599 return Ok(CommitData {
600 bg_bind,
601 new_bind,
602 img_width,
603 img_height,
604 old_img_width,
605 old_img_height,
606 format: self.format,
607 width: self.width,
608 height: self.height,
609 animated: None,
610 is_video: true,
611 generation,
612 scaling_mode,
613 });
614 }
615
616 self.video_playback.stop();
620
621 let mut animated = crate::animated::AnimatedImage::decode(path)?;
624 let (new_tex, new_bind, img_width, img_height) = if let Some(anim) = animated.as_mut() {
625 let (w, h) = (anim.width, anim.height);
626 let (tex, bind) = self.renderer.create_texture(w, h);
627 let first = anim.first_frame();
628 if !first.is_empty() {
629 self.renderer.update_texture(&tex, first, w, h);
630 }
631 (tex, bind, w, h)
632 } else {
633 let new_img = ImageReader::open(path)?.decode()?;
634 let (tex, bind) = self.renderer.load_texture(&new_img)?;
635 (tex, bind, new_img.width(), new_img.height())
636 };
637
638 let old_bind = self.current_bind.take();
639 let (old_img_width, old_img_height) = if old_bind.is_some() {
640 (self.current_width.max(1), self.current_height.max(1))
641 } else {
642 (img_width, img_height)
643 };
644 let bg_bind = old_bind.unwrap_or_else(|| new_bind.clone());
649
650 drop(self.current_tex.take());
651 self.current_tex = Some(new_tex);
652 self.current_bind = Some(new_bind.clone());
653 self.current_width = img_width;
654 self.current_height = img_height;
655
656 let generation = self.playback_gen.fetch_add(1, Ordering::SeqCst) + 1;
657 self.pacer.notify();
658
659 Ok(CommitData {
660 bg_bind,
661 new_bind,
662 img_width,
663 img_height,
664 old_img_width,
665 old_img_height,
666 format: self.format,
667 width: self.width,
668 height: self.height,
669 animated,
670 is_video: false,
671 generation,
672 scaling_mode,
673 })
674 }
675
676 fn spawn_transition(
681 &self,
682 commit: CommitData,
683 effect: &crate::animation::Effect,
684 duration_ms: u32,
685 ) {
686 let renderer = self.renderer.clone();
687 let surface: &'static wgpu::Surface<'static> = self.surface;
688 let render_lock = self.render_lock.clone();
689 let playback_gen = self.playback_gen.clone();
690 let pacer = self.pacer.clone();
691 let video_playback = self.video_playback.clone();
692 let per_output_uniforms = std::sync::Arc::clone(&self.per_output_uniforms);
693 let gif_paused = self.gif_paused.clone();
694 let effect = effect.clone();
695 drop(tokio::task::spawn_blocking(move || {
696 render_transition(
697 renderer,
698 surface,
699 render_lock,
700 playback_gen,
701 pacer,
702 video_playback,
703 gif_paused,
704 commit,
705 effect,
706 duration_ms,
707 &per_output_uniforms,
708 );
709 }));
710 }
711}
712
713#[allow(clippy::too_many_arguments)]
721fn render_transition(
722 renderer: std::sync::Arc<Renderer>,
723 surface: &'static wgpu::Surface<'static>,
724 render_lock: std::sync::Arc<std::sync::Mutex<()>>,
725 playback_gen: std::sync::Arc<std::sync::atomic::AtomicU64>,
726 pacer: std::sync::Arc<LivePacer>,
727 video_playback: std::sync::Arc<crate::video::VideoPlayback>,
728 gif_paused: std::sync::Arc<std::sync::atomic::AtomicBool>,
729 mut commit: CommitData,
730 effect: crate::animation::Effect,
731 duration_ms: u32,
732 per_output_uniforms: &crate::renderer::PerOutputUniforms,
733) {
734 let _guard = render_lock
735 .lock()
736 .unwrap_or_else(|poisoned| poisoned.into_inner());
737
738 let duration = std::time::Duration::from_millis(u64::from(duration_ms.max(1)));
739 let start = std::time::Instant::now();
740 loop {
741 let progress = start.elapsed().as_secs_f32() / duration.as_secs_f32();
742 let uniforms = crate::animation::compute_effect_uniforms(&effect, progress.clamp(0.0, 1.0));
743 let status = renderer.render_frame(
744 crate::renderer::FrameRequest {
745 surface,
746 format: commit.format,
747 bg_bind: &commit.bg_bind,
748 new_bind: &commit.new_bind,
749 effect: &uniforms,
750 width: commit.width,
751 height: commit.height,
752 img_width: commit.img_width,
753 img_height: commit.img_height,
754 old_img_width: commit.old_img_width,
755 old_img_height: commit.old_img_height,
756 scaling_mode: commit.scaling_mode,
757 },
758 per_output_uniforms,
759 );
760 let status = match status {
761 Ok(status) => status,
762 Err(err) => {
763 eprintln!("wallr: transition render failed: {err}");
764 break;
765 }
766 };
767 if progress >= 1.0 || status == crate::renderer::FrameStatus::TimedOut {
768 break;
769 }
770 }
771
772 let mut animated = commit.animated.take();
776 if let Some(animated) = animated.as_mut()
777 && playback_gen.load(Ordering::SeqCst) == commit.generation
778 {
779 play_live(
780 &renderer,
781 surface,
782 &commit,
783 animated,
784 &playback_gen,
785 &pacer,
786 &gif_paused,
787 per_output_uniforms,
788 );
789 } else if commit.is_video && playback_gen.load(Ordering::SeqCst) == commit.generation {
790 play_video(
791 &renderer,
792 surface,
793 &commit,
794 &video_playback,
795 &playback_gen,
796 per_output_uniforms,
797 );
798 }
799}
800
801#[allow(clippy::too_many_arguments)]
807fn play_live(
808 renderer: &Renderer,
809 surface: &'static wgpu::Surface<'static>,
810 commit: &CommitData,
811 animated: &mut crate::animated::AnimatedImage,
812 playback_gen: &std::sync::atomic::AtomicU64,
813 pacer: &LivePacer,
814 gif_paused: &std::sync::Arc<std::sync::atomic::AtomicBool>,
815 per_output_uniforms: &crate::renderer::PerOutputUniforms,
816) {
817 let (tex_a, bind_a) = renderer.create_texture(animated.width, animated.height);
818 let (tex_b, bind_b) = renderer.create_texture(animated.width, animated.height);
819 let (frame_w, frame_h) = (animated.width, animated.height);
820 let (bytes_per_row, rows) = (frame_w * 4, frame_h);
821 let frame_bytes = bytes_per_row as u64 * rows as u64;
822
823 let direct_upload = bytes_per_row % 256 == 0;
826 let staging: Vec<wgpu::Buffer> = if direct_upload {
827 (0..2)
828 .map(|_| {
829 renderer.device.create_buffer(&wgpu::BufferDescriptor {
830 label: Some("wallr-gif-staging"),
831 size: frame_bytes,
832 usage: wgpu::BufferUsages::MAP_WRITE | wgpu::BufferUsages::COPY_SRC,
833 mapped_at_creation: false,
834 })
835 })
836 .collect()
837 } else {
838 Vec::new()
839 };
840
841 let first = animated.first_frame();
842 if !first.is_empty() {
843 renderer.update_texture(&tex_a, first, frame_w, frame_h);
844 renderer.update_texture(&tex_b, first, frame_w, frame_h);
845 }
846 let binds = [bind_a, bind_b];
847 let textures = [tex_a, tex_b];
848
849 let upload = |renderer: &Renderer,
852 tgt: usize,
853 index: usize,
854 slot: usize,
855 animated: &mut crate::animated::AnimatedImage|
856 -> bool {
857 if direct_upload {
858 let buffer = &staging[slot];
859 let slice = buffer.slice(..);
860 slice.map_async(wgpu::MapMode::Write, |_| {});
861 renderer.device.poll(wgpu::Maintain::Wait);
862 let ok = {
863 let mut mapped = slice.get_mapped_range_mut();
864 animated.decompress_into(index, &mut mapped)
865 };
866 buffer.unmap();
867 if ok {
868 let mut encoder = renderer
869 .device
870 .create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
871 encoder.copy_buffer_to_texture(
872 wgpu::TexelCopyBufferInfo {
873 buffer,
874 layout: wgpu::TexelCopyBufferLayout {
875 offset: 0,
876 bytes_per_row: Some(bytes_per_row),
877 rows_per_image: Some(rows),
878 },
879 },
880 wgpu::TexelCopyTextureInfo {
881 texture: &textures[tgt],
882 mip_level: 0,
883 origin: wgpu::Origin3d::ZERO,
884 aspect: wgpu::TextureAspect::All,
885 },
886 wgpu::Extent3d {
887 width: frame_w,
888 height: frame_h,
889 depth_or_array_layers: 1,
890 },
891 );
892 renderer.queue.submit([encoder.finish()]);
893 return true;
894 }
895 } else {
896 let frame = animated.frame_at(index);
897 if !frame.is_empty() {
898 renderer.update_texture(&textures[tgt], frame, frame_w, frame_h);
899 return true;
900 }
901 }
902 false
903 };
904
905 let mut cur = 0usize; let mut cur_frame = 0usize; let mut next_frame = 0usize; let mut slot = 0usize; let start = std::time::Instant::now();
910 let mut paused_elapsed = std::time::Duration::ZERO; let static_effect = crate::animation::Effect::Fade(crate::animation::FadeParams::default());
912 loop {
913 if playback_gen.load(Ordering::SeqCst) != commit.generation {
914 return;
915 }
916
917 if gif_paused.load(Ordering::SeqCst) {
919 let pause_start = std::time::Instant::now();
920 let uniforms = crate::animation::compute_effect_uniforms(&static_effect, 1.0);
922 let status = renderer.render_frame(
923 crate::renderer::FrameRequest {
924 surface,
925 format: commit.format,
926 bg_bind: &binds[cur],
927 new_bind: &binds[cur],
928 effect: &uniforms,
929 width: commit.width,
930 height: commit.height,
931 img_width: animated.width,
932 img_height: animated.height,
933 old_img_width: animated.width,
934 old_img_height: animated.height,
935 scaling_mode: commit.scaling_mode,
936 },
937 per_output_uniforms,
938 );
939 match status {
940 Ok(crate::renderer::FrameStatus::Presented) => {}
941 _ => return,
942 }
943 std::thread::sleep(std::time::Duration::from_millis(16));
945 paused_elapsed += pause_start.elapsed();
946 continue;
947 }
948
949 let index = animated.frame_index_at(start.elapsed() - paused_elapsed);
950 if index != cur_frame {
951 if next_frame != index {
952 upload(renderer, cur ^ 1, index, slot, animated);
953 slot ^= 1;
954 next_frame = index;
955 }
956 cur ^= 1;
957 cur_frame = index;
958 }
959 let uniforms = crate::animation::compute_effect_uniforms(&static_effect, 1.0);
960 let status = renderer.render_frame(
961 crate::renderer::FrameRequest {
962 surface,
963 format: commit.format,
964 bg_bind: &binds[cur],
965 new_bind: &binds[cur],
966 effect: &uniforms,
967 width: commit.width,
968 height: commit.height,
969 img_width: animated.width,
970 img_height: animated.height,
971 old_img_width: animated.width,
972 old_img_height: animated.height,
973 scaling_mode: commit.scaling_mode,
974 },
975 per_output_uniforms,
976 );
977 match status {
978 Ok(crate::renderer::FrameStatus::Presented) => {}
979 _ => return,
980 }
981
982 let elapsed = start.elapsed() - paused_elapsed;
990 let total: std::time::Duration = animated.total_duration();
991 let loops = (elapsed.as_millis() / total.as_millis().max(1)) as u64;
992 let next_change = animated.frame_start(index + 1) + total * (loops as u32);
993 let wait = next_change.saturating_sub(elapsed);
994 if wait > std::time::Duration::ZERO {
995 let next = index + 1;
996 if next_frame != next {
997 upload(renderer, cur ^ 1, next, slot, animated);
998 slot ^= 1;
999 next_frame = next;
1000 }
1001 pacer.wait_until(std::time::Instant::now() + wait);
1002 }
1003 }
1004}
1005
1006fn play_video(
1008 renderer: &Renderer,
1009 surface: &'static wgpu::Surface<'static>,
1010 commit: &CommitData,
1011 video_playback: &std::sync::Arc<crate::video::VideoPlayback>,
1012 playback_gen: &std::sync::atomic::AtomicU64,
1013 per_output_uniforms: &crate::renderer::PerOutputUniforms,
1014) {
1015 let (width, height) = match video_playback.metadata() {
1017 Some(meta) => (meta.width, meta.height),
1018 None => {
1019 tracing::warn!("No video metadata available");
1020 return;
1021 }
1022 };
1023
1024 let (texture, bind) = renderer.create_texture(width, height);
1025 let static_effect = crate::animation::Effect::Fade(crate::animation::FadeParams::default());
1026
1027 let mut uploaded = false;
1030
1031 loop {
1032 if playback_gen.load(Ordering::SeqCst) != commit.generation {
1037 return;
1038 }
1039
1040 if let Some(frame) = video_playback.next_frame() {
1043 if frame.width != width || frame.height != height {
1046 continue;
1047 }
1048 renderer.update_texture(&texture, &frame.data, frame.width, frame.height);
1049 uploaded = true;
1050 }
1051
1052 if !uploaded {
1053 std::thread::sleep(std::time::Duration::from_millis(2));
1056 continue;
1057 }
1058
1059 let uniforms = crate::animation::compute_effect_uniforms(&static_effect, 1.0);
1060 let status = renderer.render_frame(
1061 crate::renderer::FrameRequest {
1062 surface,
1063 format: commit.format,
1064 bg_bind: &bind,
1065 new_bind: &bind,
1066 effect: &uniforms,
1067 width: commit.width,
1068 height: commit.height,
1069 img_width: width,
1070 img_height: height,
1071 old_img_width: width,
1072 old_img_height: height,
1073 scaling_mode: commit.scaling_mode,
1074 },
1075 per_output_uniforms,
1076 );
1077
1078 match status {
1079 Ok(crate::renderer::FrameStatus::Presented) => {}
1080 other => {
1083 tracing::warn!("Video present failed ({:?}), stopping playback", other);
1084 video_playback.stop();
1085 return;
1086 }
1087 }
1088 }
1089}
1090
1091async fn resolve_targets(
1096 render_states: &std::collections::HashMap<
1097 String,
1098 std::sync::Arc<tokio::sync::Mutex<RenderState>>,
1099 >,
1100 monitor: Option<&str>,
1101) -> Vec<std::sync::Arc<tokio::sync::Mutex<RenderState>>> {
1102 match monitor {
1103 Some(name) => {
1104 if let Some(rs) = render_states.get(name) {
1105 vec![rs.clone()]
1106 } else {
1107 Vec::new()
1108 }
1109 }
1110 None => render_states.values().cloned().collect(),
1111 }
1112}
1113
1114pub struct Daemon {
1115 config: WallrConfig,
1116 paused: Arc<AtomicBool>,
1117 engine: Arc<Mutex<WallpaperEngine>>,
1118}
1119
1120impl Daemon {
1121 pub fn new(config: WallrConfig) -> Result<Self, DaemonError> {
1122 let engine = WallpaperEngine::new(config.clone())?;
1123 Ok(Self {
1124 config,
1125 paused: Arc::new(AtomicBool::new(false)),
1126 engine: Arc::new(Mutex::new(engine)),
1127 })
1128 }
1129
1130 pub async fn start(self) -> Result<(), DaemonError> {
1131 let socket_path = crate::config::expand_path(&self.config.daemon.socket);
1132 if socket_path.exists() {
1133 if tokio::net::UnixStream::connect(&socket_path).await.is_ok() {
1134 return Err(DaemonError::AlreadyRunning(
1135 socket_path.to_string_lossy().to_string(),
1136 ));
1137 }
1138 let _ = std::fs::remove_file(&socket_path);
1139 }
1140
1141 let renderer = Renderer::new()
1142 .await
1143 .map_err(|e| DaemonError::StartError(format!("GPU init failed: {e}")))?;
1144
1145 let conn = Connection::connect_to_env()
1146 .map_err(|e| DaemonError::StartError(format!("Failed to connect to Wayland: {e:?}")))?;
1147 let backend = conn.backend();
1148 let display_ptr = backend.display_ptr() as *mut std::ffi::c_void;
1149
1150 let (globals, mut event_queue) = registry_queue_init(&conn)
1151 .map_err(|e| DaemonError::StartError(format!("registry_queue_init failed: {e:?}")))?;
1152 let qh = event_queue.handle();
1153
1154 let compositor_state = CompositorState::bind(&globals, &qh)
1155 .map_err(|e| DaemonError::StartError(format!("compositor bind failed: {e:?}")))?;
1156 let layer_shell = LayerShell::bind(&globals, &qh)
1157 .map_err(|e| DaemonError::StartError(format!("layer_shell bind failed: {e:?}")))?;
1158 let shm = Shm::bind(&globals, &qh)
1159 .map_err(|e| DaemonError::StartError(format!("shm bind failed: {e:?}")))?;
1160
1161 let compositor = globals
1163 .bind::<wl_compositor::WlCompositor, WaylandState, smithay_client_toolkit::globals::GlobalData>(
1164 &qh,
1165 1..=4,
1166 smithay_client_toolkit::globals::GlobalData,
1167 )
1168 .map_err(|e| DaemonError::StartError(format!("compositor bind failed: {e:?}")))?;
1169
1170 let mut wayland_state = WaylandState {
1171 registry_state: RegistryState::new(&globals),
1172 output_state: OutputState::new(&globals, &qh),
1173 compositor_state,
1174 shm,
1175 outputs: std::collections::HashMap::new(),
1176 surfaces: Vec::new(),
1177 layer_shell,
1178 compositor,
1179 hotplug: None,
1180 };
1181
1182 for i in 0..5 {
1186 event_queue
1187 .roundtrip(&mut wayland_state)
1188 .map_err(|e| DaemonError::StartError(format!("roundtrip {i} failed: {e:?}")))?;
1189 }
1190
1191 if wayland_state.outputs.is_empty() {
1192 return Err(DaemonError::StartError(
1193 "no outputs detected after roundtrip".into(),
1194 ));
1195 }
1196
1197 tracing::info!(
1198 "Detected {} output(s): {:?}",
1199 wayland_state.outputs.len(),
1200 wayland_state
1201 .outputs
1202 .values()
1203 .map(|o| format!("{} ({}x{})", o.name, o.width, o.height))
1204 .collect::<Vec<_>>()
1205 );
1206
1207 let renderer = std::sync::Arc::new(renderer);
1208
1209 let mut render_states_map: std::collections::HashMap<String, Arc<Mutex<RenderState>>> =
1213 std::collections::HashMap::new();
1214
1215 let output_info: Vec<(u32, OutputInfo)> = wayland_state
1218 .outputs
1219 .iter()
1220 .map(|(k, v)| {
1221 (
1222 *k,
1223 OutputInfo {
1224 name: v.name.clone(),
1225 width: v.width,
1226 height: v.height,
1227 scale_factor: v.scale_factor,
1228 wl_output: v.wl_output.clone(),
1229 },
1230 )
1231 })
1232 .collect();
1233
1234 for (proto_id, info) in &output_info {
1235 let name = info.name.clone();
1236 let rs = Self::create_render_state_for_output(
1237 &renderer,
1238 display_ptr,
1239 &mut wayland_state,
1240 &qh,
1241 info,
1242 &self.config,
1243 )
1244 .await?;
1245 let rs = Arc::new(Mutex::new(rs));
1246 render_states_map.insert(name.clone(), rs.clone());
1247
1248 let state_path = dirs::cache_dir()
1250 .unwrap_or_else(|| std::path::PathBuf::from("/tmp"))
1251 .join(format!("wallr/last_wallpaper/{name}"));
1252 if let Ok(path_str) = std::fs::read_to_string(&state_path) {
1253 let p = std::path::Path::new(path_str.trim());
1254 if p.exists() {
1255 let mut lock = rs.lock().await;
1256 let effect =
1257 crate::animation::Effect::Fade(crate::animation::FadeParams::default());
1258 let _ = lock.set_wallpaper(p, &effect, 1000, 0).await;
1259 }
1260 }
1261
1262 tracing::info!("Output ready: {name} ({proto_id})");
1263 }
1264
1265 let render_states: std::sync::Arc<
1268 tokio::sync::Mutex<std::collections::HashMap<String, Arc<Mutex<RenderState>>>>,
1269 > = std::sync::Arc::new(tokio::sync::Mutex::new(render_states_map));
1270
1271 wayland_state.hotplug = Some(DaemonHotplug {
1274 renderer: renderer.clone(),
1275 config: self.config.clone(),
1276 display_ptr: SendDisplayPtr(display_ptr),
1277 render_states: render_states.clone(),
1278 });
1279
1280 event_queue
1283 .roundtrip(&mut wayland_state)
1284 .map_err(|e| DaemonError::StartError(format!("hotplug roundtrip failed: {e:?}")))?;
1285
1286 let paused_clone = self.paused.clone();
1287 let engine_clone = self.engine.clone();
1288 let render_states_clone = render_states.clone();
1289
1290 {
1294 let rs_map = render_states.clone();
1295 let socket_path = socket_path.clone();
1296 tokio::spawn(async move {
1297 use tokio::signal::unix::{SignalKind, signal};
1298 let mut term = signal(SignalKind::terminate()).expect("SIGTERM handler");
1299 let mut int = signal(SignalKind::interrupt()).expect("SIGINT handler");
1300 let mut hup = signal(SignalKind::hangup()).expect("SIGHUP handler");
1301 tokio::select! {
1302 _ = term.recv() => {}
1303 _ = int.recv() => {}
1304 _ = hup.recv() => {}
1305 }
1306 tracing::info!("Signal received, shutting down gracefully");
1307 let states = rs_map.lock().await;
1308 for rs in states.values() {
1309 if let Ok(state) = rs.try_lock() {
1310 state.video_playback.stop();
1311 }
1312 }
1313 drop(states);
1314 let _ = std::fs::remove_file(&socket_path);
1315 std::process::exit(0);
1316 });
1317 }
1318
1319 let ipc_socket_path = socket_path.clone();
1320 start_ipc_server(&socket_path, move |cmd| {
1321 let paused = paused_clone.clone();
1322 let engine = engine_clone.clone();
1323 let render_states = render_states_clone.clone();
1324 let stop_socket = ipc_socket_path.clone();
1325 async move {
1326 let render_states = render_states.lock().await;
1327 match cmd {
1328 IpcCommand::Pause { monitor } => {
1329 let targets = resolve_targets(&render_states, monitor.as_deref()).await;
1330 if targets.is_empty() {
1331 return IpcResponse {
1332 success: false,
1333 message: Some("No matching outputs".into()),
1334 };
1335 }
1336 if monitor.is_none() {
1337 paused.store(true, Ordering::SeqCst);
1338 }
1339 for rs in targets {
1340 let rs_lock = rs.lock().await;
1341 rs_lock.video_playback.pause();
1342 rs_lock.gif_paused.store(true, Ordering::SeqCst);
1343 }
1344 IpcResponse {
1345 success: true,
1346 message: Some("Paused".into()),
1347 }
1348 }
1349 IpcCommand::Resume { monitor } => {
1350 let targets = resolve_targets(&render_states, monitor.as_deref()).await;
1351 if targets.is_empty() {
1352 return IpcResponse {
1353 success: false,
1354 message: Some("No matching outputs".into()),
1355 };
1356 }
1357 if monitor.is_none() {
1358 paused.store(false, Ordering::SeqCst);
1359 }
1360 for rs in targets {
1361 let rs_lock = rs.lock().await;
1362 rs_lock.video_playback.resume();
1363 rs_lock.gif_paused.store(false, Ordering::SeqCst);
1364 }
1365 IpcResponse {
1366 success: true,
1367 message: Some("Resumed".into()),
1368 }
1369 }
1370 IpcCommand::Reload => {
1371 let lock = engine.lock().await;
1372 match lock.reload() {
1373 Ok(_) => IpcResponse {
1374 success: true,
1375 message: Some("Reloaded".into()),
1376 },
1377 Err(e) => IpcResponse {
1378 success: false,
1379 message: Some(e.to_string()),
1380 },
1381 }
1382 }
1383 IpcCommand::Preview {
1384 path,
1385 effect,
1386 duration_ms,
1387 no_theme,
1388 theme_override,
1389 monitor,
1390 scaling_mode,
1391 } => {
1392 if paused.load(Ordering::SeqCst) {
1393 return IpcResponse {
1394 success: false,
1395 message: Some("Daemon is paused".into()),
1396 };
1397 }
1398 let p = std::path::PathBuf::from(&path);
1399 if !p.exists() {
1400 return IpcResponse {
1401 success: false,
1402 message: Some(format!("File not found: {}", path)),
1403 };
1404 }
1405
1406 let targets = resolve_targets(&render_states, monitor.as_deref()).await;
1408 if targets.is_empty() {
1409 return IpcResponse {
1410 success: false,
1411 message: match &monitor {
1412 Some(name) => Some(format!("Unknown monitor: {name}")),
1413 None => Some("No outputs available".into()),
1414 },
1415 };
1416 }
1417
1418 {
1420 let persist_names: Vec<&str> = match &monitor {
1421 Some(name) => vec![name.as_str()],
1422 None => render_states.keys().map(|s| s.as_str()).collect(),
1423 };
1424 for name in persist_names {
1425 let state_path = dirs::cache_dir()
1426 .unwrap_or_else(|| std::path::PathBuf::from("/tmp"))
1427 .join(format!("wallr/last_wallpaper/{name}"));
1428 if let Some(parent) = state_path.parent() {
1429 let _ = std::fs::create_dir_all(parent);
1430 }
1431 let _ = std::fs::write(&state_path, &path);
1432 }
1433 }
1434
1435 let effect = effect.unwrap_or_else(|| {
1436 crate::animation::Effect::Fade(crate::animation::FadeParams::default())
1437 });
1438 let is_video = crate::video::VideoDecoder::is_video_file(&p);
1442 let duration = duration_ms.unwrap_or(if is_video { 150 } else { 2000 });
1443 let sm = scaling_mode.unwrap_or(crate::config::ScalingMode::Fill);
1444 let scaling_mode_u32 = match sm {
1445 crate::config::ScalingMode::Fill => 0u32,
1446 crate::config::ScalingMode::Fit => 1,
1447 crate::config::ScalingMode::Stretch => 2,
1448 crate::config::ScalingMode::Center => 3,
1449 crate::config::ScalingMode::Tile => 4,
1450 };
1451
1452 let mut last_err = None;
1453 for rs in &targets {
1454 let rs_clone = rs.clone();
1455 let p_clone = p.clone();
1456 let effect_clone = effect.clone();
1457 let result = tokio::task::spawn_blocking(move || {
1458 let rt = tokio::runtime::Handle::current();
1459 rt.block_on(async {
1460 let mut lock = rs_clone.lock().await;
1461 lock.set_wallpaper(
1462 &p_clone,
1463 &effect_clone,
1464 duration,
1465 scaling_mode_u32,
1466 )
1467 .await
1468 })
1469 })
1470 .await;
1471
1472 if let Err(e) = result {
1473 last_err = Some(format!("Task spawn failed: {e}"));
1474 continue;
1475 }
1476 if let Err(e) = result.unwrap() {
1477 last_err = Some(format!("Render failed: {e}"));
1478 }
1479 }
1480
1481 match last_err {
1482 Some(e) => IpcResponse {
1483 success: false,
1484 message: Some(e),
1485 },
1486 None => {
1487 let opts = SetOptions {
1488 no_theme,
1489 theme_provider: theme_override,
1490 monitor,
1491 };
1492 let mut eng = engine.lock().await;
1493 match eng.set_wallpaper(&p, &opts).await {
1494 Ok(()) => IpcResponse {
1495 success: true,
1496 message: None,
1497 },
1498 Err(e) => IpcResponse {
1499 success: true,
1500 message: Some(format!(
1501 "Wallpaper set, but hooks/theme failed: {e}"
1502 )),
1503 },
1504 }
1505 }
1506 }
1507 }
1508 IpcCommand::Stop => {
1509 let sp = stop_socket.clone();
1510 tokio::spawn(async move {
1511 tokio::time::sleep(std::time::Duration::from_millis(300)).await;
1512 let _ = std::fs::remove_file(&sp);
1513 std::process::exit(0);
1514 });
1515 IpcResponse {
1516 success: true,
1517 message: Some("Stopping".into()),
1518 }
1519 }
1520 IpcCommand::Status => {
1521 let state = if paused.load(Ordering::SeqCst) {
1522 "paused"
1523 } else {
1524 "running"
1525 };
1526 IpcResponse {
1527 success: true,
1528 message: Some(format!("wallr daemon {}", state)),
1529 }
1530 }
1531 IpcCommand::Seek {
1532 timestamp_ms,
1533 monitor,
1534 } => {
1535 let targets = resolve_targets(&render_states, monitor.as_deref()).await;
1536 if targets.is_empty() {
1537 return IpcResponse {
1538 success: false,
1539 message: match &monitor {
1540 Some(name) => Some(format!("Unknown monitor: {name}")),
1541 None => Some("No outputs available".into()),
1542 },
1543 };
1544 }
1545 let mut seek_count = 0u32;
1547 let mut errors = Vec::new();
1548 for (name, rs) in render_states.iter() {
1549 if monitor.as_deref() != Some(name.as_str()) && monitor.is_some() {
1550 continue;
1551 }
1552 let rs_lock = rs.lock().await;
1553 match rs_lock
1554 .video_playback
1555 .seek(std::time::Duration::from_millis(timestamp_ms))
1556 {
1557 Ok(()) => {
1558 seek_count += 1;
1559 }
1560 Err(e) => {
1561 errors.push(format!("{}: {}", name, e));
1562 }
1563 }
1564 }
1565 if seek_count == 0 {
1566 IpcResponse {
1567 success: false,
1568 message: Some(format!(
1569 "Seek failed on all outputs: {}",
1570 errors.join("; ")
1571 )),
1572 }
1573 } else if !errors.is_empty() {
1574 IpcResponse {
1575 success: true,
1576 message: Some(format!(
1577 "Seeked {} output(s) to {}ms, {} failed: {}",
1578 seek_count,
1579 timestamp_ms,
1580 errors.len(),
1581 errors.join("; ")
1582 )),
1583 }
1584 } else {
1585 IpcResponse {
1586 success: true,
1587 message: Some(format!(
1588 "Seeked {} output(s) to {}ms",
1589 seek_count, timestamp_ms
1590 )),
1591 }
1592 }
1593 }
1594 IpcCommand::Info { monitor } => {
1595 let targets = resolve_targets(&render_states, monitor.as_deref()).await;
1596 if targets.is_empty() {
1597 return IpcResponse {
1598 success: false,
1599 message: match &monitor {
1600 Some(name) => Some(format!("Unknown monitor: {name}")),
1601 None => Some("No outputs available".into()),
1602 },
1603 };
1604 }
1605
1606 let mut lines = vec![
1607 format!("wallr v{}", env!("CARGO_PKG_VERSION")),
1608 String::new(),
1609 format!("Outputs: {}", render_states.len()),
1610 ];
1611 for name in render_states.keys() {
1612 lines.push(format!(" - {name}"));
1613 }
1614
1615 for (name, rs) in render_states.iter() {
1617 if monitor.is_some() && monitor.as_deref() != Some(name.as_str()) {
1618 continue;
1619 }
1620 let rs_lock = rs.lock().await;
1621 let gpu_info =
1622 crate::video::gpu::adapter_diagnostics(&rs_lock.renderer.adapter);
1623
1624 lines.push(String::new());
1625 lines.push(format!("[{name}] {}x{}", rs_lock.width, rs_lock.height));
1626 lines.push(gpu_info);
1627
1628 match rs_lock.video_playback.metadata() {
1629 Some(meta) => {
1630 let decoder_info = rs_lock.video_playback.decoder_info();
1631 let hw = rs_lock.video_playback.hw_accel_in_use();
1632 let state = if rs_lock.video_playback.is_paused() {
1633 "paused"
1634 } else {
1635 "playing"
1636 };
1637 let position = rs_lock
1638 .video_playback
1639 .position()
1640 .map(|p| format!("{:.2}s", p.as_secs_f64()))
1641 .unwrap_or_else(|| "?".to_string());
1642 lines.push(String::new());
1643 lines.push("Video:".into());
1644 lines.push(format!(
1645 " Resolution: {}x{}",
1646 meta.width, meta.height
1647 ));
1648 lines.push(format!(" FPS: {:.2}", meta.fps));
1649 lines.push(format!(
1650 " Duration: {:.2}s",
1651 meta.duration.as_secs_f64()
1652 ));
1653 lines.push(format!(
1654 " Codec: {}",
1655 decoder_info
1656 .as_ref()
1657 .map(|d| d.codec_name.as_str())
1658 .unwrap_or("unknown")
1659 ));
1660 lines.push(format!(" Container: {}", meta.format));
1661 lines.push(format!(" Decoder: {}", hw.name()));
1662 lines.push(format!(
1663 " GPU Decode: {}",
1664 if hw == crate::video::HwAccel::Software {
1665 "disabled"
1666 } else {
1667 "enabled"
1668 }
1669 ));
1670 lines.push(format!(" State: {} @ {}", state, position));
1671 }
1672 None => {
1673 lines.push(String::new());
1674 lines.push("Video: none active".into());
1675 lines.push("Decoder: idle".into());
1676 }
1677 }
1678 }
1679
1680 IpcResponse {
1681 success: true,
1682 message: Some(lines.join("\n")),
1683 }
1684 }
1685 IpcCommand::MonitorList => {
1686 let mut lines = Vec::new();
1687 for (name, rs) in render_states.iter() {
1688 let lock = rs.lock().await;
1689 lines.push(format!("{}: {}x{}", name, lock.width, lock.height));
1690 }
1691 if lines.is_empty() {
1692 IpcResponse {
1693 success: true,
1694 message: Some("No monitors connected".into()),
1695 }
1696 } else {
1697 IpcResponse {
1698 success: true,
1699 message: Some(lines.join("\n")),
1700 }
1701 }
1702 }
1703 IpcCommand::MonitorCurrent => {
1704 if let Some((name, rs)) = render_states.iter().next() {
1706 let lock = rs.lock().await;
1707 IpcResponse {
1708 success: true,
1709 message: Some(format!("{}: {}x{}", name, lock.width, lock.height)),
1710 }
1711 } else {
1712 IpcResponse {
1713 success: false,
1714 message: Some("No monitors connected".into()),
1715 }
1716 }
1717 }
1718 IpcCommand::Blank {
1719 monitor,
1720 effect,
1721 duration_ms,
1722 } => {
1723 let targets = resolve_targets(&render_states, monitor.as_deref()).await;
1724 if targets.is_empty() {
1725 return IpcResponse {
1726 success: false,
1727 message: match &monitor {
1728 Some(name) => Some(format!("Unknown monitor: {name}")),
1729 None => Some("No outputs available".into()),
1730 },
1731 };
1732 }
1733 let mut blanked_count = 0u32;
1734 let black_effect = effect.unwrap_or_else(|| {
1735 crate::animation::Effect::Fade(crate::animation::FadeParams::default())
1736 });
1737 let duration = duration_ms.unwrap_or(800);
1738
1739 for (name, rs) in render_states.iter() {
1740 if monitor.as_deref() != Some(name.as_str()) && monitor.is_some() {
1741 continue;
1742 }
1743 let mut lock = rs.lock().await;
1744 if lock.blanked {
1745 continue;
1746 }
1747 lock.pre_blank = Some((
1748 lock.last_wallpaper.clone().unwrap_or_default(),
1749 lock.scaling_mode,
1750 ));
1751 lock.blanked = true;
1752 let tmp = std::env::temp_dir().join("wallr_blank.png");
1753 {
1754 let img =
1755 image::RgbaImage::from_pixel(1, 1, image::Rgba([0, 0, 0, 255]));
1756 let _ = img.save(&tmp);
1757 }
1758 let _ = lock.set_wallpaper(&tmp, &black_effect, duration, 0).await;
1759 blanked_count += 1;
1760 }
1761 IpcResponse {
1762 success: true,
1763 message: Some(format!("Blanked {blanked_count} output(s)")),
1764 }
1765 }
1766 IpcCommand::Restore {
1767 monitor,
1768 effect,
1769 duration_ms,
1770 } => {
1771 let targets = resolve_targets(&render_states, monitor.as_deref()).await;
1772 if targets.is_empty() {
1773 return IpcResponse {
1774 success: false,
1775 message: match &monitor {
1776 Some(name) => Some(format!("Unknown monitor: {name}")),
1777 None => Some("No outputs available".into()),
1778 },
1779 };
1780 }
1781 let mut restored_count = 0u32;
1782 let mut errors = Vec::new();
1783 let restore_effect = effect.unwrap_or_else(|| {
1784 crate::animation::Effect::Fade(crate::animation::FadeParams::default())
1785 });
1786 let duration = duration_ms.unwrap_or(800);
1787
1788 for (name, rs) in render_states.iter() {
1789 if monitor.as_deref() != Some(name.as_str()) && monitor.is_some() {
1790 continue;
1791 }
1792 let mut lock = rs.lock().await;
1793 if !lock.blanked {
1794 continue;
1795 }
1796 if let Some((ref path, scaling_mode)) = lock.pre_blank.clone() {
1797 if !path.exists() {
1798 errors
1799 .push(format!("{}: wallpaper path no longer exists", name));
1800 lock.blanked = false;
1801 lock.pre_blank = None;
1802 continue;
1803 }
1804 match lock
1805 .set_wallpaper(
1806 std::path::Path::new(&path),
1807 &restore_effect,
1808 duration,
1809 scaling_mode,
1810 )
1811 .await
1812 {
1813 Ok(_) => {
1814 lock.blanked = false;
1815 lock.pre_blank = None;
1816 restored_count += 1;
1817 }
1818 Err(e) => {
1819 errors.push(format!("{}: restore failed: {}", name, e));
1820 lock.blanked = false;
1821 lock.pre_blank = None;
1822 }
1823 }
1824 } else {
1825 errors.push(format!("{}: no previous wallpaper to restore", name));
1826 lock.blanked = false;
1827 }
1828 }
1829 if !errors.is_empty() {
1830 IpcResponse {
1831 success: restored_count > 0,
1832 message: Some(format!(
1833 "Restored {} output(s), {} error(s): {}",
1834 restored_count,
1835 errors.len(),
1836 errors.join("; ")
1837 )),
1838 }
1839 } else {
1840 IpcResponse {
1841 success: true,
1842 message: Some(format!("Restored {restored_count} output(s)")),
1843 }
1844 }
1845 }
1846 }
1847 }
1848 })
1849 .await?;
1850
1851 if self.config.watch.enabled
1853 && let Some(ref watch_dir) = self.config.watch.dir
1854 {
1855 let watch_path = crate::config::expand_path(watch_dir);
1856 self.start_watcher(watch_path, render_states.clone())
1857 .await?;
1858 }
1859
1860 tokio::task::spawn_blocking(move || {
1861 loop {
1862 if let Err(e) = event_queue.blocking_dispatch(&mut wayland_state) {
1863 eprintln!("Wayland dispatch error: {e:?}");
1864 break;
1865 }
1866 }
1867 eprintln!("wallr: Wayland connection lost, exiting");
1871 std::process::exit(1);
1872 });
1873
1874 loop {
1875 tokio::time::sleep(std::time::Duration::from_secs(3600)).await;
1876 }
1877 }
1878
1879 async fn start_watcher(
1880 &self,
1881 dir: PathBuf,
1882 render_states: std::sync::Arc<
1883 tokio::sync::Mutex<std::collections::HashMap<String, Arc<Mutex<RenderState>>>>,
1884 >,
1885 ) -> Result<(), DaemonError> {
1886 let engine = self.engine.clone();
1887 let paused = self.paused.clone();
1888 let debounce = crate::config::parse_duration(&self.config.watch.debounce)
1889 .unwrap_or(std::time::Duration::from_millis(500));
1890
1891 let (tx, mut rx) = tokio::sync::mpsc::channel(100);
1892
1893 let mut watcher = notify::recommended_watcher(move |res: Result<Event, notify::Error>| {
1894 if let Ok(event) = res
1895 && let EventKind::Create(_) = event.kind
1896 {
1897 for path in event.paths {
1898 let _ = tx.blocking_send(path);
1899 }
1900 }
1901 })
1902 .map_err(|e| DaemonError::StartError(e.to_string()))?;
1903
1904 watcher
1905 .watch(&dir, RecursiveMode::NonRecursive)
1906 .map_err(|e| DaemonError::StartError(e.to_string()))?;
1907
1908 tokio::spawn(async move {
1909 let _watcher = watcher;
1910 let mut last: Option<(PathBuf, std::time::Instant)> = None;
1911
1912 while let Some(path) = rx.recv().await {
1913 if paused.load(Ordering::SeqCst) {
1914 continue;
1915 }
1916 if let Some((ref lp, ref lt)) = last
1917 && lp == &path
1918 && lt.elapsed() < debounce
1919 {
1920 continue;
1921 }
1922 let ext = path
1923 .extension()
1924 .unwrap_or_default()
1925 .to_string_lossy()
1926 .to_lowercase();
1927 if !["jpg", "jpeg", "png", "gif", "webp"].contains(&ext.as_str()) {
1928 continue;
1929 }
1930 last = Some((path.clone(), std::time::Instant::now()));
1931
1932 let states = render_states.lock().await;
1934 for (name, rs) in states.iter() {
1935 let rs = rs.clone();
1936 let eng = engine.clone();
1937 let p = path.clone();
1938 let name = name.clone();
1939 tokio::spawn(async move {
1940 let mut lock = rs.lock().await;
1941 let effect =
1942 crate::animation::Effect::Fade(crate::animation::FadeParams::default());
1943 let _ = lock.set_wallpaper(&p, &effect, 600, 0).await;
1944 drop(lock);
1945 let opts = SetOptions {
1946 no_theme: false,
1947 theme_provider: None,
1948 monitor: Some(name),
1949 };
1950 let mut elock = eng.lock().await;
1951 let _ = elock.set_wallpaper(&p, &opts).await;
1952 });
1953 }
1954 }
1955 });
1956
1957 Ok(())
1958 }
1959
1960 #[allow(clippy::too_many_arguments)]
1963 async fn create_render_state_for_output(
1964 renderer: &std::sync::Arc<Renderer>,
1965 display_ptr: *mut std::ffi::c_void,
1966 wayland_state: &mut WaylandState,
1967 qh: &QueueHandle<WaylandState>,
1968 output: &OutputInfo,
1969 config: &WallrConfig,
1970 ) -> Result<RenderState, DaemonError> {
1971 let wl_surface = wayland_state.compositor_state.create_surface(qh);
1972 let layer_surface = wayland_state.layer_shell.create_layer_surface(
1973 qh,
1974 wl_surface,
1975 Layer::Background,
1976 Some("wallr"),
1977 Some(&output.wl_output),
1978 );
1979 layer_surface.set_anchor(Anchor::TOP | Anchor::BOTTOM | Anchor::LEFT | Anchor::RIGHT);
1980 layer_surface.set_exclusive_zone(-1);
1981 layer_surface.set_keyboard_interactivity(KeyboardInteractivity::None);
1982
1983 let empty_region = wayland_state.compositor.create_region(qh, ());
1985 layer_surface
1986 .wl_surface()
1987 .set_input_region(Some(&empty_region));
1988 layer_surface.commit();
1989 empty_region.destroy();
1990
1991 let scale_factor = if output.scale_factor > 0 {
1992 output.scale_factor
1993 } else {
1994 1
1995 };
1996 layer_surface.wl_surface().set_buffer_scale(scale_factor);
1997
1998 let width = output.width;
2000 let height = output.height;
2001
2002 let raw_surface = layer_surface.wl_surface().id().as_ptr() as *mut std::ffi::c_void;
2003 wayland_state
2004 .surfaces
2005 .push((output.wl_output.id().protocol_id(), layer_surface));
2006
2007 let window_handle = WaylandWindow {
2008 display: display_ptr,
2009 surface: raw_surface,
2010 };
2011
2012 let wgpu_surface = renderer
2013 .instance
2014 .create_surface(&window_handle)
2015 .map_err(|e| DaemonError::StartError(format!("wgpu surface creation failed: {e:?}")))?;
2016
2017 let adapter = renderer
2018 .instance
2019 .request_adapter(&wgpu::RequestAdapterOptions {
2020 compatible_surface: Some(&wgpu_surface),
2021 power_preference: wgpu::PowerPreference::HighPerformance,
2022 force_fallback_adapter: false,
2023 })
2024 .await;
2025 let surf_format = adapter
2026 .as_ref()
2027 .map(|a| {
2028 let caps = wgpu_surface.get_capabilities(a);
2029 caps.formats
2030 .into_iter()
2031 .next()
2032 .unwrap_or(wgpu::TextureFormat::Bgra8UnormSrgb)
2033 })
2034 .unwrap_or(wgpu::TextureFormat::Bgra8UnormSrgb);
2035
2036 let surf_config = wgpu::SurfaceConfiguration {
2037 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
2038 format: surf_format,
2039 width,
2040 height,
2041 present_mode: wgpu::PresentMode::Fifo,
2042 alpha_mode: wgpu::CompositeAlphaMode::Opaque,
2043 view_formats: vec![],
2044 desired_maximum_frame_latency: 2,
2045 };
2046 wgpu_surface.configure(&renderer.device, &surf_config);
2047
2048 let wgpu_surface: wgpu::Surface<'static> = unsafe { std::mem::transmute(wgpu_surface) };
2051 let surface: &'static wgpu::Surface<'static> = Box::leak(Box::new(wgpu_surface));
2052
2053 Ok(RenderState {
2054 renderer: renderer.clone(),
2055 surface,
2056 render_lock: std::sync::Arc::new(std::sync::Mutex::new(())),
2057 playback_gen: std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0)),
2058 pacer: std::sync::Arc::new(LivePacer::new()),
2059 current_bind: None,
2060 current_tex: None,
2061 width,
2062 height,
2063 current_width: 0,
2064 current_height: 0,
2065 format: surf_format,
2066 video_playback: std::sync::Arc::new(crate::video::VideoPlayback::new()),
2067 hw_accel: crate::video::HwAccel::from_config(&config.video.hw_decode),
2068 scaling_mode: 0,
2069 per_output_uniforms: std::sync::Arc::new(renderer.create_per_output_uniforms()),
2070 last_wallpaper: None,
2071 pre_blank: None,
2072 blanked: false,
2073 gif_paused: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
2074 })
2075 }
2076}
2077
2078#[allow(clippy::too_many_arguments)]
2082fn create_render_state_for_output_sync(
2083 renderer: &std::sync::Arc<Renderer>,
2084 display_ptr: *mut std::ffi::c_void,
2085 wayland_state: &mut WaylandState,
2086 qh: &QueueHandle<WaylandState>,
2087 output: &OutputInfo,
2088 config: &WallrConfig,
2089) -> Result<RenderState, DaemonError> {
2090 let wl_surface = wayland_state.compositor_state.create_surface(qh);
2091 let layer_surface = wayland_state.layer_shell.create_layer_surface(
2092 qh,
2093 wl_surface,
2094 Layer::Background,
2095 Some("wallr"),
2096 Some(&output.wl_output),
2097 );
2098 layer_surface.set_anchor(Anchor::TOP | Anchor::BOTTOM | Anchor::LEFT | Anchor::RIGHT);
2099 layer_surface.set_exclusive_zone(-1);
2100 layer_surface.set_keyboard_interactivity(KeyboardInteractivity::None);
2101
2102 let empty_region = wayland_state.compositor.create_region(qh, ());
2103 layer_surface
2104 .wl_surface()
2105 .set_input_region(Some(&empty_region));
2106 layer_surface.commit();
2107 empty_region.destroy();
2108
2109 let scale_factor = if output.scale_factor > 0 {
2110 output.scale_factor
2111 } else {
2112 1
2113 };
2114 layer_surface.wl_surface().set_buffer_scale(scale_factor);
2115
2116 let width = output.width;
2118 let height = output.height;
2119
2120 let raw_surface = layer_surface.wl_surface().id().as_ptr() as *mut std::ffi::c_void;
2121 wayland_state
2122 .surfaces
2123 .push((output.wl_output.id().protocol_id(), layer_surface));
2124
2125 let window_handle = WaylandWindow {
2126 display: display_ptr,
2127 surface: raw_surface,
2128 };
2129
2130 let wgpu_surface = renderer
2131 .instance
2132 .create_surface(&window_handle)
2133 .map_err(|e| DaemonError::StartError(format!("wgpu surface creation failed: {e:?}")))?;
2134
2135 let surf_format = {
2136 let caps = wgpu_surface.get_capabilities(&renderer.adapter);
2137 caps.formats
2138 .into_iter()
2139 .next()
2140 .unwrap_or(wgpu::TextureFormat::Bgra8UnormSrgb)
2141 };
2142
2143 let surf_config = wgpu::SurfaceConfiguration {
2144 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
2145 format: surf_format,
2146 width,
2147 height,
2148 present_mode: wgpu::PresentMode::Fifo,
2149 alpha_mode: wgpu::CompositeAlphaMode::Opaque,
2150 view_formats: vec![],
2151 desired_maximum_frame_latency: 2,
2152 };
2153 wgpu_surface.configure(&renderer.device, &surf_config);
2154
2155 let wgpu_surface: wgpu::Surface<'static> = unsafe { std::mem::transmute(wgpu_surface) };
2156 let surface: &'static wgpu::Surface<'static> = Box::leak(Box::new(wgpu_surface));
2157
2158 Ok(RenderState {
2159 renderer: renderer.clone(),
2160 surface,
2161 render_lock: std::sync::Arc::new(std::sync::Mutex::new(())),
2162 playback_gen: std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0)),
2163 pacer: std::sync::Arc::new(LivePacer::new()),
2164 current_bind: None,
2165 current_tex: None,
2166 width,
2167 height,
2168 current_width: 0,
2169 current_height: 0,
2170 format: surf_format,
2171 video_playback: std::sync::Arc::new(crate::video::VideoPlayback::new()),
2172 hw_accel: crate::video::HwAccel::from_config(&config.video.hw_decode),
2173 scaling_mode: 0,
2174 per_output_uniforms: std::sync::Arc::new(renderer.create_per_output_uniforms()),
2175 last_wallpaper: None,
2176 pre_blank: None,
2177 blanked: false,
2178 gif_paused: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
2179 })
2180}