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
75struct OutputInfo {
76 name: String,
77 width: u32,
78 height: u32,
79 scale_factor: i32,
80 wl_output: wl_output::WlOutput,
81}
82
83struct WaylandState {
84 registry_state: RegistryState,
85 output_state: OutputState,
86 compositor_state: CompositorState,
87 shm: Shm,
88 outputs: std::collections::HashMap<u32, OutputInfo>,
89 surfaces: Vec<(u32, LayerSurface)>,
90}
91
92impl ProvidesRegistryState for WaylandState {
93 fn registry(&mut self) -> &mut RegistryState {
94 &mut self.registry_state
95 }
96
97 registry_handlers![OutputState,];
98}
99
100impl CompositorHandler for WaylandState {
101 fn scale_factor_changed(
102 &mut self,
103 _conn: &Connection,
104 _qh: &QueueHandle<Self>,
105 _surface: &wl_surface::WlSurface,
106 _new_factor: i32,
107 ) {
108 }
109 fn transform_changed(
110 &mut self,
111 _conn: &Connection,
112 _qh: &QueueHandle<Self>,
113 _surface: &wl_surface::WlSurface,
114 _new_transform: wl_output::Transform,
115 ) {
116 }
117 fn frame(
118 &mut self,
119 _conn: &Connection,
120 _qh: &QueueHandle<Self>,
121 _surface: &wl_surface::WlSurface,
122 _time: u32,
123 ) {
124 }
125 fn surface_enter(
126 &mut self,
127 _conn: &Connection,
128 _qh: &QueueHandle<Self>,
129 _surface: &wl_surface::WlSurface,
130 _output: &wl_output::WlOutput,
131 ) {
132 }
133 fn surface_leave(
134 &mut self,
135 _conn: &Connection,
136 _qh: &QueueHandle<Self>,
137 _surface: &wl_surface::WlSurface,
138 _output: &wl_output::WlOutput,
139 ) {
140 }
141}
142
143impl wayland_client::Dispatch<wayland_client::protocol::wl_region::WlRegion, ()> for WaylandState {
144 fn event(
145 _state: &mut WaylandState,
146 _region: &wayland_client::protocol::wl_region::WlRegion,
147 _event: wayland_client::protocol::wl_region::Event,
148 _data: &(),
149 _conn: &Connection,
150 _qh: &QueueHandle<WaylandState>,
151 ) {
152 }
153}
154
155impl LayerShellHandler for WaylandState {
156 fn configure(
157 &mut self,
158 _conn: &Connection,
159 _qh: &QueueHandle<Self>,
160 layer: &LayerSurface,
161 _configure: LayerSurfaceConfigure,
162 _serial: u32,
163 ) {
164 layer.commit();
165 }
166
167 fn closed(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _layer: &LayerSurface) {}
168}
169
170impl ShmHandler for WaylandState {
171 fn shm_state(&mut self) -> &mut Shm {
172 &mut self.shm
173 }
174}
175
176impl OutputHandler for WaylandState {
177 fn output_state(&mut self) -> &mut OutputState {
178 &mut self.output_state
179 }
180 fn new_output(
181 &mut self,
182 _conn: &Connection,
183 _qh: &QueueHandle<Self>,
184 output: wl_output::WlOutput,
185 ) {
186 let id = output.id().protocol_id();
187 let info = OutputInfo {
188 name: format!("output-{id}"),
189 width: 1920,
190 height: 1080,
191 scale_factor: 1,
192 wl_output: output,
193 };
194 self.outputs.insert(id, info);
195 }
196 fn update_output(
197 &mut self,
198 _conn: &Connection,
199 _qh: &QueueHandle<Self>,
200 output: wl_output::WlOutput,
201 ) {
202 let id = output.id().protocol_id();
203 if let Some(info) = self.outputs.get_mut(&id) {
204 if let Some(mode) = self
205 .output_state
206 .info(&output)
207 .and_then(|i| i.modes.iter().find(|m| m.current).cloned())
208 {
209 info.width = mode.dimensions.0 as u32;
210 info.height = mode.dimensions.1 as u32;
211 }
212 if let Some(info_data) = self.output_state.info(&output) {
213 info.scale_factor = info_data.scale_factor;
214 if !info.name.starts_with("output-") {
215 info.name = info_data.name.clone().unwrap_or_else(|| info.name.clone());
216 }
217 }
218 }
219 }
220 fn output_destroyed(
221 &mut self,
222 _conn: &Connection,
223 _qh: &QueueHandle<Self>,
224 output: wl_output::WlOutput,
225 ) {
226 let id = output.id().protocol_id();
227 self.outputs.remove(&id);
228 }
229}
230
231delegate_compositor!(WaylandState);
232delegate_layer!(WaylandState);
233delegate_output!(WaylandState);
234delegate_registry!(WaylandState);
235delegate_shm!(WaylandState);
236
237struct LivePacer {
239 lock: std::sync::Mutex<()>,
240 cond: std::sync::Condvar,
241}
242
243impl LivePacer {
244 fn new() -> Self {
245 Self {
246 lock: std::sync::Mutex::new(()),
247 cond: std::sync::Condvar::new(),
248 }
249 }
250
251 fn notify(&self) {
252 let _guard = self.lock.lock().unwrap();
253 self.cond.notify_all();
254 }
255
256 fn wait_until(&self, deadline: std::time::Instant) {
259 let guard = self.lock.lock().unwrap();
260 let now = std::time::Instant::now();
261 if deadline <= now {
262 return;
263 }
264 let _ = self
265 .cond
266 .wait_timeout_while(guard, deadline - now, |_| true);
267 }
268}
269
270struct RenderState {
271 renderer: std::sync::Arc<Renderer>,
272 surface: &'static wgpu::Surface<'static>,
273 render_lock: std::sync::Arc<std::sync::Mutex<()>>,
277 playback_gen: std::sync::Arc<std::sync::atomic::AtomicU64>,
280 pacer: std::sync::Arc<LivePacer>,
283 current_bind: Option<wgpu::BindGroup>,
284 current_tex: Option<wgpu::Texture>,
285 width: u32,
286 height: u32,
287 current_width: u32,
288 current_height: u32,
289 format: wgpu::TextureFormat,
290 video_playback: std::sync::Arc<crate::video::VideoPlayback>,
292 hw_accel: crate::video::HwAccel,
294 scaling_mode: u32,
296}
297
298struct CommitData {
301 bg_bind: wgpu::BindGroup,
302 new_bind: wgpu::BindGroup,
303 img_width: u32,
304 img_height: u32,
305 old_img_width: u32,
306 old_img_height: u32,
307 format: wgpu::TextureFormat,
308 width: u32,
309 height: u32,
310 animated: Option<crate::animated::AnimatedImage>,
313 is_video: bool,
315 generation: u64,
318 scaling_mode: u32,
320}
321
322impl RenderState {
323 async fn set_wallpaper(
324 &mut self,
325 path: &std::path::Path,
326 effect: &crate::animation::Effect,
327 duration_ms: u32,
328 scaling_mode: u32,
329 ) -> anyhow::Result<()> {
330 self.scaling_mode = scaling_mode;
331 let commit = self.commit_wallpaper(path, scaling_mode)?;
332 self.spawn_transition(commit, effect, duration_ms);
333 Ok(())
334 }
335
336 fn commit_wallpaper(
340 &mut self,
341 path: &std::path::Path,
342 scaling_mode: u32,
343 ) -> anyhow::Result<CommitData> {
344 use image::ImageReader;
345
346 if crate::video::VideoDecoder::is_video_file(path) {
348 tracing::info!("Video file detected: {:?}", path);
349
350 let generation = self.playback_gen.fetch_add(1, Ordering::SeqCst) + 1;
356 self.pacer.notify();
357
358 let metadata = self.video_playback.start(path, self.hw_accel)?;
361
362 let first_frame = self
365 .video_playback
366 .wait_first_frame(std::time::Duration::from_millis(1000));
367
368 let (new_tex, new_bind, img_width, img_height) = if let Some(frame) = first_frame {
369 let (tex, bind) = self.renderer.create_texture(frame.width, frame.height);
370 self.renderer
371 .update_texture(&tex, &frame.data, frame.width, frame.height);
372 (tex, bind, frame.width, frame.height)
373 } else {
374 tracing::warn!("No first frame available, using black texture");
376 let (tex, bind) = self
377 .renderer
378 .create_texture(metadata.width, metadata.height);
379 let black = vec![0u8; (metadata.width * metadata.height * 4) as usize];
380 self.renderer
381 .update_texture(&tex, &black, metadata.width, metadata.height);
382 (tex, bind, metadata.width, metadata.height)
383 };
384
385 let old_bind = self.current_bind.take();
386 let (old_img_width, old_img_height) = if old_bind.is_some() {
387 (self.current_width.max(1), self.current_height.max(1))
388 } else {
389 (img_width, img_height)
390 };
391 let bg_bind = old_bind.unwrap_or_else(|| new_bind.clone());
392
393 drop(self.current_tex.take());
394 self.current_tex = Some(new_tex);
395 self.current_bind = Some(new_bind.clone());
396 self.current_width = img_width;
397 self.current_height = img_height;
398
399 return Ok(CommitData {
400 bg_bind,
401 new_bind,
402 img_width,
403 img_height,
404 old_img_width,
405 old_img_height,
406 format: self.format,
407 width: self.width,
408 height: self.height,
409 animated: None,
410 is_video: true,
411 generation,
412 scaling_mode,
413 });
414 }
415
416 self.video_playback.stop();
420
421 let mut animated = crate::animated::AnimatedImage::decode(path)?;
424 let (new_tex, new_bind, img_width, img_height) = if let Some(anim) = animated.as_mut() {
425 let (w, h) = (anim.width, anim.height);
426 let (tex, bind) = self.renderer.create_texture(w, h);
427 let first = anim.first_frame();
428 if !first.is_empty() {
429 self.renderer.update_texture(&tex, first, w, h);
430 }
431 (tex, bind, w, h)
432 } else {
433 let new_img = ImageReader::open(path)?.decode()?;
434 let (tex, bind) = self.renderer.load_texture(&new_img)?;
435 (tex, bind, new_img.width(), new_img.height())
436 };
437
438 let old_bind = self.current_bind.take();
439 let (old_img_width, old_img_height) = if old_bind.is_some() {
440 (self.current_width.max(1), self.current_height.max(1))
441 } else {
442 (img_width, img_height)
443 };
444 let bg_bind = old_bind.unwrap_or_else(|| new_bind.clone());
449
450 drop(self.current_tex.take());
451 self.current_tex = Some(new_tex);
452 self.current_bind = Some(new_bind.clone());
453 self.current_width = img_width;
454 self.current_height = img_height;
455
456 let generation = self.playback_gen.fetch_add(1, Ordering::SeqCst) + 1;
457 self.pacer.notify();
458
459 Ok(CommitData {
460 bg_bind,
461 new_bind,
462 img_width,
463 img_height,
464 old_img_width,
465 old_img_height,
466 format: self.format,
467 width: self.width,
468 height: self.height,
469 animated,
470 is_video: false,
471 generation,
472 scaling_mode,
473 })
474 }
475
476 fn spawn_transition(
481 &self,
482 commit: CommitData,
483 effect: &crate::animation::Effect,
484 duration_ms: u32,
485 ) {
486 let renderer = self.renderer.clone();
487 let surface: &'static wgpu::Surface<'static> = self.surface;
488 let render_lock = self.render_lock.clone();
489 let playback_gen = self.playback_gen.clone();
490 let pacer = self.pacer.clone();
491 let video_playback = self.video_playback.clone();
492 let effect = effect.clone();
493 drop(tokio::task::spawn_blocking(move || {
494 render_transition(
495 renderer,
496 surface,
497 render_lock,
498 playback_gen,
499 pacer,
500 video_playback,
501 commit,
502 effect,
503 duration_ms,
504 );
505 }));
506 }
507}
508
509#[allow(clippy::too_many_arguments)]
517fn render_transition(
518 renderer: std::sync::Arc<Renderer>,
519 surface: &'static wgpu::Surface<'static>,
520 render_lock: std::sync::Arc<std::sync::Mutex<()>>,
521 playback_gen: std::sync::Arc<std::sync::atomic::AtomicU64>,
522 pacer: std::sync::Arc<LivePacer>,
523 video_playback: std::sync::Arc<crate::video::VideoPlayback>,
524 mut commit: CommitData,
525 effect: crate::animation::Effect,
526 duration_ms: u32,
527) {
528 let _guard = render_lock
529 .lock()
530 .unwrap_or_else(|poisoned| poisoned.into_inner());
531
532 let duration = std::time::Duration::from_millis(u64::from(duration_ms.max(1)));
533 let start = std::time::Instant::now();
534 loop {
535 let progress = start.elapsed().as_secs_f32() / duration.as_secs_f32();
536 let uniforms = crate::animation::compute_effect_uniforms(&effect, progress.clamp(0.0, 1.0));
537 let status = renderer.render_frame(crate::renderer::FrameRequest {
538 surface,
539 format: commit.format,
540 bg_bind: &commit.bg_bind,
541 new_bind: &commit.new_bind,
542 effect: &uniforms,
543 width: commit.width,
544 height: commit.height,
545 img_width: commit.img_width,
546 img_height: commit.img_height,
547 old_img_width: commit.old_img_width,
548 old_img_height: commit.old_img_height,
549 scaling_mode: commit.scaling_mode,
550 });
551 let status = match status {
552 Ok(status) => status,
553 Err(err) => {
554 eprintln!("wallr: transition render failed: {err}");
555 break;
556 }
557 };
558 if progress >= 1.0 || status == crate::renderer::FrameStatus::TimedOut {
559 break;
560 }
561 }
562
563 let mut animated = commit.animated.take();
567 if let Some(animated) = animated.as_mut()
568 && playback_gen.load(Ordering::SeqCst) == commit.generation
569 {
570 play_live(&renderer, surface, &commit, animated, &playback_gen, &pacer);
571 } else if commit.is_video && playback_gen.load(Ordering::SeqCst) == commit.generation {
572 play_video(&renderer, surface, &commit, &video_playback, &playback_gen);
573 }
574}
575
576fn play_live(
582 renderer: &Renderer,
583 surface: &'static wgpu::Surface<'static>,
584 commit: &CommitData,
585 animated: &mut crate::animated::AnimatedImage,
586 playback_gen: &std::sync::atomic::AtomicU64,
587 pacer: &LivePacer,
588) {
589 let (tex_a, bind_a) = renderer.create_texture(animated.width, animated.height);
590 let (tex_b, bind_b) = renderer.create_texture(animated.width, animated.height);
591 let (frame_w, frame_h) = (animated.width, animated.height);
592 let (bytes_per_row, rows) = (frame_w * 4, frame_h);
593 let frame_bytes = bytes_per_row as u64 * rows as u64;
594
595 let direct_upload = bytes_per_row % 256 == 0;
598 let staging: Vec<wgpu::Buffer> = if direct_upload {
599 (0..2)
600 .map(|_| {
601 renderer.device.create_buffer(&wgpu::BufferDescriptor {
602 label: Some("wallr-gif-staging"),
603 size: frame_bytes,
604 usage: wgpu::BufferUsages::MAP_WRITE | wgpu::BufferUsages::COPY_SRC,
605 mapped_at_creation: false,
606 })
607 })
608 .collect()
609 } else {
610 Vec::new()
611 };
612
613 let first = animated.first_frame();
614 if !first.is_empty() {
615 renderer.update_texture(&tex_a, first, frame_w, frame_h);
616 renderer.update_texture(&tex_b, first, frame_w, frame_h);
617 }
618 let binds = [bind_a, bind_b];
619 let textures = [tex_a, tex_b];
620
621 let upload = |renderer: &Renderer,
624 tgt: usize,
625 index: usize,
626 slot: usize,
627 animated: &mut crate::animated::AnimatedImage|
628 -> bool {
629 if direct_upload {
630 let buffer = &staging[slot];
631 let slice = buffer.slice(..);
632 slice.map_async(wgpu::MapMode::Write, |_| {});
633 renderer.device.poll(wgpu::Maintain::Wait);
634 let ok = {
635 let mut mapped = slice.get_mapped_range_mut();
636 animated.decompress_into(index, &mut mapped)
637 };
638 buffer.unmap();
639 if ok {
640 let mut encoder = renderer
641 .device
642 .create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
643 encoder.copy_buffer_to_texture(
644 wgpu::TexelCopyBufferInfo {
645 buffer,
646 layout: wgpu::TexelCopyBufferLayout {
647 offset: 0,
648 bytes_per_row: Some(bytes_per_row),
649 rows_per_image: Some(rows),
650 },
651 },
652 wgpu::TexelCopyTextureInfo {
653 texture: &textures[tgt],
654 mip_level: 0,
655 origin: wgpu::Origin3d::ZERO,
656 aspect: wgpu::TextureAspect::All,
657 },
658 wgpu::Extent3d {
659 width: frame_w,
660 height: frame_h,
661 depth_or_array_layers: 1,
662 },
663 );
664 renderer.queue.submit([encoder.finish()]);
665 return true;
666 }
667 } else {
668 let frame = animated.frame_at(index);
669 if !frame.is_empty() {
670 renderer.update_texture(&textures[tgt], frame, frame_w, frame_h);
671 return true;
672 }
673 }
674 false
675 };
676
677 let mut cur = 0usize; let mut cur_frame = 0usize; let mut next_frame = 0usize; let mut slot = 0usize; let start = std::time::Instant::now();
682 let static_effect = crate::animation::Effect::Fade(crate::animation::FadeParams::default());
683 loop {
684 if playback_gen.load(Ordering::SeqCst) != commit.generation {
685 return;
686 }
687 let index = animated.frame_index_at(start.elapsed());
688 if index != cur_frame {
689 if next_frame != index {
690 upload(renderer, cur ^ 1, index, slot, animated);
691 slot ^= 1;
692 next_frame = index;
693 }
694 cur ^= 1;
695 cur_frame = index;
696 }
697 let uniforms = crate::animation::compute_effect_uniforms(&static_effect, 1.0);
698 let status = renderer.render_frame(crate::renderer::FrameRequest {
699 surface,
700 format: commit.format,
701 bg_bind: &binds[cur],
702 new_bind: &binds[cur],
703 effect: &uniforms,
704 width: commit.width,
705 height: commit.height,
706 img_width: animated.width,
707 img_height: animated.height,
708 old_img_width: animated.width,
709 old_img_height: animated.height,
710 scaling_mode: commit.scaling_mode,
711 });
712 match status {
713 Ok(crate::renderer::FrameStatus::Presented) => {}
714 _ => return,
718 }
719
720 let elapsed = start.elapsed();
728 let total: std::time::Duration = animated.total_duration();
729 let loops = (elapsed.as_millis() / total.as_millis().max(1)) as u64;
730 let next_change = animated.frame_start(index + 1) + total * (loops as u32);
731 let wait = next_change.saturating_sub(elapsed);
732 if wait > std::time::Duration::ZERO {
733 let next = index + 1;
734 if next_frame != next {
735 upload(renderer, cur ^ 1, next, slot, animated);
736 slot ^= 1;
737 next_frame = next;
738 }
739 pacer.wait_until(std::time::Instant::now() + wait);
740 }
741 }
742}
743
744fn play_video(
746 renderer: &Renderer,
747 surface: &'static wgpu::Surface<'static>,
748 commit: &CommitData,
749 video_playback: &std::sync::Arc<crate::video::VideoPlayback>,
750 playback_gen: &std::sync::atomic::AtomicU64,
751) {
752 let (width, height) = match video_playback.metadata() {
754 Some(meta) => (meta.width, meta.height),
755 None => {
756 tracing::warn!("No video metadata available");
757 return;
758 }
759 };
760
761 let (texture, bind) = renderer.create_texture(width, height);
762 let static_effect = crate::animation::Effect::Fade(crate::animation::FadeParams::default());
763
764 let mut uploaded = false;
767
768 loop {
769 if playback_gen.load(Ordering::SeqCst) != commit.generation {
774 return;
775 }
776
777 if let Some(frame) = video_playback.next_frame() {
780 if frame.width != width || frame.height != height {
783 continue;
784 }
785 renderer.update_texture(&texture, &frame.data, frame.width, frame.height);
786 uploaded = true;
787 }
788
789 if !uploaded {
790 std::thread::sleep(std::time::Duration::from_millis(2));
793 continue;
794 }
795
796 let uniforms = crate::animation::compute_effect_uniforms(&static_effect, 1.0);
797 let status = renderer.render_frame(crate::renderer::FrameRequest {
798 surface,
799 format: commit.format,
800 bg_bind: &bind,
801 new_bind: &bind,
802 effect: &uniforms,
803 width: commit.width,
804 height: commit.height,
805 img_width: width,
806 img_height: height,
807 old_img_width: width,
808 old_img_height: height,
809 scaling_mode: commit.scaling_mode,
810 });
811
812 match status {
813 Ok(crate::renderer::FrameStatus::Presented) => {}
814 other => {
817 tracing::warn!("Video present failed ({:?}), stopping playback", other);
818 video_playback.stop();
819 return;
820 }
821 }
822 }
823}
824
825pub struct Daemon {
826 config: WallrConfig,
827 paused: Arc<AtomicBool>,
828 engine: Arc<Mutex<WallpaperEngine>>,
829}
830
831impl Daemon {
832 pub fn new(config: WallrConfig) -> Result<Self, DaemonError> {
833 let engine = WallpaperEngine::new(config.clone())?;
834 Ok(Self {
835 config,
836 paused: Arc::new(AtomicBool::new(false)),
837 engine: Arc::new(Mutex::new(engine)),
838 })
839 }
840
841 pub async fn start(self) -> Result<(), DaemonError> {
842 let socket_path = crate::config::expand_path(&self.config.daemon.socket);
843 if socket_path.exists() {
844 if tokio::net::UnixStream::connect(&socket_path).await.is_ok() {
845 return Err(DaemonError::AlreadyRunning(
846 socket_path.to_string_lossy().to_string(),
847 ));
848 }
849 let _ = std::fs::remove_file(&socket_path);
850 }
851
852 let renderer = Renderer::new()
853 .await
854 .map_err(|e| DaemonError::StartError(format!("GPU init failed: {e}")))?;
855
856 let conn = Connection::connect_to_env()
857 .map_err(|e| DaemonError::StartError(format!("Failed to connect to Wayland: {e:?}")))?;
858 let backend = conn.backend();
859 let display_ptr = backend.display_ptr() as *mut std::ffi::c_void;
860
861 let (globals, mut event_queue) = registry_queue_init(&conn)
862 .map_err(|e| DaemonError::StartError(format!("registry_queue_init failed: {e:?}")))?;
863 let qh = event_queue.handle();
864
865 let compositor_state = CompositorState::bind(&globals, &qh)
866 .map_err(|e| DaemonError::StartError(format!("compositor bind failed: {e:?}")))?;
867 let layer_shell = LayerShell::bind(&globals, &qh)
868 .map_err(|e| DaemonError::StartError(format!("layer_shell bind failed: {e:?}")))?;
869 let shm = Shm::bind(&globals, &qh)
870 .map_err(|e| DaemonError::StartError(format!("shm bind failed: {e:?}")))?;
871
872 let mut wayland_state = WaylandState {
873 registry_state: RegistryState::new(&globals),
874 output_state: OutputState::new(&globals, &qh),
875 compositor_state,
876 shm,
877 outputs: std::collections::HashMap::new(),
878 surfaces: Vec::new(),
879 };
880
881 let compositor = globals
883 .bind::<wl_compositor::WlCompositor, WaylandState, smithay_client_toolkit::globals::GlobalData>(
884 &qh,
885 1..=4,
886 smithay_client_toolkit::globals::GlobalData,
887 )
888 .map_err(|e| DaemonError::StartError(format!("compositor bind failed: {e:?}")))?;
889
890 event_queue
892 .roundtrip(&mut wayland_state)
893 .map_err(|e| DaemonError::StartError(format!("roundtrip failed: {e:?}")))?;
894 event_queue
895 .roundtrip(&mut wayland_state)
896 .map_err(|e| DaemonError::StartError(format!("roundtrip2 failed: {e:?}")))?;
897
898 if wayland_state.outputs.is_empty() {
899 return Err(DaemonError::StartError(
900 "no outputs detected after roundtrip".into(),
901 ));
902 }
903
904 let renderer = std::sync::Arc::new(renderer);
905
906 let mut render_states: std::collections::HashMap<String, Arc<Mutex<RenderState>>> =
910 std::collections::HashMap::new();
911
912 let output_info: Vec<(u32, OutputInfo)> = wayland_state
915 .outputs
916 .iter()
917 .map(|(k, v)| {
918 (
919 *k,
920 OutputInfo {
921 name: v.name.clone(),
922 width: v.width,
923 height: v.height,
924 scale_factor: v.scale_factor,
925 wl_output: v.wl_output.clone(),
926 },
927 )
928 })
929 .collect();
930
931 for (proto_id, info) in &output_info {
932 let name = info.name.clone();
933 let rs = Self::create_render_state_for_output(
934 &renderer,
935 display_ptr,
936 &mut wayland_state,
937 &qh,
938 &layer_shell,
939 &compositor,
940 info,
941 &self.config,
942 )
943 .await?;
944 let rs = Arc::new(Mutex::new(rs));
945 render_states.insert(name.clone(), rs.clone());
946
947 let state_path = dirs::cache_dir()
949 .unwrap_or_else(|| std::path::PathBuf::from("/tmp"))
950 .join(format!("wallr/last_wallpaper/{name}"));
951 if let Ok(path_str) = std::fs::read_to_string(&state_path) {
952 let p = std::path::Path::new(path_str.trim());
953 if p.exists() {
954 let mut lock = rs.lock().await;
955 let effect =
956 crate::animation::Effect::Fade(crate::animation::FadeParams::default());
957 let _ = lock.set_wallpaper(p, &effect, 0, 0).await;
958 }
959 }
960
961 tracing::info!("Output ready: {name} ({proto_id})");
962 }
963
964 let paused_clone = self.paused.clone();
965 let engine_clone = self.engine.clone();
966 let render_states_clone = render_states.clone();
967
968 {
972 let rs_map = render_states.clone();
973 let socket_path = socket_path.clone();
974 tokio::spawn(async move {
975 use tokio::signal::unix::{SignalKind, signal};
976 let mut term = signal(SignalKind::terminate()).expect("SIGTERM handler");
977 let mut int = signal(SignalKind::interrupt()).expect("SIGINT handler");
978 let mut hup = signal(SignalKind::hangup()).expect("SIGHUP handler");
979 tokio::select! {
980 _ = term.recv() => {}
981 _ = int.recv() => {}
982 _ = hup.recv() => {}
983 }
984 tracing::info!("Signal received, shutting down gracefully");
985 for rs in rs_map.values() {
986 if let Ok(state) = rs.try_lock() {
987 state.video_playback.stop();
988 }
989 }
990 let _ = std::fs::remove_file(&socket_path);
991 std::process::exit(0);
992 });
993 }
994
995 let ipc_socket_path = socket_path.clone();
996 start_ipc_server(&socket_path, move |cmd| {
997 let paused = paused_clone.clone();
998 let engine = engine_clone.clone();
999 let render_states = render_states_clone.clone();
1000 let stop_socket = ipc_socket_path.clone();
1001 async move {
1002 match cmd {
1003 IpcCommand::Pause => {
1004 paused.store(true, Ordering::SeqCst);
1005 for rs in render_states.values() {
1006 let rs_lock = rs.lock().await;
1007 rs_lock.video_playback.pause();
1008 }
1009 IpcResponse {
1010 success: true,
1011 message: Some("Paused".into()),
1012 }
1013 }
1014 IpcCommand::Resume => {
1015 paused.store(false, Ordering::SeqCst);
1016 for rs in render_states.values() {
1017 let rs_lock = rs.lock().await;
1018 rs_lock.video_playback.resume();
1019 }
1020 IpcResponse {
1021 success: true,
1022 message: Some("Resumed".into()),
1023 }
1024 }
1025 IpcCommand::Reload => {
1026 let lock = engine.lock().await;
1027 match lock.reload() {
1028 Ok(_) => IpcResponse {
1029 success: true,
1030 message: Some("Reloaded".into()),
1031 },
1032 Err(e) => IpcResponse {
1033 success: false,
1034 message: Some(e.to_string()),
1035 },
1036 }
1037 }
1038 IpcCommand::Preview {
1039 path,
1040 effect,
1041 duration_ms,
1042 no_theme,
1043 theme_override,
1044 monitor,
1045 scaling_mode,
1046 } => {
1047 if paused.load(Ordering::SeqCst) {
1048 return IpcResponse {
1049 success: false,
1050 message: Some("Daemon is paused".into()),
1051 };
1052 }
1053 let p = std::path::PathBuf::from(&path);
1054 if !p.exists() {
1055 return IpcResponse {
1056 success: false,
1057 message: Some(format!("File not found: {}", path)),
1058 };
1059 }
1060
1061 let target_rs = if let Some(ref mon) = monitor {
1064 render_states
1065 .get(mon)
1066 .cloned()
1067 .or_else(|| render_states.values().next().cloned())
1068 } else {
1069 render_states.values().next().cloned()
1070 };
1071 let target_rs = match target_rs {
1072 Some(rs) => rs,
1073 None => {
1074 return IpcResponse {
1075 success: false,
1076 message: Some("No outputs available".into()),
1077 };
1078 }
1079 };
1080
1081 let output_names: Vec<String> = if monitor.is_some() {
1084 monitor
1085 .as_ref()
1086 .map(|m| vec![m.clone()])
1087 .unwrap_or_default()
1088 } else {
1089 render_states.keys().cloned().collect()
1090 };
1091 for name in &output_names {
1092 let state_path = dirs::cache_dir()
1093 .unwrap_or_else(|| std::path::PathBuf::from("/tmp"))
1094 .join(format!("wallr/last_wallpaper/{name}"));
1095 if let Some(parent) = state_path.parent() {
1096 let _ = std::fs::create_dir_all(parent);
1097 }
1098 let _ = std::fs::write(&state_path, &path);
1099 }
1100
1101 let effect = effect.unwrap_or_else(|| {
1102 crate::animation::Effect::Fade(crate::animation::FadeParams::default())
1103 });
1104 let is_video = crate::video::VideoDecoder::is_video_file(&p);
1108 let duration = duration_ms.unwrap_or(if is_video { 150 } else { 2000 });
1109 let sm = scaling_mode.unwrap_or(crate::config::ScalingMode::Fill);
1110 let scaling_mode_u32 = match sm {
1111 crate::config::ScalingMode::Fill => 0u32,
1112 crate::config::ScalingMode::Fit => 1,
1113 crate::config::ScalingMode::Stretch => 2,
1114 crate::config::ScalingMode::Center => 3,
1115 crate::config::ScalingMode::Tile => 4,
1116 };
1117
1118 let rs_clone = target_rs.clone();
1119 let p_clone = p.clone();
1120 let result = tokio::task::spawn_blocking(move || {
1121 let rt = tokio::runtime::Handle::current();
1122 rt.block_on(async {
1123 let mut lock = rs_clone.lock().await;
1124 lock.set_wallpaper(&p_clone, &effect, duration, scaling_mode_u32)
1125 .await
1126 })
1127 })
1128 .await;
1129
1130 match result {
1131 Ok(Ok(())) => {
1132 let opts = SetOptions {
1133 no_theme,
1134 theme_provider: theme_override,
1135 monitor,
1136 };
1137 let mut eng = engine.lock().await;
1138 match eng.set_wallpaper(&p, &opts).await {
1139 Ok(()) => IpcResponse {
1140 success: true,
1141 message: None,
1142 },
1143 Err(e) => IpcResponse {
1144 success: true,
1145 message: Some(format!(
1146 "Wallpaper set, but hooks/theme failed: {e}"
1147 )),
1148 },
1149 }
1150 }
1151 Ok(Err(e)) => IpcResponse {
1152 success: false,
1153 message: Some(format!("Render failed: {}", e)),
1154 },
1155 Err(e) => IpcResponse {
1156 success: false,
1157 message: Some(format!("Task spawn failed: {}", e)),
1158 },
1159 }
1160 }
1161 IpcCommand::Stop => {
1162 let sp = stop_socket.clone();
1163 tokio::spawn(async move {
1164 tokio::time::sleep(std::time::Duration::from_millis(300)).await;
1165 let _ = std::fs::remove_file(&sp);
1166 std::process::exit(0);
1167 });
1168 IpcResponse {
1169 success: true,
1170 message: Some("Stopping".into()),
1171 }
1172 }
1173 IpcCommand::Status => {
1174 let state = if paused.load(Ordering::SeqCst) {
1175 "paused"
1176 } else {
1177 "running"
1178 };
1179 IpcResponse {
1180 success: true,
1181 message: Some(format!("wallr daemon {}", state)),
1182 }
1183 }
1184 IpcCommand::Seek { timestamp_ms } => {
1185 let target_rs = render_states.values().next().cloned();
1186 let target_rs = match target_rs {
1187 Some(rs) => rs,
1188 None => {
1189 return IpcResponse {
1190 success: false,
1191 message: Some("No outputs available".into()),
1192 };
1193 }
1194 };
1195 let rs_lock = target_rs.lock().await;
1196 match rs_lock
1197 .video_playback
1198 .seek(std::time::Duration::from_millis(timestamp_ms))
1199 {
1200 Ok(()) => IpcResponse {
1201 success: true,
1202 message: Some(format!("Seeked to {}ms", timestamp_ms)),
1203 },
1204 Err(e) => IpcResponse {
1205 success: false,
1206 message: Some(format!("Seek failed: {}", e)),
1207 },
1208 }
1209 }
1210 IpcCommand::Info => {
1211 let target_rs = render_states.values().next().cloned();
1212 let target_rs = match target_rs {
1213 Some(rs) => rs,
1214 None => {
1215 return IpcResponse {
1216 success: false,
1217 message: Some("No outputs available".into()),
1218 };
1219 }
1220 };
1221 let rs_lock = target_rs.lock().await;
1222
1223 let gpu_info =
1225 crate::video::gpu::adapter_diagnostics(&rs_lock.renderer.adapter);
1226
1227 let mut lines = vec![
1228 format!("wallr v{}", env!("CARGO_PKG_VERSION")),
1229 String::new(),
1230 format!("Outputs: {}", render_states.len()),
1231 ];
1232 for name in render_states.keys() {
1233 lines.push(format!(" - {name}"));
1234 }
1235 lines.push(String::new());
1236 lines.push(gpu_info);
1237
1238 match rs_lock.video_playback.metadata() {
1239 Some(meta) => {
1240 let decoder_info = rs_lock.video_playback.decoder_info();
1241 let hw = rs_lock.video_playback.hw_accel_in_use();
1242 let state = if rs_lock.video_playback.is_paused() {
1243 "paused"
1244 } else {
1245 "playing"
1246 };
1247 let position = rs_lock
1248 .video_playback
1249 .position()
1250 .map(|p| format!("{:.2}s", p.as_secs_f64()))
1251 .unwrap_or_else(|| "?".to_string());
1252 lines.push(String::new());
1253 lines.push("Video:".into());
1254 lines.push(format!(" Resolution: {}x{}", meta.width, meta.height));
1255 lines.push(format!(" FPS: {:.2}", meta.fps));
1256 lines.push(format!(
1257 " Duration: {:.2}s",
1258 meta.duration.as_secs_f64()
1259 ));
1260 lines.push(format!(
1261 " Codec: {}",
1262 decoder_info
1263 .as_ref()
1264 .map(|d| d.codec_name.as_str())
1265 .unwrap_or("unknown")
1266 ));
1267 lines.push(format!(" Container: {}", meta.format));
1268 lines.push(format!(" Decoder: {}", hw.name()));
1269 lines.push(format!(
1270 " GPU Decode: {}",
1271 if hw == crate::video::HwAccel::Software {
1272 "disabled"
1273 } else {
1274 "enabled"
1275 }
1276 ));
1277 lines.push(format!(" State: {} @ {}", state, position));
1278 }
1279 None => {
1280 lines.push(String::new());
1281 lines.push("Video: none active".into());
1282 lines.push("Decoder: idle".into());
1283 }
1284 }
1285
1286 IpcResponse {
1287 success: true,
1288 message: Some(lines.join("\n")),
1289 }
1290 }
1291 IpcCommand::MonitorList => {
1292 let mut lines = Vec::new();
1293 for (name, rs) in &render_states {
1294 let lock = rs.lock().await;
1295 lines.push(format!("{}: {}x{}", name, lock.width, lock.height));
1296 }
1297 if lines.is_empty() {
1298 IpcResponse {
1299 success: true,
1300 message: Some("No monitors connected".into()),
1301 }
1302 } else {
1303 IpcResponse {
1304 success: true,
1305 message: Some(lines.join("\n")),
1306 }
1307 }
1308 }
1309 IpcCommand::MonitorCurrent => {
1310 if let Some((name, rs)) = render_states.iter().next() {
1312 let lock = rs.lock().await;
1313 IpcResponse {
1314 success: true,
1315 message: Some(format!("{}: {}x{}", name, lock.width, lock.height)),
1316 }
1317 } else {
1318 IpcResponse {
1319 success: false,
1320 message: Some("No monitors connected".into()),
1321 }
1322 }
1323 }
1324 }
1325 }
1326 })
1327 .await?;
1328
1329 if self.config.watch.enabled
1331 && let Some(ref watch_dir) = self.config.watch.dir
1332 {
1333 let watch_path = crate::config::expand_path(watch_dir);
1334 self.start_watcher(watch_path, render_states).await?;
1335 }
1336
1337 tokio::task::spawn_blocking(move || {
1338 loop {
1339 if let Err(e) = event_queue.blocking_dispatch(&mut wayland_state) {
1340 eprintln!("Wayland dispatch error: {e:?}");
1341 break;
1342 }
1343 }
1344 eprintln!("wallr: Wayland connection lost, exiting");
1348 std::process::exit(1);
1349 });
1350
1351 loop {
1352 tokio::time::sleep(std::time::Duration::from_secs(3600)).await;
1353 }
1354 }
1355
1356 async fn start_watcher(
1357 &self,
1358 dir: PathBuf,
1359 render_states: std::collections::HashMap<String, Arc<Mutex<RenderState>>>,
1360 ) -> Result<(), DaemonError> {
1361 let engine = self.engine.clone();
1362 let paused = self.paused.clone();
1363 let debounce = crate::config::parse_duration(&self.config.watch.debounce)
1364 .unwrap_or(std::time::Duration::from_millis(500));
1365
1366 let (tx, mut rx) = tokio::sync::mpsc::channel(100);
1367
1368 let mut watcher = notify::recommended_watcher(move |res: Result<Event, notify::Error>| {
1369 if let Ok(event) = res
1370 && let EventKind::Create(_) = event.kind
1371 {
1372 for path in event.paths {
1373 let _ = tx.blocking_send(path);
1374 }
1375 }
1376 })
1377 .map_err(|e| DaemonError::StartError(e.to_string()))?;
1378
1379 watcher
1380 .watch(&dir, RecursiveMode::NonRecursive)
1381 .map_err(|e| DaemonError::StartError(e.to_string()))?;
1382
1383 tokio::spawn(async move {
1384 let _watcher = watcher;
1385 let mut last: Option<(PathBuf, std::time::Instant)> = None;
1386
1387 while let Some(path) = rx.recv().await {
1388 if paused.load(Ordering::SeqCst) {
1389 continue;
1390 }
1391 if let Some((ref lp, ref lt)) = last
1392 && lp == &path
1393 && lt.elapsed() < debounce
1394 {
1395 continue;
1396 }
1397 let ext = path
1398 .extension()
1399 .unwrap_or_default()
1400 .to_string_lossy()
1401 .to_lowercase();
1402 if !["jpg", "jpeg", "png", "gif", "webp"].contains(&ext.as_str()) {
1403 continue;
1404 }
1405 last = Some((path.clone(), std::time::Instant::now()));
1406
1407 for (name, rs) in &render_states {
1409 let rs = rs.clone();
1410 let eng = engine.clone();
1411 let p = path.clone();
1412 let name = name.clone();
1413 tokio::spawn(async move {
1414 let mut lock = rs.lock().await;
1415 let effect =
1416 crate::animation::Effect::Fade(crate::animation::FadeParams::default());
1417 let _ = lock.set_wallpaper(&p, &effect, 600, 0).await;
1418 drop(lock);
1419 let opts = SetOptions {
1420 no_theme: false,
1421 theme_provider: None,
1422 monitor: Some(name),
1423 };
1424 let mut elock = eng.lock().await;
1425 let _ = elock.set_wallpaper(&p, &opts).await;
1426 });
1427 }
1428 }
1429 });
1430
1431 Ok(())
1432 }
1433
1434 #[allow(clippy::too_many_arguments)]
1437 async fn create_render_state_for_output(
1438 renderer: &std::sync::Arc<Renderer>,
1439 display_ptr: *mut std::ffi::c_void,
1440 wayland_state: &mut WaylandState,
1441 qh: &QueueHandle<WaylandState>,
1442 layer_shell: &LayerShell,
1443 compositor: &wl_compositor::WlCompositor,
1444 output: &OutputInfo,
1445 config: &WallrConfig,
1446 ) -> Result<RenderState, DaemonError> {
1447 let wl_surface = wayland_state.compositor_state.create_surface(qh);
1448 let layer_surface = layer_shell.create_layer_surface(
1449 qh,
1450 wl_surface,
1451 Layer::Background,
1452 Some("wallr"),
1453 Some(&output.wl_output),
1454 );
1455 layer_surface.set_anchor(Anchor::TOP | Anchor::BOTTOM | Anchor::LEFT | Anchor::RIGHT);
1456 layer_surface.set_exclusive_zone(-1);
1457 layer_surface.set_keyboard_interactivity(KeyboardInteractivity::None);
1458
1459 let empty_region = compositor.create_region(qh, ());
1461 layer_surface
1462 .wl_surface()
1463 .set_input_region(Some(&empty_region));
1464 layer_surface.commit();
1465 empty_region.destroy();
1466
1467 let scale_factor = if output.scale_factor > 0 {
1468 output.scale_factor
1469 } else {
1470 1
1471 };
1472 layer_surface.wl_surface().set_buffer_scale(scale_factor);
1473
1474 let width = output.width * scale_factor as u32;
1475 let height = output.height * scale_factor as u32;
1476
1477 let raw_surface = layer_surface.wl_surface().id().as_ptr() as *mut std::ffi::c_void;
1478 wayland_state
1479 .surfaces
1480 .push((output.wl_output.id().protocol_id(), layer_surface));
1481
1482 let window_handle = WaylandWindow {
1483 display: display_ptr,
1484 surface: raw_surface,
1485 };
1486
1487 let wgpu_surface = renderer
1488 .instance
1489 .create_surface(&window_handle)
1490 .map_err(|e| DaemonError::StartError(format!("wgpu surface creation failed: {e:?}")))?;
1491
1492 let adapter = renderer
1493 .instance
1494 .request_adapter(&wgpu::RequestAdapterOptions {
1495 compatible_surface: Some(&wgpu_surface),
1496 power_preference: wgpu::PowerPreference::HighPerformance,
1497 force_fallback_adapter: false,
1498 })
1499 .await;
1500 let surf_format = adapter
1501 .as_ref()
1502 .map(|a| {
1503 let caps = wgpu_surface.get_capabilities(a);
1504 caps.formats
1505 .into_iter()
1506 .next()
1507 .unwrap_or(wgpu::TextureFormat::Bgra8UnormSrgb)
1508 })
1509 .unwrap_or(wgpu::TextureFormat::Bgra8UnormSrgb);
1510
1511 let surf_config = wgpu::SurfaceConfiguration {
1512 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
1513 format: surf_format,
1514 width,
1515 height,
1516 present_mode: wgpu::PresentMode::Fifo,
1517 alpha_mode: wgpu::CompositeAlphaMode::Opaque,
1518 view_formats: vec![],
1519 desired_maximum_frame_latency: 2,
1520 };
1521 wgpu_surface.configure(&renderer.device, &surf_config);
1522
1523 let wgpu_surface: wgpu::Surface<'static> = unsafe { std::mem::transmute(wgpu_surface) };
1526 let surface: &'static wgpu::Surface<'static> = Box::leak(Box::new(wgpu_surface));
1527
1528 Ok(RenderState {
1529 renderer: renderer.clone(),
1530 surface,
1531 render_lock: std::sync::Arc::new(std::sync::Mutex::new(())),
1532 playback_gen: std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0)),
1533 pacer: std::sync::Arc::new(LivePacer::new()),
1534 current_bind: None,
1535 current_tex: None,
1536 width,
1537 height,
1538 current_width: 0,
1539 current_height: 0,
1540 format: surf_format,
1541 video_playback: std::sync::Arc::new(crate::video::VideoPlayback::new()),
1542 hw_accel: crate::video::HwAccel::from_config(&config.video.hw_decode),
1543 scaling_mode: 0,
1544 })
1545 }
1546}