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_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 WaylandState {
76    registry_state: RegistryState,
77    output_state: OutputState,
78    compositor_state: CompositorState,
79    shm: Shm,
80    surfaces: Vec<LayerSurface>,
81    width: u32,
82    height: u32,
83    scale_factor: i32,
84}
85
86impl ProvidesRegistryState for WaylandState {
87    fn registry(&mut self) -> &mut RegistryState {
88        &mut self.registry_state
89    }
90
91    registry_handlers![OutputState,];
92}
93
94impl CompositorHandler for WaylandState {
95    fn scale_factor_changed(
96        &mut self,
97        _conn: &Connection,
98        _qh: &QueueHandle<Self>,
99        _surface: &wl_surface::WlSurface,
100        new_factor: i32,
101    ) {
102        self.scale_factor = new_factor;
103    }
104    fn transform_changed(
105        &mut self,
106        _conn: &Connection,
107        _qh: &QueueHandle<Self>,
108        _surface: &wl_surface::WlSurface,
109        _new_transform: wl_output::Transform,
110    ) {
111    }
112    fn frame(
113        &mut self,
114        _conn: &Connection,
115        _qh: &QueueHandle<Self>,
116        _surface: &wl_surface::WlSurface,
117        _time: u32,
118    ) {
119    }
120    fn surface_enter(
121        &mut self,
122        _conn: &Connection,
123        _qh: &QueueHandle<Self>,
124        _surface: &wl_surface::WlSurface,
125        _output: &wl_output::WlOutput,
126    ) {
127    }
128    fn surface_leave(
129        &mut self,
130        _conn: &Connection,
131        _qh: &QueueHandle<Self>,
132        _surface: &wl_surface::WlSurface,
133        _output: &wl_output::WlOutput,
134    ) {
135    }
136}
137
138impl wayland_client::Dispatch<wayland_client::protocol::wl_region::WlRegion, ()> for WaylandState {
139    fn event(
140        _state: &mut WaylandState,
141        _region: &wayland_client::protocol::wl_region::WlRegion,
142        _event: wayland_client::protocol::wl_region::Event,
143        _data: &(),
144        _conn: &Connection,
145        _qh: &QueueHandle<WaylandState>,
146    ) {
147    }
148}
149
150impl LayerShellHandler for WaylandState {
151    fn configure(
152        &mut self,
153        _conn: &Connection,
154        _qh: &QueueHandle<Self>,
155        layer: &LayerSurface,
156        configure: LayerSurfaceConfigure,
157        _serial: u32,
158    ) {
159        if configure.new_size.0 > 0 {
160            self.width = configure.new_size.0;
161        }
162        if configure.new_size.1 > 0 {
163            self.height = configure.new_size.1;
164        }
165        // Note: Input region is set once at creation and persists across
166        // configure events. The empty region ensures clicks pass through.
167        layer.commit();
168    }
169
170    fn closed(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _layer: &LayerSurface) {}
171}
172
173impl ShmHandler for WaylandState {
174    fn shm_state(&mut self) -> &mut Shm {
175        &mut self.shm
176    }
177}
178
179impl OutputHandler for WaylandState {
180    fn output_state(&mut self) -> &mut OutputState {
181        &mut self.output_state
182    }
183    fn new_output(
184        &mut self,
185        _conn: &Connection,
186        _qh: &QueueHandle<Self>,
187        _output: wl_output::WlOutput,
188    ) {
189    }
190    fn update_output(
191        &mut self,
192        _conn: &Connection,
193        _qh: &QueueHandle<Self>,
194        _output: wl_output::WlOutput,
195    ) {
196    }
197    fn output_destroyed(
198        &mut self,
199        _conn: &Connection,
200        _qh: &QueueHandle<Self>,
201        _output: wl_output::WlOutput,
202    ) {
203    }
204}
205
206delegate_compositor!(WaylandState);
207delegate_layer!(WaylandState);
208delegate_output!(WaylandState);
209delegate_registry!(WaylandState);
210delegate_shm!(WaylandState);
211
212/// Wakes paced live-playback loops when a new commit bumps the generation.
213struct LivePacer {
214    lock: std::sync::Mutex<()>,
215    cond: std::sync::Condvar,
216}
217
218impl LivePacer {
219    fn new() -> Self {
220        Self {
221            lock: std::sync::Mutex::new(()),
222            cond: std::sync::Condvar::new(),
223        }
224    }
225
226    fn notify(&self) {
227        let _guard = self.lock.lock().unwrap();
228        self.cond.notify_all();
229    }
230
231    /// Blocks until `deadline` or until `notify` is called, whichever comes
232    /// first.
233    fn wait_until(&self, deadline: std::time::Instant) {
234        let guard = self.lock.lock().unwrap();
235        let now = std::time::Instant::now();
236        if deadline <= now {
237            return;
238        }
239        let _ = self
240            .cond
241            .wait_timeout_while(guard, deadline - now, |_| true);
242    }
243}
244
245struct RenderState {
246    renderer: std::sync::Arc<Renderer>,
247    surface: &'static wgpu::Surface<'static>,
248    /// Serializes transition rendering. The lock is only ever held by the
249    /// detached render task, never by the IPC loop, so a stalled present
250    /// cannot freeze the daemon.
251    render_lock: std::sync::Arc<std::sync::Mutex<()>>,
252    /// Bumped on every commit. Live playback checks it each frame and stops
253    /// as soon as a new wallpaper supersedes the one it is playing.
254    playback_gen: std::sync::Arc<std::sync::atomic::AtomicU64>,
255    /// Wakes paced live-playback loops when a new commit bumps the generation,
256    /// so an old player exits immediately instead of after its sleep quantum.
257    pacer: std::sync::Arc<LivePacer>,
258    current_bind: Option<wgpu::BindGroup>,
259    current_tex: Option<wgpu::Texture>,
260    width: u32,
261    height: u32,
262    current_width: u32,
263    current_height: u32,
264    format: wgpu::TextureFormat,
265    /// Video playback manager
266    video_playback: std::sync::Arc<crate::video::VideoPlayback>,
267    /// Hardware backend to request for new decoders (from `video.hw_decode`).
268    hw_accel: crate::video::HwAccel,
269    /// Current scaling mode for live playback.
270    scaling_mode: u32,
271}
272
273/// Everything the transition render task needs; the daemon state has already
274/// been promoted to the new wallpaper before a transition is spawned.
275struct CommitData {
276    bg_bind: wgpu::BindGroup,
277    new_bind: wgpu::BindGroup,
278    img_width: u32,
279    img_height: u32,
280    old_img_width: u32,
281    old_img_height: u32,
282    format: wgpu::TextureFormat,
283    width: u32,
284    height: u32,
285    /// Animated frames to play live after the transition, when the committed
286    /// file is a GIF.
287    animated: Option<crate::animated::AnimatedImage>,
288    /// Video metadata when committed file is a video.
289    is_video: bool,
290    /// Playback generation captured at commit time; live playback stops when
291    /// it no longer matches `RenderState::playback_gen`.
292    generation: u64,
293    /// Scaling mode: 0=Fill, 1=Fit, 2=Stretch, 3=Center, 4=Tile.
294    scaling_mode: u32,
295}
296
297impl RenderState {
298    async fn set_wallpaper(
299        &mut self,
300        path: &std::path::Path,
301        effect: &crate::animation::Effect,
302        duration_ms: u32,
303        scaling_mode: u32,
304    ) -> anyhow::Result<()> {
305        self.scaling_mode = scaling_mode;
306        let commit = self.commit_wallpaper(path, scaling_mode)?;
307        self.spawn_transition(commit, effect, duration_ms);
308        Ok(())
309    }
310
311    /// Loads the new wallpaper and atomically promotes it to the current
312    /// frame. The outgoing bind group stays alive for the transition, so the
313    /// render task can keep drawing from it after this commit returns.
314    fn commit_wallpaper(
315        &mut self,
316        path: &std::path::Path,
317        scaling_mode: u32,
318    ) -> anyhow::Result<CommitData> {
319        use image::ImageReader;
320
321        // Check if this is a video file FIRST
322        if crate::video::VideoDecoder::is_video_file(path) {
323            tracing::info!("Video file detected: {:?}", path);
324
325            // Invalidate any in-flight video render task BEFORE touching the
326            // shared decoder: the old task checks the generation on every
327            // iteration, so bumping first makes it exit (or skip the upload)
328            // before it could pull a frame of the new resolution from the
329            // freshly started decoder.
330            let generation = self.playback_gen.fetch_add(1, Ordering::SeqCst) + 1;
331            self.pacer.notify();
332
333            // Start video playback (replaces any previous playback and joins
334            // its decode thread, releasing the old decoder's buffers).
335            let metadata = self.video_playback.start(path, self.hw_accel)?;
336
337            // Wait for the first frame so the transition's incoming image is
338            // the real first frame, not a black placeholder.
339            let first_frame = self
340                .video_playback
341                .wait_first_frame(std::time::Duration::from_millis(1000));
342
343            let (new_tex, new_bind, img_width, img_height) = if let Some(frame) = first_frame {
344                let (tex, bind) = self.renderer.create_texture(frame.width, frame.height);
345                self.renderer
346                    .update_texture(&tex, &frame.data, frame.width, frame.height);
347                (tex, bind, frame.width, frame.height)
348            } else {
349                // Fallback: create black texture
350                tracing::warn!("No first frame available, using black texture");
351                let (tex, bind) = self
352                    .renderer
353                    .create_texture(metadata.width, metadata.height);
354                let black = vec![0u8; (metadata.width * metadata.height * 4) as usize];
355                self.renderer
356                    .update_texture(&tex, &black, metadata.width, metadata.height);
357                (tex, bind, metadata.width, metadata.height)
358            };
359
360            let old_bind = self.current_bind.take();
361            let (old_img_width, old_img_height) = if old_bind.is_some() {
362                (self.current_width.max(1), self.current_height.max(1))
363            } else {
364                (img_width, img_height)
365            };
366            let bg_bind = old_bind.unwrap_or_else(|| new_bind.clone());
367
368            drop(self.current_tex.take());
369            self.current_tex = Some(new_tex);
370            self.current_bind = Some(new_bind.clone());
371            self.current_width = img_width;
372            self.current_height = img_height;
373
374            return Ok(CommitData {
375                bg_bind,
376                new_bind,
377                img_width,
378                img_height,
379                old_img_width,
380                old_img_height,
381                format: self.format,
382                width: self.width,
383                height: self.height,
384                animated: None,
385                is_video: true,
386                generation,
387                scaling_mode,
388            });
389        }
390
391        // A static image or GIF supersedes any video: release the video
392        // decoder and its buffers immediately (the generation bump also stops
393        // the video render task on its next vsync).
394        self.video_playback.stop();
395
396        // Stream animated frames (GIF) on demand during playback; the
397        // transition's incoming texture is the GIF's first frame.
398        let mut animated = crate::animated::AnimatedImage::decode(path)?;
399        let (new_tex, new_bind, img_width, img_height) = if let Some(anim) = animated.as_mut() {
400            let (w, h) = (anim.width, anim.height);
401            let (tex, bind) = self.renderer.create_texture(w, h);
402            let first = anim.first_frame();
403            if !first.is_empty() {
404                self.renderer.update_texture(&tex, first, w, h);
405            }
406            (tex, bind, w, h)
407        } else {
408            let new_img = ImageReader::open(path)?.decode()?;
409            let (tex, bind) = self.renderer.load_texture(&new_img)?;
410            (tex, bind, new_img.width(), new_img.height())
411        };
412
413        let old_bind = self.current_bind.take();
414        let (old_img_width, old_img_height) = if old_bind.is_some() {
415            (self.current_width.max(1), self.current_height.max(1))
416        } else {
417            (img_width, img_height)
418        };
419        // Keep the last image as the outgoing frame. On the first ever run,
420        // using the incoming image for both sides is a clean no-op transition;
421        // it avoids a black flash while still allowing the cached wallpaper
422        // restored at daemon startup to become the real outgoing frame.
423        let bg_bind = old_bind.unwrap_or_else(|| new_bind.clone());
424
425        drop(self.current_tex.take());
426        self.current_tex = Some(new_tex);
427        self.current_bind = Some(new_bind.clone());
428        self.current_width = img_width;
429        self.current_height = img_height;
430
431        let generation = self.playback_gen.fetch_add(1, Ordering::SeqCst) + 1;
432        self.pacer.notify();
433
434        Ok(CommitData {
435            bg_bind,
436            new_bind,
437            img_width,
438            img_height,
439            old_img_width,
440            old_img_height,
441            format: self.format,
442            width: self.width,
443            height: self.height,
444            animated,
445            is_video: false,
446            generation,
447            scaling_mode,
448        })
449    }
450
451    /// Renders the committed transition on a detached blocking task. The IPC
452    /// path never waits on GPU presents, so a stalled compositor (monitor
453    /// off, suspend) cannot hang the daemon. Transitions are serialized by
454    /// the render lock: a later one simply waits until the earlier drains.
455    fn spawn_transition(
456        &self,
457        commit: CommitData,
458        effect: &crate::animation::Effect,
459        duration_ms: u32,
460    ) {
461        let renderer = self.renderer.clone();
462        let surface: &'static wgpu::Surface<'static> = self.surface;
463        let render_lock = self.render_lock.clone();
464        let playback_gen = self.playback_gen.clone();
465        let pacer = self.pacer.clone();
466        let video_playback = self.video_playback.clone();
467        let effect = effect.clone();
468        drop(tokio::task::spawn_blocking(move || {
469            render_transition(
470                renderer,
471                surface,
472                render_lock,
473                playback_gen,
474                pacer,
475                video_playback,
476                commit,
477                effect,
478                duration_ms,
479            );
480        }));
481    }
482}
483
484/// Presents one frame per vsync until the wall-clock duration elapses. With
485/// PresentMode::Fifo, `get_current_texture` blocks until the previous frame
486/// is presented, so this loop is paced to the monitor refresh rate, and the
487/// transition lasts exactly `duration_ms` on any refresh rate — frame-count
488/// pacing would run too fast on high-refresh panels and too slow when the
489/// present rate is low. If the compositor stops presenting, the loop can park
490/// inside a present; that is fine here because the task is detached.
491#[allow(clippy::too_many_arguments)]
492fn render_transition(
493    renderer: std::sync::Arc<Renderer>,
494    surface: &'static wgpu::Surface<'static>,
495    render_lock: std::sync::Arc<std::sync::Mutex<()>>,
496    playback_gen: std::sync::Arc<std::sync::atomic::AtomicU64>,
497    pacer: std::sync::Arc<LivePacer>,
498    video_playback: std::sync::Arc<crate::video::VideoPlayback>,
499    mut commit: CommitData,
500    effect: crate::animation::Effect,
501    duration_ms: u32,
502) {
503    let _guard = render_lock
504        .lock()
505        .unwrap_or_else(|poisoned| poisoned.into_inner());
506
507    let duration = std::time::Duration::from_millis(u64::from(duration_ms.max(1)));
508    let start = std::time::Instant::now();
509    loop {
510        let progress = start.elapsed().as_secs_f32() / duration.as_secs_f32();
511        let uniforms = crate::animation::compute_effect_uniforms(&effect, progress.clamp(0.0, 1.0));
512        let status = renderer.render_frame(crate::renderer::FrameRequest {
513            surface,
514            format: commit.format,
515            bg_bind: &commit.bg_bind,
516            new_bind: &commit.new_bind,
517            effect: &uniforms,
518            width: commit.width,
519            height: commit.height,
520            img_width: commit.img_width,
521            img_height: commit.img_height,
522            old_img_width: commit.old_img_width,
523            old_img_height: commit.old_img_height,
524            scaling_mode: commit.scaling_mode,
525        });
526        let status = match status {
527            Ok(status) => status,
528            Err(err) => {
529                eprintln!("wallr: transition render failed: {err}");
530                break;
531            }
532        };
533        if progress >= 1.0 || status == crate::renderer::FrameStatus::TimedOut {
534            break;
535        }
536    }
537
538    // The transition ended; if the committed wallpaper is an animated GIF and
539    // nothing superseded it while we rendered, keep the render lock and play
540    // the frames live until the next commit bumps the generation.
541    let mut animated = commit.animated.take();
542    if let Some(animated) = animated.as_mut()
543        && playback_gen.load(Ordering::SeqCst) == commit.generation
544    {
545        play_live(&renderer, surface, &commit, animated, &playback_gen, &pacer);
546    } else if commit.is_video && playback_gen.load(Ordering::SeqCst) == commit.generation {
547        play_video(&renderer, surface, &commit, &video_playback, &playback_gen);
548    }
549}
550
551/// Presents live wallpaper frames until the next commit. One frame is
552/// presented per GIF frame boundary instead of at the monitor refresh rate.
553/// Two textures are double-buffered and frames are decompressed directly
554/// into a mapped staging ring (no intermediate copy), so the wake path only
555/// presents and the pacing sleep hides the decode/upload entirely.
556fn play_live(
557    renderer: &Renderer,
558    surface: &'static wgpu::Surface<'static>,
559    commit: &CommitData,
560    animated: &mut crate::animated::AnimatedImage,
561    playback_gen: &std::sync::atomic::AtomicU64,
562    pacer: &LivePacer,
563) {
564    let (tex_a, bind_a) = renderer.create_texture(animated.width, animated.height);
565    let (tex_b, bind_b) = renderer.create_texture(animated.width, animated.height);
566    let (frame_w, frame_h) = (animated.width, animated.height);
567    let (bytes_per_row, rows) = (frame_w * 4, frame_h);
568    let frame_bytes = bytes_per_row as u64 * rows as u64;
569
570    // Map+decompress+copy path needs a byte-per-row multiple of the copy
571    // alignment; fall back to write_texture for odd widths.
572    let direct_upload = bytes_per_row % 256 == 0;
573    let staging: Vec<wgpu::Buffer> = if direct_upload {
574        (0..2)
575            .map(|_| {
576                renderer.device.create_buffer(&wgpu::BufferDescriptor {
577                    label: Some("wallr-gif-staging"),
578                    size: frame_bytes,
579                    usage: wgpu::BufferUsages::MAP_WRITE | wgpu::BufferUsages::COPY_SRC,
580                    mapped_at_creation: false,
581                })
582            })
583            .collect()
584    } else {
585        Vec::new()
586    };
587
588    let first = animated.first_frame();
589    if !first.is_empty() {
590        renderer.update_texture(&tex_a, first, frame_w, frame_h);
591        renderer.update_texture(&tex_b, first, frame_w, frame_h);
592    }
593    let binds = [bind_a, bind_b];
594    let textures = [tex_a, tex_b];
595
596    // Uploads frame `index` into `textures[tgt]`. Returns true when the GPU
597    // copy was recorded.
598    let upload = |renderer: &Renderer,
599                  tgt: usize,
600                  index: usize,
601                  slot: usize,
602                  animated: &mut crate::animated::AnimatedImage|
603     -> bool {
604        if direct_upload {
605            let buffer = &staging[slot];
606            let slice = buffer.slice(..);
607            slice.map_async(wgpu::MapMode::Write, |_| {});
608            renderer.device.poll(wgpu::Maintain::Wait);
609            let ok = {
610                let mut mapped = slice.get_mapped_range_mut();
611                animated.decompress_into(index, &mut mapped)
612            };
613            buffer.unmap();
614            if ok {
615                let mut encoder = renderer
616                    .device
617                    .create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
618                encoder.copy_buffer_to_texture(
619                    wgpu::TexelCopyBufferInfo {
620                        buffer,
621                        layout: wgpu::TexelCopyBufferLayout {
622                            offset: 0,
623                            bytes_per_row: Some(bytes_per_row),
624                            rows_per_image: Some(rows),
625                        },
626                    },
627                    wgpu::TexelCopyTextureInfo {
628                        texture: &textures[tgt],
629                        mip_level: 0,
630                        origin: wgpu::Origin3d::ZERO,
631                        aspect: wgpu::TextureAspect::All,
632                    },
633                    wgpu::Extent3d {
634                        width: frame_w,
635                        height: frame_h,
636                        depth_or_array_layers: 1,
637                    },
638                );
639                renderer.queue.submit([encoder.finish()]);
640                return true;
641            }
642        } else {
643            let frame = animated.frame_at(index);
644            if !frame.is_empty() {
645                renderer.update_texture(&textures[tgt], frame, frame_w, frame_h);
646                return true;
647            }
648        }
649        false
650    };
651
652    let mut cur = 0usize; // texture index currently holding the presented frame
653    let mut cur_frame = 0usize; // frame index currently in texture `cur`
654    let mut next_frame = 0usize; // frame index currently in the idle texture
655    let mut slot = 0usize; // staging ring slot for the next upload
656    let start = std::time::Instant::now();
657    let static_effect = crate::animation::Effect::Fade(crate::animation::FadeParams::default());
658    loop {
659        if playback_gen.load(Ordering::SeqCst) != commit.generation {
660            return;
661        }
662        let index = animated.frame_index_at(start.elapsed());
663        if index != cur_frame {
664            if next_frame != index {
665                upload(renderer, cur ^ 1, index, slot, animated);
666                slot ^= 1;
667                next_frame = index;
668            }
669            cur ^= 1;
670            cur_frame = index;
671        }
672        let uniforms = crate::animation::compute_effect_uniforms(&static_effect, 1.0);
673        let status = renderer.render_frame(crate::renderer::FrameRequest {
674            surface,
675            format: commit.format,
676            bg_bind: &binds[cur],
677            new_bind: &binds[cur],
678            effect: &uniforms,
679            width: commit.width,
680            height: commit.height,
681            img_width: animated.width,
682            img_height: animated.height,
683            old_img_width: animated.width,
684            old_img_height: animated.height,
685            scaling_mode: commit.scaling_mode,
686        });
687        match status {
688            Ok(crate::renderer::FrameStatus::Presented) => {}
689            // A stalled present parks inside the acquire; a Timeout or error
690            // means the surface is unusable, so give up and let the next
691            // transition take over.
692            _ => return,
693        }
694
695        // Pace to the next GIF frame boundary instead of presenting at the
696        // monitor refresh rate: an animated wallpaper only needs a present
697        // when its frame changes. A commit wakes us via the pacer. The
698        // boundary is computed in absolute time (frame_start is loop-relative,
699        // so add the completed loops) to stay correct after the animation
700        // wraps. While waiting, warm the idle texture with the next frame so
701        // the wake path stays on the hot critical section.
702        let elapsed = start.elapsed();
703        let total: std::time::Duration = animated.total_duration();
704        let loops = (elapsed.as_millis() / total.as_millis().max(1)) as u64;
705        let next_change = animated.frame_start(index + 1) + total * (loops as u32);
706        let wait = next_change.saturating_sub(elapsed);
707        if wait > std::time::Duration::ZERO {
708            let next = index + 1;
709            if next_frame != next {
710                upload(renderer, cur ^ 1, next, slot, animated);
711                slot ^= 1;
712                next_frame = next;
713            }
714            pacer.wait_until(std::time::Instant::now() + wait);
715        }
716    }
717}
718
719/// Live video playback loop: continuously updates texture with decoded frames.
720fn play_video(
721    renderer: &Renderer,
722    surface: &'static wgpu::Surface<'static>,
723    commit: &CommitData,
724    video_playback: &std::sync::Arc<crate::video::VideoPlayback>,
725    playback_gen: &std::sync::atomic::AtomicU64,
726) {
727    // Get initial frame dimensions
728    let (width, height) = match video_playback.metadata() {
729        Some(meta) => (meta.width, meta.height),
730        None => {
731            tracing::warn!("No video metadata available");
732            return;
733        }
734    };
735
736    let (texture, bind) = renderer.create_texture(width, height);
737    let static_effect = crate::animation::Effect::Fade(crate::animation::FadeParams::default());
738
739    // The texture starts empty; present a real frame before the first
740    // vsync so the surface never flashes black.
741    let mut uploaded = false;
742
743    loop {
744        // A newer commit superseded us. Do NOT touch the shared
745        // `video_playback` here: the successor commit already replaced the
746        // decoder (video) or stopped it (static image), and stopping it now
747        // would kill the successor's playback too.
748        if playback_gen.load(Ordering::SeqCst) != commit.generation {
749            return;
750        }
751
752        // Pull the next displayable frame. The decoder queue is bounded, so
753        // this never blocks; `None` means "present the current texture".
754        if let Some(frame) = video_playback.next_frame() {
755            // The shared decoder can be replaced between commits; never
756            // upload a frame whose size does not match this task's texture.
757            if frame.width != width || frame.height != height {
758                continue;
759            }
760            renderer.update_texture(&texture, &frame.data, frame.width, frame.height);
761            uploaded = true;
762        }
763
764        if !uploaded {
765            // No frame yet; wait briefly and try again instead of presenting
766            // an uninitialized texture.
767            std::thread::sleep(std::time::Duration::from_millis(2));
768            continue;
769        }
770
771        let uniforms = crate::animation::compute_effect_uniforms(&static_effect, 1.0);
772        let status = renderer.render_frame(crate::renderer::FrameRequest {
773            surface,
774            format: commit.format,
775            bg_bind: &bind,
776            new_bind: &bind,
777            effect: &uniforms,
778            width: commit.width,
779            height: commit.height,
780            img_width: width,
781            img_height: height,
782            old_img_width: width,
783            old_img_height: height,
784            scaling_mode: commit.scaling_mode,
785        });
786
787        match status {
788            Ok(crate::renderer::FrameStatus::Presented) => {}
789            // A stalled present parks inside the acquire; a Timeout or error
790            // means the surface is unusable, so give up.
791            other => {
792                tracing::warn!("Video present failed ({:?}), stopping playback", other);
793                video_playback.stop();
794                return;
795            }
796        }
797    }
798}
799
800pub struct Daemon {
801    config: WallrConfig,
802    paused: Arc<AtomicBool>,
803    engine: Arc<Mutex<WallpaperEngine>>,
804}
805
806impl Daemon {
807    pub fn new(config: WallrConfig) -> Result<Self, DaemonError> {
808        let engine = WallpaperEngine::new(config.clone())?;
809        Ok(Self {
810            config,
811            paused: Arc::new(AtomicBool::new(false)),
812            engine: Arc::new(Mutex::new(engine)),
813        })
814    }
815
816    pub async fn start(self) -> Result<(), DaemonError> {
817        let socket_path = crate::config::expand_path(&self.config.daemon.socket);
818        if socket_path.exists() {
819            if tokio::net::UnixStream::connect(&socket_path).await.is_ok() {
820                return Err(DaemonError::AlreadyRunning(
821                    socket_path.to_string_lossy().to_string(),
822                ));
823            }
824            let _ = std::fs::remove_file(&socket_path);
825        }
826
827        let renderer = Renderer::new()
828            .await
829            .map_err(|e| DaemonError::StartError(format!("GPU init failed: {e}")))?;
830
831        let conn = Connection::connect_to_env()
832            .map_err(|e| DaemonError::StartError(format!("Failed to connect to Wayland: {e:?}")))?;
833        let backend = conn.backend();
834        let display_ptr = backend.display_ptr() as *mut std::ffi::c_void;
835
836        let (globals, mut event_queue) = registry_queue_init(&conn)
837            .map_err(|e| DaemonError::StartError(format!("registry_queue_init failed: {e:?}")))?;
838        let qh = event_queue.handle();
839
840        let compositor_state = CompositorState::bind(&globals, &qh)
841            .map_err(|e| DaemonError::StartError(format!("compositor bind failed: {e:?}")))?;
842        let layer_shell = LayerShell::bind(&globals, &qh)
843            .map_err(|e| DaemonError::StartError(format!("layer_shell bind failed: {e:?}")))?;
844        let shm = Shm::bind(&globals, &qh)
845            .map_err(|e| DaemonError::StartError(format!("shm bind failed: {e:?}")))?;
846
847        let mut wayland_state = WaylandState {
848            registry_state: RegistryState::new(&globals),
849            output_state: OutputState::new(&globals, &qh),
850            compositor_state,
851            shm,
852            surfaces: Vec::new(),
853            width: 1920,
854            height: 1080,
855            scale_factor: 1,
856        };
857
858        let wl_surface = wayland_state.compositor_state.create_surface(&qh);
859        let layer_surface = layer_shell.create_layer_surface(
860            &qh,
861            wl_surface,
862            Layer::Background,
863            Some("wallr"),
864            None,
865        );
866        layer_surface.set_anchor(Anchor::TOP | Anchor::BOTTOM | Anchor::LEFT | Anchor::RIGHT);
867        layer_surface.set_exclusive_zone(-1); // Don't reserve space
868        layer_surface.set_keyboard_interactivity(KeyboardInteractivity::None); // No keyboard
869
870        // CRITICAL: Empty input region so ALL clicks pass through to desktop
871        // Without this, the wallpaper blocks desktop interaction on KDE/Plasma
872        let compositor = globals
873            .bind::<wl_compositor::WlCompositor, WaylandState, smithay_client_toolkit::globals::GlobalData>(
874                &qh,
875                1..=4,
876                smithay_client_toolkit::globals::GlobalData,
877            )
878            .map_err(|e| DaemonError::StartError(format!("compositor bind failed: {e:?}")))?;
879        let empty_region = compositor.create_region(&qh, ());
880        layer_surface
881            .wl_surface()
882            .set_input_region(Some(&empty_region));
883        layer_surface.commit();
884        empty_region.destroy();
885
886        event_queue
887            .roundtrip(&mut wayland_state)
888            .map_err(|e| DaemonError::StartError(format!("roundtrip failed: {e:?}")))?;
889        event_queue
890            .roundtrip(&mut wayland_state)
891            .map_err(|e| DaemonError::StartError(format!("roundtrip2 failed: {e:?}")))?;
892
893        let scale_factor = if wayland_state.scale_factor > 0 {
894            wayland_state.scale_factor
895        } else {
896            1
897        };
898        layer_surface.wl_surface().set_buffer_scale(scale_factor);
899
900        let width = wayland_state.width * scale_factor as u32;
901        let height = wayland_state.height * scale_factor as u32;
902
903        let raw_surface = layer_surface.wl_surface().id().as_ptr() as *mut std::ffi::c_void;
904        wayland_state.surfaces.push(layer_surface);
905
906        let window_handle = WaylandWindow {
907            display: display_ptr,
908            surface: raw_surface,
909        };
910
911        let wgpu_surface = renderer
912            .instance
913            .create_surface(&window_handle)
914            .map_err(|e| DaemonError::StartError(format!("wgpu surface creation failed: {e:?}")))?;
915
916        let adapter = renderer
917            .instance
918            .request_adapter(&wgpu::RequestAdapterOptions {
919                compatible_surface: Some(&wgpu_surface),
920                power_preference: wgpu::PowerPreference::HighPerformance,
921                force_fallback_adapter: false,
922            })
923            .await;
924        let surf_format = adapter
925            .as_ref()
926            .map(|a| {
927                let caps = wgpu_surface.get_capabilities(a);
928                caps.formats
929                    .into_iter()
930                    .next()
931                    .unwrap_or(wgpu::TextureFormat::Bgra8UnormSrgb)
932            })
933            .unwrap_or(wgpu::TextureFormat::Bgra8UnormSrgb);
934
935        let surf_config = wgpu::SurfaceConfiguration {
936            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
937            format: surf_format,
938            width,
939            height,
940            present_mode: wgpu::PresentMode::Fifo,
941            alpha_mode: wgpu::CompositeAlphaMode::Opaque,
942            view_formats: vec![],
943            desired_maximum_frame_latency: 2,
944        };
945        wgpu_surface.configure(&renderer.device, &surf_config);
946
947        // SAFETY: We transmute the surface lifetime to 'static so it can be moved
948        // into the shared Arc. The surface is tied to window_handle / wayland_state
949        // both of which live as long as the process.
950        let wgpu_surface: wgpu::Surface<'static> = unsafe { std::mem::transmute(wgpu_surface) };
951        // The daemon lives for the whole process, so leaking one surface is fine
952        // and gives every detached transition task a stable reference to present
953        // to without holding the RenderState lock during the blocking acquire.
954        let surface: &'static wgpu::Surface<'static> = Box::leak(Box::new(wgpu_surface));
955
956        let render_state = Arc::new(Mutex::new(RenderState {
957            renderer: std::sync::Arc::new(renderer),
958            surface,
959            render_lock: std::sync::Arc::new(std::sync::Mutex::new(())),
960            playback_gen: std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0)),
961            pacer: std::sync::Arc::new(LivePacer::new()),
962            current_bind: None,
963            current_tex: None,
964            width,
965            height,
966            current_width: 0,
967            current_height: 0,
968            format: surf_format,
969            video_playback: std::sync::Arc::new(crate::video::VideoPlayback::new()),
970            hw_accel: crate::video::HwAccel::from_config(&self.config.video.hw_decode),
971            scaling_mode: 0,
972        }));
973
974        {
975            let state_path = dirs::cache_dir()
976                .unwrap_or_else(|| std::path::PathBuf::from("/tmp"))
977                .join("wallr/last_wallpaper");
978            if let Ok(path_str) = std::fs::read_to_string(&state_path) {
979                let p = std::path::Path::new(path_str.trim());
980                if p.exists() {
981                    let mut rs = render_state.lock().await;
982                    let effect =
983                        crate::animation::Effect::Fade(crate::animation::FadeParams::default());
984                    let _ = rs.set_wallpaper(p, &effect, 0, 0).await;
985                }
986            }
987        }
988
989        let paused_clone = self.paused.clone();
990        let engine_clone = self.engine.clone();
991        let render_state_clone = render_state.clone();
992
993        // Graceful shutdown on POSIX signals: stop video decoding, remove the
994        // IPC socket, and exit. The compositor releases the layer-shell
995        // surface automatically when the process exits.
996        {
997            let rs = render_state.clone();
998            let socket_path = socket_path.clone();
999            tokio::spawn(async move {
1000                use tokio::signal::unix::{SignalKind, signal};
1001                let mut term = signal(SignalKind::terminate()).expect("SIGTERM handler");
1002                let mut int = signal(SignalKind::interrupt()).expect("SIGINT handler");
1003                let mut hup = signal(SignalKind::hangup()).expect("SIGHUP handler");
1004                tokio::select! {
1005                    _ = term.recv() => {}
1006                    _ = int.recv() => {}
1007                    _ = hup.recv() => {}
1008                }
1009                tracing::info!("Signal received, shutting down gracefully");
1010                if let Ok(state) = rs.try_lock() {
1011                    state.video_playback.stop();
1012                }
1013                let _ = std::fs::remove_file(&socket_path);
1014                std::process::exit(0);
1015            });
1016        }
1017
1018        let ipc_socket_path = socket_path.clone();
1019        start_ipc_server(&socket_path, move |cmd| {
1020            let paused = paused_clone.clone();
1021            let engine = engine_clone.clone();
1022            let rs = render_state_clone.clone();
1023            let stop_socket = ipc_socket_path.clone();
1024            async move {
1025                match cmd {
1026                    IpcCommand::Pause => {
1027                        paused.store(true, Ordering::SeqCst);
1028                        let rs_lock = rs.lock().await;
1029                        rs_lock.video_playback.pause();
1030                        IpcResponse {
1031                            success: true,
1032                            message: Some("Paused".into()),
1033                        }
1034                    }
1035                    IpcCommand::Resume => {
1036                        paused.store(false, Ordering::SeqCst);
1037                        let rs_lock = rs.lock().await;
1038                        rs_lock.video_playback.resume();
1039                        IpcResponse {
1040                            success: true,
1041                            message: Some("Resumed".into()),
1042                        }
1043                    }
1044                    IpcCommand::Reload => {
1045                        let lock = engine.lock().await;
1046                        match lock.reload() {
1047                            Ok(_) => IpcResponse {
1048                                success: true,
1049                                message: Some("Reloaded".into()),
1050                            },
1051                            Err(e) => IpcResponse {
1052                                success: false,
1053                                message: Some(e.to_string()),
1054                            },
1055                        }
1056                    }
1057                    IpcCommand::Preview {
1058                        path,
1059                        effect,
1060                        duration_ms,
1061                        no_theme,
1062                        theme_override,
1063                        monitor,
1064                        scaling_mode,
1065                    } => {
1066                        if paused.load(Ordering::SeqCst) {
1067                            return IpcResponse {
1068                                success: false,
1069                                message: Some("Daemon is paused".into()),
1070                            };
1071                        }
1072                        let p = std::path::PathBuf::from(&path);
1073                        if !p.exists() {
1074                            return IpcResponse {
1075                                success: false,
1076                                message: Some(format!("File not found: {}", path)),
1077                            };
1078                        }
1079
1080                        let state_path = dirs::cache_dir()
1081                            .unwrap_or_else(|| std::path::PathBuf::from("/tmp"))
1082                            .join("wallr/last_wallpaper");
1083                        if let Some(parent) = state_path.parent() {
1084                            let _ = std::fs::create_dir_all(parent);
1085                        }
1086                        let _ = std::fs::write(&state_path, &path);
1087
1088                        let effect = effect.unwrap_or_else(|| {
1089                            crate::animation::Effect::Fade(crate::animation::FadeParams::default())
1090                        });
1091                        // Live playback only starts after the transition, so for
1092                        // videos an unrequested 2s fade reads as a long "load".
1093                        // Default to a short fade unless the user asked for one.
1094                        let is_video = crate::video::VideoDecoder::is_video_file(&p);
1095                        let duration = duration_ms.unwrap_or(if is_video { 150 } else { 2000 });
1096                        let sm = scaling_mode.unwrap_or(crate::config::ScalingMode::Fill);
1097                        let scaling_mode_u32 = match sm {
1098                            crate::config::ScalingMode::Fill => 0u32,
1099                            crate::config::ScalingMode::Fit => 1,
1100                            crate::config::ScalingMode::Stretch => 2,
1101                            crate::config::ScalingMode::Center => 3,
1102                            crate::config::ScalingMode::Tile => 4,
1103                        };
1104
1105                        let rs_clone = rs.clone();
1106                        let p_clone = p.clone();
1107                        let result = tokio::task::spawn_blocking(move || {
1108                            let rt = tokio::runtime::Handle::current();
1109                            rt.block_on(async {
1110                                let mut lock = rs_clone.lock().await;
1111                                lock.set_wallpaper(&p_clone, &effect, duration, scaling_mode_u32)
1112                                    .await
1113                            })
1114                        })
1115                        .await;
1116
1117                        match result {
1118                            Ok(Ok(())) => {
1119                                let opts = SetOptions {
1120                                    no_theme,
1121                                    theme_provider: theme_override,
1122                                    monitor,
1123                                };
1124                                let mut eng = engine.lock().await;
1125                                match eng.set_wallpaper(&p, &opts).await {
1126                                    Ok(()) => IpcResponse {
1127                                        success: true,
1128                                        message: None,
1129                                    },
1130                                    Err(e) => IpcResponse {
1131                                        success: true,
1132                                        message: Some(format!(
1133                                            "Wallpaper set, but hooks/theme failed: {e}"
1134                                        )),
1135                                    },
1136                                }
1137                            }
1138                            Ok(Err(e)) => IpcResponse {
1139                                success: false,
1140                                message: Some(format!("Render failed: {}", e)),
1141                            },
1142                            Err(e) => IpcResponse {
1143                                success: false,
1144                                message: Some(format!("Task spawn failed: {}", e)),
1145                            },
1146                        }
1147                    }
1148                    IpcCommand::Stop => {
1149                        let sp = stop_socket.clone();
1150                        tokio::spawn(async move {
1151                            tokio::time::sleep(std::time::Duration::from_millis(300)).await;
1152                            let _ = std::fs::remove_file(&sp);
1153                            std::process::exit(0);
1154                        });
1155                        IpcResponse {
1156                            success: true,
1157                            message: Some("Stopping".into()),
1158                        }
1159                    }
1160                    IpcCommand::Status => {
1161                        let state = if paused.load(Ordering::SeqCst) {
1162                            "paused"
1163                        } else {
1164                            "running"
1165                        };
1166                        IpcResponse {
1167                            success: true,
1168                            message: Some(format!("wallr daemon {}", state)),
1169                        }
1170                    }
1171                    IpcCommand::Seek { timestamp_ms } => {
1172                        let rs_lock = rs.lock().await;
1173                        match rs_lock
1174                            .video_playback
1175                            .seek(std::time::Duration::from_millis(timestamp_ms))
1176                        {
1177                            Ok(()) => IpcResponse {
1178                                success: true,
1179                                message: Some(format!("Seeked to {}ms", timestamp_ms)),
1180                            },
1181                            Err(e) => IpcResponse {
1182                                success: false,
1183                                message: Some(format!("Seek failed: {}", e)),
1184                            },
1185                        }
1186                    }
1187                    IpcCommand::Info => {
1188                        let rs_lock = rs.lock().await;
1189
1190                        // Get GPU info from renderer
1191                        let gpu_info =
1192                            crate::video::gpu::adapter_diagnostics(&rs_lock.renderer.adapter);
1193
1194                        let mut lines = vec![
1195                            format!("wallr v{}", env!("CARGO_PKG_VERSION")),
1196                            String::new(),
1197                            gpu_info,
1198                        ];
1199
1200                        match rs_lock.video_playback.metadata() {
1201                            Some(meta) => {
1202                                let decoder_info = rs_lock.video_playback.decoder_info();
1203                                let hw = rs_lock.video_playback.hw_accel_in_use();
1204                                let state = if rs_lock.video_playback.is_paused() {
1205                                    "paused"
1206                                } else {
1207                                    "playing"
1208                                };
1209                                let position = rs_lock
1210                                    .video_playback
1211                                    .position()
1212                                    .map(|p| format!("{:.2}s", p.as_secs_f64()))
1213                                    .unwrap_or_else(|| "?".to_string());
1214                                lines.push(String::new());
1215                                lines.push("Video:".into());
1216                                lines.push(format!("  Resolution: {}x{}", meta.width, meta.height));
1217                                lines.push(format!("  FPS: {:.2}", meta.fps));
1218                                lines.push(format!(
1219                                    "  Duration: {:.2}s",
1220                                    meta.duration.as_secs_f64()
1221                                ));
1222                                lines.push(format!(
1223                                    "  Codec: {}",
1224                                    decoder_info
1225                                        .as_ref()
1226                                        .map(|d| d.codec_name.as_str())
1227                                        .unwrap_or("unknown")
1228                                ));
1229                                lines.push(format!("  Container: {}", meta.format));
1230                                lines.push(format!("  Decoder: {}", hw.name()));
1231                                lines.push(format!(
1232                                    "  GPU Decode: {}",
1233                                    if hw == crate::video::HwAccel::Software {
1234                                        "disabled"
1235                                    } else {
1236                                        "enabled"
1237                                    }
1238                                ));
1239                                lines.push(format!("  State: {} @ {}", state, position));
1240                            }
1241                            None => {
1242                                lines.push(String::new());
1243                                lines.push("Video: none active".into());
1244                                lines.push("Decoder: idle".into());
1245                            }
1246                        }
1247
1248                        IpcResponse {
1249                            success: true,
1250                            message: Some(lines.join("\n")),
1251                        }
1252                    }
1253                }
1254            }
1255        })
1256        .await?;
1257
1258        // Start file watcher if configured
1259        if self.config.watch.enabled
1260            && let Some(ref watch_dir) = self.config.watch.dir
1261        {
1262            let watch_path = crate::config::expand_path(watch_dir);
1263            self.start_watcher(watch_path, render_state.clone()).await?;
1264        }
1265
1266        tokio::task::spawn_blocking(move || {
1267            loop {
1268                if let Err(e) = event_queue.blocking_dispatch(&mut wayland_state) {
1269                    eprintln!("Wayland dispatch error: {e:?}");
1270                    break;
1271                }
1272            }
1273            // The compositor connection is dead (e.g. the compositor exited
1274            // or killed our layer surface with a protocol error). Rendering
1275            // can never recover, so exit and let the supervisor restart us.
1276            eprintln!("wallr: Wayland connection lost, exiting");
1277            std::process::exit(1);
1278        });
1279
1280        loop {
1281            tokio::time::sleep(std::time::Duration::from_secs(3600)).await;
1282        }
1283    }
1284
1285    async fn start_watcher(
1286        &self,
1287        dir: PathBuf,
1288        render_state: Arc<Mutex<RenderState>>,
1289    ) -> Result<(), DaemonError> {
1290        let engine = self.engine.clone();
1291        let paused = self.paused.clone();
1292        let debounce = crate::config::parse_duration(&self.config.watch.debounce)
1293            .unwrap_or(std::time::Duration::from_millis(500));
1294
1295        let (tx, mut rx) = tokio::sync::mpsc::channel(100);
1296
1297        let mut watcher = notify::recommended_watcher(move |res: Result<Event, notify::Error>| {
1298            if let Ok(event) = res
1299                && let EventKind::Create(_) = event.kind
1300            {
1301                for path in event.paths {
1302                    let _ = tx.blocking_send(path);
1303                }
1304            }
1305        })
1306        .map_err(|e| DaemonError::StartError(e.to_string()))?;
1307
1308        watcher
1309            .watch(&dir, RecursiveMode::NonRecursive)
1310            .map_err(|e| DaemonError::StartError(e.to_string()))?;
1311
1312        tokio::spawn(async move {
1313            let _watcher = watcher;
1314            let mut last: Option<(PathBuf, std::time::Instant)> = None;
1315
1316            while let Some(path) = rx.recv().await {
1317                if paused.load(Ordering::SeqCst) {
1318                    continue;
1319                }
1320                if let Some((ref lp, ref lt)) = last
1321                    && lp == &path
1322                    && lt.elapsed() < debounce
1323                {
1324                    continue;
1325                }
1326                let ext = path
1327                    .extension()
1328                    .unwrap_or_default()
1329                    .to_string_lossy()
1330                    .to_lowercase();
1331                if !["jpg", "jpeg", "png", "gif", "webp"].contains(&ext.as_str()) {
1332                    continue;
1333                }
1334                last = Some((path.clone(), std::time::Instant::now()));
1335
1336                let rs = render_state.clone();
1337                let eng = engine.clone();
1338                let p = path.clone();
1339                tokio::spawn(async move {
1340                    let mut lock = rs.lock().await;
1341                    let effect =
1342                        crate::animation::Effect::Fade(crate::animation::FadeParams::default());
1343                    let _ = lock.set_wallpaper(&p, &effect, 600, 0).await;
1344                    drop(lock);
1345                    let opts = SetOptions {
1346                        no_theme: false,
1347                        theme_provider: None,
1348                        monitor: None,
1349                    };
1350                    let mut elock = eng.lock().await;
1351                    let _ = elock.set_wallpaper(&p, &opts).await;
1352                });
1353            }
1354        });
1355
1356        Ok(())
1357    }
1358}