Skip to main content

wallr_core/daemon/
mod.rs

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_output, wl_surface},
32};
33
34#[derive(Debug, thiserror::Error)]
35pub enum DaemonError {
36    #[error("daemon already running: {0}")]
37    AlreadyRunning(String),
38    #[error("failed to start daemon: {0}")]
39    StartError(String),
40    #[error("I/O error: {0}")]
41    Io(#[from] std::io::Error),
42    #[error("IPC error: {0}")]
43    Ipc(#[from] crate::ipc::IpcError),
44    #[error("Config error: {0}")]
45    Config(#[from] crate::config::ConfigError),
46    #[error("Wallpaper error: {0}")]
47    Wallpaper(#[from] crate::wallpaper::WallpaperError),
48}
49
50pub struct WaylandWindow {
51    pub display: *mut std::ffi::c_void,
52    pub surface: *mut std::ffi::c_void,
53}
54
55unsafe impl Send for WaylandWindow {}
56unsafe impl Sync for WaylandWindow {}
57
58impl HasWindowHandle for WaylandWindow {
59    fn window_handle(&self) -> Result<WindowHandle<'_>, raw_window_handle::HandleError> {
60        let surface = std::ptr::NonNull::new(self.surface)
61            .ok_or(raw_window_handle::HandleError::Unavailable)?;
62        let handle = WaylandWindowHandle::new(surface);
63        unsafe { Ok(WindowHandle::borrow_raw(RawWindowHandle::Wayland(handle))) }
64    }
65}
66
67impl HasDisplayHandle for WaylandWindow {
68    fn display_handle(&self) -> Result<DisplayHandle<'_>, raw_window_handle::HandleError> {
69        let display = std::ptr::NonNull::new(self.display)
70            .ok_or(raw_window_handle::HandleError::Unavailable)?;
71        let handle = WaylandDisplayHandle::new(display);
72        unsafe { Ok(DisplayHandle::borrow_raw(RawDisplayHandle::Wayland(handle))) }
73    }
74}
75
76struct WaylandState {
77    registry_state: RegistryState,
78    output_state: OutputState,
79    compositor_state: CompositorState,
80    shm: Shm,
81    surfaces: Vec<LayerSurface>,
82    width: u32,
83    height: u32,
84    scale_factor: i32,
85}
86
87impl ProvidesRegistryState for WaylandState {
88    fn registry(&mut self) -> &mut RegistryState {
89        &mut self.registry_state
90    }
91
92    registry_handlers![OutputState,];
93}
94
95impl CompositorHandler for WaylandState {
96    fn scale_factor_changed(
97        &mut self,
98        _conn: &Connection,
99        _qh: &QueueHandle<Self>,
100        _surface: &wl_surface::WlSurface,
101        new_factor: i32,
102    ) {
103        self.scale_factor = new_factor;
104    }
105    fn transform_changed(
106        &mut self,
107        _conn: &Connection,
108        _qh: &QueueHandle<Self>,
109        _surface: &wl_surface::WlSurface,
110        _new_transform: wl_output::Transform,
111    ) {
112    }
113    fn frame(
114        &mut self,
115        _conn: &Connection,
116        _qh: &QueueHandle<Self>,
117        _surface: &wl_surface::WlSurface,
118        _time: u32,
119    ) {
120    }
121    fn surface_enter(
122        &mut self,
123        _conn: &Connection,
124        _qh: &QueueHandle<Self>,
125        _surface: &wl_surface::WlSurface,
126        _output: &wl_output::WlOutput,
127    ) {
128    }
129    fn surface_leave(
130        &mut self,
131        _conn: &Connection,
132        _qh: &QueueHandle<Self>,
133        _surface: &wl_surface::WlSurface,
134        _output: &wl_output::WlOutput,
135    ) {
136    }
137}
138
139impl LayerShellHandler for WaylandState {
140    fn configure(
141        &mut self,
142        _conn: &Connection,
143        _qh: &QueueHandle<Self>,
144        layer: &LayerSurface,
145        configure: LayerSurfaceConfigure,
146        _serial: u32,
147    ) {
148        if configure.new_size.0 > 0 {
149            self.width = configure.new_size.0;
150        }
151        if configure.new_size.1 > 0 {
152            self.height = configure.new_size.1;
153        }
154        layer.commit();
155    }
156
157    fn closed(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _layer: &LayerSurface) {}
158}
159
160impl ShmHandler for WaylandState {
161    fn shm_state(&mut self) -> &mut Shm {
162        &mut self.shm
163    }
164}
165
166impl OutputHandler for WaylandState {
167    fn output_state(&mut self) -> &mut OutputState {
168        &mut self.output_state
169    }
170    fn new_output(
171        &mut self,
172        _conn: &Connection,
173        _qh: &QueueHandle<Self>,
174        _output: wl_output::WlOutput,
175    ) {
176    }
177    fn update_output(
178        &mut self,
179        _conn: &Connection,
180        _qh: &QueueHandle<Self>,
181        _output: wl_output::WlOutput,
182    ) {
183    }
184    fn output_destroyed(
185        &mut self,
186        _conn: &Connection,
187        _qh: &QueueHandle<Self>,
188        _output: wl_output::WlOutput,
189    ) {
190    }
191}
192
193delegate_compositor!(WaylandState);
194delegate_layer!(WaylandState);
195delegate_output!(WaylandState);
196delegate_registry!(WaylandState);
197delegate_shm!(WaylandState);
198
199struct RenderState {
200    renderer: std::sync::Arc<Renderer>,
201    surface: &'static wgpu::Surface<'static>,
202    /// Serializes transition rendering. The lock is only ever held by the
203    /// detached render task, never by the IPC loop, so a stalled present
204    /// cannot freeze the daemon.
205    render_lock: std::sync::Arc<std::sync::Mutex<()>>,
206    /// Bumped on every commit. Live playback checks it each frame and stops
207    /// as soon as a new wallpaper supersedes the one it is playing.
208    playback_gen: std::sync::Arc<std::sync::atomic::AtomicU64>,
209    current_bind: Option<wgpu::BindGroup>,
210    current_tex: Option<wgpu::Texture>,
211    width: u32,
212    height: u32,
213    current_width: u32,
214    current_height: u32,
215    format: wgpu::TextureFormat,
216}
217
218/// Everything the transition render task needs; the daemon state has already
219/// been promoted to the new wallpaper before a transition is spawned.
220struct CommitData {
221    bg_bind: wgpu::BindGroup,
222    new_bind: wgpu::BindGroup,
223    img_width: u32,
224    img_height: u32,
225    old_img_width: u32,
226    old_img_height: u32,
227    format: wgpu::TextureFormat,
228    width: u32,
229    height: u32,
230    /// Animated frames to play live after the transition, when the committed
231    /// file is a GIF.
232    animated: Option<crate::animated::AnimatedImage>,
233    /// Playback generation captured at commit time; live playback stops when
234    /// it no longer matches `RenderState::playback_gen`.
235    generation: u64,
236}
237
238impl RenderState {
239    async fn set_wallpaper(
240        &mut self,
241        path: &std::path::Path,
242        effect: &crate::animation::Effect,
243        duration_ms: u32,
244    ) -> anyhow::Result<()> {
245        let commit = self.commit_wallpaper(path)?;
246        self.spawn_transition(commit, effect, duration_ms);
247        Ok(())
248    }
249
250    /// Loads the new wallpaper and atomically promotes it to the current
251    /// frame. The outgoing bind group stays alive for the transition, so the
252    /// render task can keep drawing from it after this commit returns.
253    fn commit_wallpaper(&mut self, path: &std::path::Path) -> anyhow::Result<CommitData> {
254        use image::ImageReader;
255
256        // Decode animated frames first (cheap for static images: a six-byte
257        // magic sniff); the first frame also serves as the transition's
258        // incoming image.
259        let animated = crate::animated::AnimatedImage::decode(path)?;
260        let new_img = ImageReader::open(path)?.decode()?;
261        let (new_tex, new_bind) = self.renderer.load_texture(&new_img)?;
262        let img_width = new_img.width();
263        let img_height = new_img.height();
264
265        let old_bind = self.current_bind.take();
266        let (old_img_width, old_img_height) = if old_bind.is_some() {
267            (self.current_width.max(1), self.current_height.max(1))
268        } else {
269            (img_width, img_height)
270        };
271        // Keep the last image as the outgoing frame. On the first ever run,
272        // using the incoming image for both sides is a clean no-op transition;
273        // it avoids a black flash while still allowing the cached wallpaper
274        // restored at daemon startup to become the real outgoing frame.
275        let bg_bind = old_bind.unwrap_or_else(|| new_bind.clone());
276
277        drop(self.current_tex.take());
278        self.current_tex = Some(new_tex);
279        self.current_bind = Some(new_bind.clone());
280        self.current_width = img_width;
281        self.current_height = img_height;
282
283        let generation = self.playback_gen.fetch_add(1, Ordering::SeqCst) + 1;
284
285        Ok(CommitData {
286            bg_bind,
287            new_bind,
288            img_width,
289            img_height,
290            old_img_width,
291            old_img_height,
292            format: self.format,
293            width: self.width,
294            height: self.height,
295            animated,
296            generation,
297        })
298    }
299
300    /// Renders the committed transition on a detached blocking task. The IPC
301    /// path never waits on GPU presents, so a stalled compositor (monitor
302    /// off, suspend) cannot hang the daemon. Transitions are serialized by
303    /// the render lock: a later one simply waits until the earlier drains.
304    fn spawn_transition(
305        &self,
306        commit: CommitData,
307        effect: &crate::animation::Effect,
308        duration_ms: u32,
309    ) {
310        let renderer = self.renderer.clone();
311        let surface: &'static wgpu::Surface<'static> = self.surface;
312        let render_lock = self.render_lock.clone();
313        let playback_gen = self.playback_gen.clone();
314        let effect = effect.clone();
315        drop(tokio::task::spawn_blocking(move || {
316            render_transition(
317                renderer,
318                surface,
319                render_lock,
320                playback_gen,
321                commit,
322                effect,
323                duration_ms,
324            );
325        }));
326    }
327}
328
329/// Presents one frame per vsync until the wall-clock duration elapses. With
330/// PresentMode::Fifo, `get_current_texture` blocks until the previous frame
331/// is presented, so this loop is paced to the monitor refresh rate, and the
332/// transition lasts exactly `duration_ms` on any refresh rate — frame-count
333/// pacing would run too fast on high-refresh panels and too slow when the
334/// present rate is low. If the compositor stops presenting, the loop can park
335/// inside a present; that is fine here because the task is detached.
336fn render_transition(
337    renderer: std::sync::Arc<Renderer>,
338    surface: &'static wgpu::Surface<'static>,
339    render_lock: std::sync::Arc<std::sync::Mutex<()>>,
340    playback_gen: std::sync::Arc<std::sync::atomic::AtomicU64>,
341    commit: CommitData,
342    effect: crate::animation::Effect,
343    duration_ms: u32,
344) {
345    let _guard = render_lock
346        .lock()
347        .unwrap_or_else(|poisoned| poisoned.into_inner());
348
349    let duration = std::time::Duration::from_millis(u64::from(duration_ms.max(1)));
350    let start = std::time::Instant::now();
351    loop {
352        let progress = start.elapsed().as_secs_f32() / duration.as_secs_f32();
353        let uniforms = crate::animation::compute_effect_uniforms(&effect, progress.clamp(0.0, 1.0));
354        let status = renderer.render_frame(crate::renderer::FrameRequest {
355            surface,
356            format: commit.format,
357            bg_bind: &commit.bg_bind,
358            new_bind: &commit.new_bind,
359            effect: &uniforms,
360            width: commit.width,
361            height: commit.height,
362            img_width: commit.img_width,
363            img_height: commit.img_height,
364            old_img_width: commit.old_img_width,
365            old_img_height: commit.old_img_height,
366        });
367        let status = match status {
368            Ok(status) => status,
369            Err(err) => {
370                eprintln!("wallr: transition render failed: {err}");
371                break;
372            }
373        };
374        if progress >= 1.0 || status == crate::renderer::FrameStatus::TimedOut {
375            break;
376        }
377    }
378
379    // The transition ended; if the committed wallpaper is an animated GIF and
380    // nothing superseded it while we rendered, keep the render lock and play
381    // the frames live until the next commit bumps the generation.
382    if let Some(animated) = &commit.animated
383        && playback_gen.load(Ordering::SeqCst) == commit.generation
384    {
385        play_live(&renderer, surface, &commit, animated, &playback_gen);
386    }
387}
388
389/// Presents live wallpaper frames until the next commit. One frame is
390/// presented per vsync (Fifo), and texture uploads only happen when the
391/// playhead crosses into a new GIF frame, so playback is smooth without
392/// burning GPU bandwidth on unchanged pixels.
393fn play_live(
394    renderer: &Renderer,
395    surface: &'static wgpu::Surface<'static>,
396    commit: &CommitData,
397    animated: &crate::animated::AnimatedImage,
398    playback_gen: &std::sync::atomic::AtomicU64,
399) {
400    let (texture, bind) = renderer.create_texture(animated.width, animated.height);
401    renderer.update_texture(
402        &texture,
403        animated.first_frame(),
404        animated.width,
405        animated.height,
406    );
407
408    let mut shown = usize::MAX;
409    let start = std::time::Instant::now();
410    let static_effect = crate::animation::Effect::Fade(crate::animation::FadeParams::default());
411    loop {
412        if playback_gen.load(Ordering::SeqCst) != commit.generation {
413            return;
414        }
415        let index = animated.frame_index_at(start.elapsed());
416        if index != shown {
417            renderer.update_texture(
418                &texture,
419                animated.frame_at(index),
420                animated.width,
421                animated.height,
422            );
423            shown = index;
424        }
425        let uniforms = crate::animation::compute_effect_uniforms(&static_effect, 1.0);
426        let status = renderer.render_frame(crate::renderer::FrameRequest {
427            surface,
428            format: commit.format,
429            bg_bind: &bind,
430            new_bind: &bind,
431            effect: &uniforms,
432            width: commit.width,
433            height: commit.height,
434            img_width: animated.width,
435            img_height: animated.height,
436            old_img_width: animated.width,
437            old_img_height: animated.height,
438        });
439        match status {
440            Ok(crate::renderer::FrameStatus::Presented) => {}
441            // A stalled present parks inside the acquire; a Timeout or error
442            // means the surface is unusable, so give up and let the next
443            // transition take over.
444            _ => return,
445        }
446    }
447}
448
449pub struct Daemon {
450    config: WallrConfig,
451    paused: Arc<AtomicBool>,
452    engine: Arc<Mutex<WallpaperEngine>>,
453}
454
455impl Daemon {
456    pub fn new(config: WallrConfig) -> Result<Self, DaemonError> {
457        let engine = WallpaperEngine::new(config.clone())?;
458        Ok(Self {
459            config,
460            paused: Arc::new(AtomicBool::new(false)),
461            engine: Arc::new(Mutex::new(engine)),
462        })
463    }
464
465    pub async fn start(self) -> Result<(), DaemonError> {
466        let socket_path = crate::config::expand_path(&self.config.daemon.socket);
467        if socket_path.exists() {
468            if tokio::net::UnixStream::connect(&socket_path).await.is_ok() {
469                return Err(DaemonError::AlreadyRunning(
470                    socket_path.to_string_lossy().to_string(),
471                ));
472            }
473            let _ = std::fs::remove_file(&socket_path);
474        }
475
476        let renderer = Renderer::new()
477            .await
478            .map_err(|e| DaemonError::StartError(format!("GPU init failed: {e}")))?;
479
480        let conn = Connection::connect_to_env()
481            .map_err(|e| DaemonError::StartError(format!("Failed to connect to Wayland: {e:?}")))?;
482        let backend = conn.backend();
483        let display_ptr = backend.display_ptr() as *mut std::ffi::c_void;
484
485        let (globals, mut event_queue) = registry_queue_init(&conn)
486            .map_err(|e| DaemonError::StartError(format!("registry_queue_init failed: {e:?}")))?;
487        let qh = event_queue.handle();
488
489        let compositor_state = CompositorState::bind(&globals, &qh)
490            .map_err(|e| DaemonError::StartError(format!("compositor bind failed: {e:?}")))?;
491        let layer_shell = LayerShell::bind(&globals, &qh)
492            .map_err(|e| DaemonError::StartError(format!("layer_shell bind failed: {e:?}")))?;
493        let shm = Shm::bind(&globals, &qh)
494            .map_err(|e| DaemonError::StartError(format!("shm bind failed: {e:?}")))?;
495
496        let mut wayland_state = WaylandState {
497            registry_state: RegistryState::new(&globals),
498            output_state: OutputState::new(&globals, &qh),
499            compositor_state,
500            shm,
501            surfaces: Vec::new(),
502            width: 1920,
503            height: 1080,
504            scale_factor: 1,
505        };
506
507        let wl_surface = wayland_state.compositor_state.create_surface(&qh);
508        let layer_surface = layer_shell.create_layer_surface(
509            &qh,
510            wl_surface,
511            Layer::Background,
512            Some("wallr"),
513            None,
514        );
515        layer_surface.set_anchor(Anchor::TOP | Anchor::BOTTOM | Anchor::LEFT | Anchor::RIGHT);
516        layer_surface.set_exclusive_zone(-1);
517        layer_surface.set_keyboard_interactivity(KeyboardInteractivity::None);
518        layer_surface.commit();
519
520        event_queue
521            .roundtrip(&mut wayland_state)
522            .map_err(|e| DaemonError::StartError(format!("roundtrip failed: {e:?}")))?;
523        event_queue
524            .roundtrip(&mut wayland_state)
525            .map_err(|e| DaemonError::StartError(format!("roundtrip2 failed: {e:?}")))?;
526
527        let scale_factor = if wayland_state.scale_factor > 0 {
528            wayland_state.scale_factor
529        } else {
530            1
531        };
532        layer_surface.wl_surface().set_buffer_scale(scale_factor);
533
534        let width = wayland_state.width * scale_factor as u32;
535        let height = wayland_state.height * scale_factor as u32;
536
537        let raw_surface = layer_surface.wl_surface().id().as_ptr() as *mut std::ffi::c_void;
538        wayland_state.surfaces.push(layer_surface);
539
540        let window_handle = WaylandWindow {
541            display: display_ptr,
542            surface: raw_surface,
543        };
544
545        let wgpu_surface = renderer
546            .instance
547            .create_surface(&window_handle)
548            .map_err(|e| DaemonError::StartError(format!("wgpu surface creation failed: {e:?}")))?;
549
550        let adapter = renderer
551            .instance
552            .request_adapter(&wgpu::RequestAdapterOptions {
553                compatible_surface: Some(&wgpu_surface),
554                power_preference: wgpu::PowerPreference::HighPerformance,
555                force_fallback_adapter: false,
556            })
557            .await;
558        let surf_format = adapter
559            .as_ref()
560            .map(|a| {
561                let caps = wgpu_surface.get_capabilities(a);
562                caps.formats
563                    .into_iter()
564                    .next()
565                    .unwrap_or(wgpu::TextureFormat::Bgra8UnormSrgb)
566            })
567            .unwrap_or(wgpu::TextureFormat::Bgra8UnormSrgb);
568
569        let surf_config = wgpu::SurfaceConfiguration {
570            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
571            format: surf_format,
572            width,
573            height,
574            present_mode: wgpu::PresentMode::Fifo,
575            alpha_mode: wgpu::CompositeAlphaMode::Opaque,
576            view_formats: vec![],
577            desired_maximum_frame_latency: 2,
578        };
579        wgpu_surface.configure(&renderer.device, &surf_config);
580
581        // SAFETY: We transmute the surface lifetime to 'static so it can be moved
582        // into the shared Arc. The surface is tied to window_handle / wayland_state
583        // both of which live as long as the process.
584        let wgpu_surface: wgpu::Surface<'static> = unsafe { std::mem::transmute(wgpu_surface) };
585        // The daemon lives for the whole process, so leaking one surface is fine
586        // and gives every detached transition task a stable reference to present
587        // to without holding the RenderState lock during the blocking acquire.
588        let surface: &'static wgpu::Surface<'static> = Box::leak(Box::new(wgpu_surface));
589
590        let render_state = Arc::new(Mutex::new(RenderState {
591            renderer: std::sync::Arc::new(renderer),
592            surface,
593            render_lock: std::sync::Arc::new(std::sync::Mutex::new(())),
594            playback_gen: std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0)),
595            current_bind: None,
596            current_tex: None,
597            width,
598            height,
599            current_width: 0,
600            current_height: 0,
601            format: surf_format,
602        }));
603
604        {
605            let state_path = dirs::cache_dir()
606                .unwrap_or_else(|| std::path::PathBuf::from("/tmp"))
607                .join("wallr/last_wallpaper");
608            if let Ok(path_str) = std::fs::read_to_string(&state_path) {
609                let p = std::path::Path::new(path_str.trim());
610                if p.exists() {
611                    let mut rs = render_state.lock().await;
612                    let effect =
613                        crate::animation::Effect::Fade(crate::animation::FadeParams::default());
614                    let _ = rs.set_wallpaper(p, &effect, 0).await;
615                }
616            }
617        }
618
619        let paused_clone = self.paused.clone();
620        let engine_clone = self.engine.clone();
621        let render_state_clone = render_state.clone();
622
623        start_ipc_server(&socket_path, move |cmd| {
624            let paused = paused_clone.clone();
625            let engine = engine_clone.clone();
626            let rs = render_state_clone.clone();
627            async move {
628                match cmd {
629                    IpcCommand::Pause => {
630                        paused.store(true, Ordering::SeqCst);
631                        IpcResponse {
632                            success: true,
633                            message: Some("Paused".into()),
634                        }
635                    }
636                    IpcCommand::Resume => {
637                        paused.store(false, Ordering::SeqCst);
638                        IpcResponse {
639                            success: true,
640                            message: Some("Resumed".into()),
641                        }
642                    }
643                    IpcCommand::Reload => {
644                        let lock = engine.lock().await;
645                        match lock.reload() {
646                            Ok(_) => IpcResponse {
647                                success: true,
648                                message: Some("Reloaded".into()),
649                            },
650                            Err(e) => IpcResponse {
651                                success: false,
652                                message: Some(e.to_string()),
653                            },
654                        }
655                    }
656                    IpcCommand::Preview {
657                        path,
658                        effect,
659                        duration_ms,
660                        no_theme,
661                        theme_override,
662                        monitor,
663                    } => {
664                        if paused.load(Ordering::SeqCst) {
665                            return IpcResponse {
666                                success: false,
667                                message: Some("Daemon is paused".into()),
668                            };
669                        }
670                        let p = std::path::PathBuf::from(&path);
671                        if !p.exists() {
672                            return IpcResponse {
673                                success: false,
674                                message: Some(format!("File not found: {}", path)),
675                            };
676                        }
677
678                        let state_path = dirs::cache_dir()
679                            .unwrap_or_else(|| std::path::PathBuf::from("/tmp"))
680                            .join("wallr/last_wallpaper");
681                        if let Some(parent) = state_path.parent() {
682                            let _ = std::fs::create_dir_all(parent);
683                        }
684                        let _ = std::fs::write(&state_path, &path);
685
686                        let effect = effect.unwrap_or_else(|| {
687                            crate::animation::Effect::Fade(crate::animation::FadeParams::default())
688                        });
689                        let duration = duration_ms.unwrap_or(2000);
690
691                        let rs_clone = rs.clone();
692                        let p_clone = p.clone();
693                        let result = tokio::task::spawn_blocking(move || {
694                            let rt = tokio::runtime::Handle::current();
695                            rt.block_on(async {
696                                let mut lock = rs_clone.lock().await;
697                                lock.set_wallpaper(&p_clone, &effect, duration).await
698                            })
699                        })
700                        .await;
701
702                        match result {
703                            Ok(Ok(())) => {
704                                let opts = SetOptions {
705                                    no_theme,
706                                    theme_provider: theme_override,
707                                    monitor,
708                                };
709                                let mut eng = engine.lock().await;
710                                match eng.set_wallpaper(&p, &opts).await {
711                                    Ok(()) => IpcResponse {
712                                        success: true,
713                                        message: None,
714                                    },
715                                    Err(e) => IpcResponse {
716                                        success: true,
717                                        message: Some(format!(
718                                            "Wallpaper set, but hooks/theme failed: {e}"
719                                        )),
720                                    },
721                                }
722                            }
723                            Ok(Err(e)) => IpcResponse {
724                                success: false,
725                                message: Some(format!("Render failed: {}", e)),
726                            },
727                            Err(e) => IpcResponse {
728                                success: false,
729                                message: Some(format!("Task spawn failed: {}", e)),
730                            },
731                        }
732                    }
733                    IpcCommand::Stop => {
734                        tokio::spawn(async {
735                            tokio::time::sleep(std::time::Duration::from_millis(300)).await;
736                            std::process::exit(0);
737                        });
738                        IpcResponse {
739                            success: true,
740                            message: Some("Stopping".into()),
741                        }
742                    }
743                    IpcCommand::Status => {
744                        let state = if paused.load(Ordering::SeqCst) {
745                            "paused"
746                        } else {
747                            "running"
748                        };
749                        IpcResponse {
750                            success: true,
751                            message: Some(format!("wallr daemon {}", state)),
752                        }
753                    }
754                }
755            }
756        })
757        .await?;
758
759        // Start file watcher if configured
760        if self.config.watch.enabled
761            && let Some(ref watch_dir) = self.config.watch.dir
762        {
763            let watch_path = crate::config::expand_path(watch_dir);
764            self.start_watcher(watch_path, render_state.clone()).await?;
765        }
766
767        tokio::task::spawn_blocking(move || {
768            loop {
769                if let Err(e) = event_queue.blocking_dispatch(&mut wayland_state) {
770                    eprintln!("Wayland dispatch error: {e:?}");
771                    break;
772                }
773            }
774        });
775
776        loop {
777            tokio::time::sleep(std::time::Duration::from_secs(3600)).await;
778        }
779    }
780
781    async fn start_watcher(
782        &self,
783        dir: PathBuf,
784        render_state: Arc<Mutex<RenderState>>,
785    ) -> Result<(), DaemonError> {
786        let engine = self.engine.clone();
787        let paused = self.paused.clone();
788        let debounce = crate::config::parse_duration(&self.config.watch.debounce)
789            .unwrap_or(std::time::Duration::from_millis(500));
790
791        let (tx, mut rx) = tokio::sync::mpsc::channel(100);
792
793        let mut watcher = notify::recommended_watcher(move |res: Result<Event, notify::Error>| {
794            if let Ok(event) = res
795                && let EventKind::Create(_) = event.kind
796            {
797                for path in event.paths {
798                    let _ = tx.blocking_send(path);
799                }
800            }
801        })
802        .map_err(|e| DaemonError::StartError(e.to_string()))?;
803
804        watcher
805            .watch(&dir, RecursiveMode::NonRecursive)
806            .map_err(|e| DaemonError::StartError(e.to_string()))?;
807
808        tokio::spawn(async move {
809            let _watcher = watcher;
810            let mut last: Option<(PathBuf, std::time::Instant)> = None;
811
812            while let Some(path) = rx.recv().await {
813                if paused.load(Ordering::SeqCst) {
814                    continue;
815                }
816                if let Some((ref lp, ref lt)) = last
817                    && lp == &path
818                    && lt.elapsed() < debounce
819                {
820                    continue;
821                }
822                let ext = path
823                    .extension()
824                    .unwrap_or_default()
825                    .to_string_lossy()
826                    .to_lowercase();
827                if !["jpg", "jpeg", "png", "gif", "webp"].contains(&ext.as_str()) {
828                    continue;
829                }
830                last = Some((path.clone(), std::time::Instant::now()));
831
832                let rs = render_state.clone();
833                let eng = engine.clone();
834                let p = path.clone();
835                tokio::spawn(async move {
836                    let mut lock = rs.lock().await;
837                    let effect =
838                        crate::animation::Effect::Fade(crate::animation::FadeParams::default());
839                    let _ = lock.set_wallpaper(&p, &effect, 600).await;
840                    drop(lock);
841                    let opts = SetOptions {
842                        no_theme: false,
843                        theme_provider: None,
844                        monitor: None,
845                    };
846                    let mut elock = eng.lock().await;
847                    let _ = elock.set_wallpaper(&p, &opts).await;
848                });
849            }
850        });
851
852        Ok(())
853    }
854}