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