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 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
212struct 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 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 render_lock: std::sync::Arc<std::sync::Mutex<()>>,
252 playback_gen: std::sync::Arc<std::sync::atomic::AtomicU64>,
255 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: std::sync::Arc<crate::video::VideoPlayback>,
267 hw_accel: crate::video::HwAccel,
269}
270
271struct CommitData {
274 bg_bind: wgpu::BindGroup,
275 new_bind: wgpu::BindGroup,
276 img_width: u32,
277 img_height: u32,
278 old_img_width: u32,
279 old_img_height: u32,
280 format: wgpu::TextureFormat,
281 width: u32,
282 height: u32,
283 animated: Option<crate::animated::AnimatedImage>,
286 is_video: bool,
288 generation: u64,
291}
292
293impl RenderState {
294 async fn set_wallpaper(
295 &mut self,
296 path: &std::path::Path,
297 effect: &crate::animation::Effect,
298 duration_ms: u32,
299 ) -> anyhow::Result<()> {
300 let commit = self.commit_wallpaper(path)?;
301 self.spawn_transition(commit, effect, duration_ms);
302 Ok(())
303 }
304
305 fn commit_wallpaper(&mut self, path: &std::path::Path) -> anyhow::Result<CommitData> {
309 use image::ImageReader;
310
311 if crate::video::VideoDecoder::is_video_file(path) {
313 tracing::info!("Video file detected: {:?}", path);
314
315 let generation = self.playback_gen.fetch_add(1, Ordering::SeqCst) + 1;
321 self.pacer.notify();
322
323 let metadata = self.video_playback.start(path, self.hw_accel)?;
326
327 let first_frame = self
330 .video_playback
331 .wait_first_frame(std::time::Duration::from_millis(1000));
332
333 let (new_tex, new_bind, img_width, img_height) = if let Some(frame) = first_frame {
334 let (tex, bind) = self.renderer.create_texture(frame.width, frame.height);
335 self.renderer
336 .update_texture(&tex, &frame.data, frame.width, frame.height);
337 (tex, bind, frame.width, frame.height)
338 } else {
339 tracing::warn!("No first frame available, using black texture");
341 let (tex, bind) = self
342 .renderer
343 .create_texture(metadata.width, metadata.height);
344 let black = vec![0u8; (metadata.width * metadata.height * 4) as usize];
345 self.renderer
346 .update_texture(&tex, &black, metadata.width, metadata.height);
347 (tex, bind, metadata.width, metadata.height)
348 };
349
350 let old_bind = self.current_bind.take();
351 let (old_img_width, old_img_height) = if old_bind.is_some() {
352 (self.current_width.max(1), self.current_height.max(1))
353 } else {
354 (img_width, img_height)
355 };
356 let bg_bind = old_bind.unwrap_or_else(|| new_bind.clone());
357
358 drop(self.current_tex.take());
359 self.current_tex = Some(new_tex);
360 self.current_bind = Some(new_bind.clone());
361 self.current_width = img_width;
362 self.current_height = img_height;
363
364 return Ok(CommitData {
365 bg_bind,
366 new_bind,
367 img_width,
368 img_height,
369 old_img_width,
370 old_img_height,
371 format: self.format,
372 width: self.width,
373 height: self.height,
374 animated: None,
375 is_video: true,
376 generation,
377 });
378 }
379
380 self.video_playback.stop();
384
385 let mut animated = crate::animated::AnimatedImage::decode(path)?;
388 let (new_tex, new_bind, img_width, img_height) = if let Some(anim) = animated.as_mut() {
389 let (w, h) = (anim.width, anim.height);
390 let (tex, bind) = self.renderer.create_texture(w, h);
391 let first = anim.first_frame();
392 if !first.is_empty() {
393 self.renderer.update_texture(&tex, first, w, h);
394 }
395 (tex, bind, w, h)
396 } else {
397 let new_img = ImageReader::open(path)?.decode()?;
398 let (tex, bind) = self.renderer.load_texture(&new_img)?;
399 (tex, bind, new_img.width(), new_img.height())
400 };
401
402 let old_bind = self.current_bind.take();
403 let (old_img_width, old_img_height) = if old_bind.is_some() {
404 (self.current_width.max(1), self.current_height.max(1))
405 } else {
406 (img_width, img_height)
407 };
408 let bg_bind = old_bind.unwrap_or_else(|| new_bind.clone());
413
414 drop(self.current_tex.take());
415 self.current_tex = Some(new_tex);
416 self.current_bind = Some(new_bind.clone());
417 self.current_width = img_width;
418 self.current_height = img_height;
419
420 let generation = self.playback_gen.fetch_add(1, Ordering::SeqCst) + 1;
421 self.pacer.notify();
422
423 Ok(CommitData {
424 bg_bind,
425 new_bind,
426 img_width,
427 img_height,
428 old_img_width,
429 old_img_height,
430 format: self.format,
431 width: self.width,
432 height: self.height,
433 animated,
434 is_video: false,
435 generation,
436 })
437 }
438
439 fn spawn_transition(
444 &self,
445 commit: CommitData,
446 effect: &crate::animation::Effect,
447 duration_ms: u32,
448 ) {
449 let renderer = self.renderer.clone();
450 let surface: &'static wgpu::Surface<'static> = self.surface;
451 let render_lock = self.render_lock.clone();
452 let playback_gen = self.playback_gen.clone();
453 let pacer = self.pacer.clone();
454 let video_playback = self.video_playback.clone();
455 let effect = effect.clone();
456 drop(tokio::task::spawn_blocking(move || {
457 render_transition(
458 renderer,
459 surface,
460 render_lock,
461 playback_gen,
462 pacer,
463 video_playback,
464 commit,
465 effect,
466 duration_ms,
467 );
468 }));
469 }
470}
471
472#[allow(clippy::too_many_arguments)]
480fn render_transition(
481 renderer: std::sync::Arc<Renderer>,
482 surface: &'static wgpu::Surface<'static>,
483 render_lock: std::sync::Arc<std::sync::Mutex<()>>,
484 playback_gen: std::sync::Arc<std::sync::atomic::AtomicU64>,
485 pacer: std::sync::Arc<LivePacer>,
486 video_playback: std::sync::Arc<crate::video::VideoPlayback>,
487 mut commit: CommitData,
488 effect: crate::animation::Effect,
489 duration_ms: u32,
490) {
491 let _guard = render_lock
492 .lock()
493 .unwrap_or_else(|poisoned| poisoned.into_inner());
494
495 let duration = std::time::Duration::from_millis(u64::from(duration_ms.max(1)));
496 let start = std::time::Instant::now();
497 loop {
498 let progress = start.elapsed().as_secs_f32() / duration.as_secs_f32();
499 let uniforms = crate::animation::compute_effect_uniforms(&effect, progress.clamp(0.0, 1.0));
500 let status = renderer.render_frame(crate::renderer::FrameRequest {
501 surface,
502 format: commit.format,
503 bg_bind: &commit.bg_bind,
504 new_bind: &commit.new_bind,
505 effect: &uniforms,
506 width: commit.width,
507 height: commit.height,
508 img_width: commit.img_width,
509 img_height: commit.img_height,
510 old_img_width: commit.old_img_width,
511 old_img_height: commit.old_img_height,
512 });
513 let status = match status {
514 Ok(status) => status,
515 Err(err) => {
516 eprintln!("wallr: transition render failed: {err}");
517 break;
518 }
519 };
520 if progress >= 1.0 || status == crate::renderer::FrameStatus::TimedOut {
521 break;
522 }
523 }
524
525 let mut animated = commit.animated.take();
529 if let Some(animated) = animated.as_mut()
530 && playback_gen.load(Ordering::SeqCst) == commit.generation
531 {
532 play_live(&renderer, surface, &commit, animated, &playback_gen, &pacer);
533 } else if commit.is_video && playback_gen.load(Ordering::SeqCst) == commit.generation {
534 play_video(&renderer, surface, &commit, &video_playback, &playback_gen);
535 }
536}
537
538fn play_live(
544 renderer: &Renderer,
545 surface: &'static wgpu::Surface<'static>,
546 commit: &CommitData,
547 animated: &mut crate::animated::AnimatedImage,
548 playback_gen: &std::sync::atomic::AtomicU64,
549 pacer: &LivePacer,
550) {
551 let (tex_a, bind_a) = renderer.create_texture(animated.width, animated.height);
552 let (tex_b, bind_b) = renderer.create_texture(animated.width, animated.height);
553 let (frame_w, frame_h) = (animated.width, animated.height);
554 let (bytes_per_row, rows) = (frame_w * 4, frame_h);
555 let frame_bytes = bytes_per_row as u64 * rows as u64;
556
557 let direct_upload = bytes_per_row % 256 == 0;
560 let staging: Vec<wgpu::Buffer> = if direct_upload {
561 (0..2)
562 .map(|_| {
563 renderer.device.create_buffer(&wgpu::BufferDescriptor {
564 label: Some("wallr-gif-staging"),
565 size: frame_bytes,
566 usage: wgpu::BufferUsages::MAP_WRITE | wgpu::BufferUsages::COPY_SRC,
567 mapped_at_creation: false,
568 })
569 })
570 .collect()
571 } else {
572 Vec::new()
573 };
574
575 let first = animated.first_frame();
576 if !first.is_empty() {
577 renderer.update_texture(&tex_a, first, frame_w, frame_h);
578 renderer.update_texture(&tex_b, first, frame_w, frame_h);
579 }
580 let binds = [bind_a, bind_b];
581 let textures = [tex_a, tex_b];
582
583 let upload = |renderer: &Renderer,
586 tgt: usize,
587 index: usize,
588 slot: usize,
589 animated: &mut crate::animated::AnimatedImage|
590 -> bool {
591 if direct_upload {
592 let buffer = &staging[slot];
593 let slice = buffer.slice(..);
594 slice.map_async(wgpu::MapMode::Write, |_| {});
595 renderer.device.poll(wgpu::Maintain::Wait);
596 let ok = {
597 let mut mapped = slice.get_mapped_range_mut();
598 animated.decompress_into(index, &mut mapped)
599 };
600 buffer.unmap();
601 if ok {
602 let mut encoder = renderer
603 .device
604 .create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
605 encoder.copy_buffer_to_texture(
606 wgpu::TexelCopyBufferInfo {
607 buffer,
608 layout: wgpu::TexelCopyBufferLayout {
609 offset: 0,
610 bytes_per_row: Some(bytes_per_row),
611 rows_per_image: Some(rows),
612 },
613 },
614 wgpu::TexelCopyTextureInfo {
615 texture: &textures[tgt],
616 mip_level: 0,
617 origin: wgpu::Origin3d::ZERO,
618 aspect: wgpu::TextureAspect::All,
619 },
620 wgpu::Extent3d {
621 width: frame_w,
622 height: frame_h,
623 depth_or_array_layers: 1,
624 },
625 );
626 renderer.queue.submit([encoder.finish()]);
627 return true;
628 }
629 } else {
630 let frame = animated.frame_at(index);
631 if !frame.is_empty() {
632 renderer.update_texture(&textures[tgt], frame, frame_w, frame_h);
633 return true;
634 }
635 }
636 false
637 };
638
639 let mut cur = 0usize; let mut cur_frame = 0usize; let mut next_frame = 0usize; let mut slot = 0usize; let start = std::time::Instant::now();
644 let static_effect = crate::animation::Effect::Fade(crate::animation::FadeParams::default());
645 loop {
646 if playback_gen.load(Ordering::SeqCst) != commit.generation {
647 return;
648 }
649 let index = animated.frame_index_at(start.elapsed());
650 if index != cur_frame {
651 if next_frame != index {
652 upload(renderer, cur ^ 1, index, slot, animated);
653 slot ^= 1;
654 next_frame = index;
655 }
656 cur ^= 1;
657 cur_frame = index;
658 }
659 let uniforms = crate::animation::compute_effect_uniforms(&static_effect, 1.0);
660 let status = renderer.render_frame(crate::renderer::FrameRequest {
661 surface,
662 format: commit.format,
663 bg_bind: &binds[cur],
664 new_bind: &binds[cur],
665 effect: &uniforms,
666 width: commit.width,
667 height: commit.height,
668 img_width: animated.width,
669 img_height: animated.height,
670 old_img_width: animated.width,
671 old_img_height: animated.height,
672 });
673 match status {
674 Ok(crate::renderer::FrameStatus::Presented) => {}
675 _ => return,
679 }
680
681 let elapsed = start.elapsed();
689 let total: std::time::Duration = animated.total_duration();
690 let loops = (elapsed.as_millis() / total.as_millis().max(1)) as u64;
691 let next_change = animated.frame_start(index + 1) + total * (loops as u32);
692 let wait = next_change.saturating_sub(elapsed);
693 if wait > std::time::Duration::ZERO {
694 let next = index + 1;
695 if next_frame != next {
696 upload(renderer, cur ^ 1, next, slot, animated);
697 slot ^= 1;
698 next_frame = next;
699 }
700 pacer.wait_until(std::time::Instant::now() + wait);
701 }
702 }
703}
704
705fn play_video(
707 renderer: &Renderer,
708 surface: &'static wgpu::Surface<'static>,
709 commit: &CommitData,
710 video_playback: &std::sync::Arc<crate::video::VideoPlayback>,
711 playback_gen: &std::sync::atomic::AtomicU64,
712) {
713 let (width, height) = match video_playback.metadata() {
715 Some(meta) => (meta.width, meta.height),
716 None => {
717 tracing::warn!("No video metadata available");
718 return;
719 }
720 };
721
722 let (texture, bind) = renderer.create_texture(width, height);
723 let static_effect = crate::animation::Effect::Fade(crate::animation::FadeParams::default());
724
725 let mut uploaded = false;
728
729 loop {
730 if playback_gen.load(Ordering::SeqCst) != commit.generation {
735 return;
736 }
737
738 if let Some(frame) = video_playback.next_frame() {
741 if frame.width != width || frame.height != height {
744 continue;
745 }
746 renderer.update_texture(&texture, &frame.data, frame.width, frame.height);
747 uploaded = true;
748 }
749
750 if !uploaded {
751 std::thread::sleep(std::time::Duration::from_millis(2));
754 continue;
755 }
756
757 let uniforms = crate::animation::compute_effect_uniforms(&static_effect, 1.0);
758 let status = renderer.render_frame(crate::renderer::FrameRequest {
759 surface,
760 format: commit.format,
761 bg_bind: &bind,
762 new_bind: &bind,
763 effect: &uniforms,
764 width: commit.width,
765 height: commit.height,
766 img_width: width,
767 img_height: height,
768 old_img_width: width,
769 old_img_height: height,
770 });
771
772 match status {
773 Ok(crate::renderer::FrameStatus::Presented) => {}
774 other => {
777 tracing::warn!("Video present failed ({:?}), stopping playback", other);
778 video_playback.stop();
779 return;
780 }
781 }
782 }
783}
784
785pub struct Daemon {
786 config: WallrConfig,
787 paused: Arc<AtomicBool>,
788 engine: Arc<Mutex<WallpaperEngine>>,
789}
790
791impl Daemon {
792 pub fn new(config: WallrConfig) -> Result<Self, DaemonError> {
793 let engine = WallpaperEngine::new(config.clone())?;
794 Ok(Self {
795 config,
796 paused: Arc::new(AtomicBool::new(false)),
797 engine: Arc::new(Mutex::new(engine)),
798 })
799 }
800
801 pub async fn start(self) -> Result<(), DaemonError> {
802 let socket_path = crate::config::expand_path(&self.config.daemon.socket);
803 if socket_path.exists() {
804 if tokio::net::UnixStream::connect(&socket_path).await.is_ok() {
805 return Err(DaemonError::AlreadyRunning(
806 socket_path.to_string_lossy().to_string(),
807 ));
808 }
809 let _ = std::fs::remove_file(&socket_path);
810 }
811
812 let renderer = Renderer::new()
813 .await
814 .map_err(|e| DaemonError::StartError(format!("GPU init failed: {e}")))?;
815
816 let conn = Connection::connect_to_env()
817 .map_err(|e| DaemonError::StartError(format!("Failed to connect to Wayland: {e:?}")))?;
818 let backend = conn.backend();
819 let display_ptr = backend.display_ptr() as *mut std::ffi::c_void;
820
821 let (globals, mut event_queue) = registry_queue_init(&conn)
822 .map_err(|e| DaemonError::StartError(format!("registry_queue_init failed: {e:?}")))?;
823 let qh = event_queue.handle();
824
825 let compositor_state = CompositorState::bind(&globals, &qh)
826 .map_err(|e| DaemonError::StartError(format!("compositor bind failed: {e:?}")))?;
827 let layer_shell = LayerShell::bind(&globals, &qh)
828 .map_err(|e| DaemonError::StartError(format!("layer_shell bind failed: {e:?}")))?;
829 let shm = Shm::bind(&globals, &qh)
830 .map_err(|e| DaemonError::StartError(format!("shm bind failed: {e:?}")))?;
831
832 let mut wayland_state = WaylandState {
833 registry_state: RegistryState::new(&globals),
834 output_state: OutputState::new(&globals, &qh),
835 compositor_state,
836 shm,
837 surfaces: Vec::new(),
838 width: 1920,
839 height: 1080,
840 scale_factor: 1,
841 };
842
843 let wl_surface = wayland_state.compositor_state.create_surface(&qh);
844 let layer_surface = layer_shell.create_layer_surface(
845 &qh,
846 wl_surface,
847 Layer::Background,
848 Some("wallr"),
849 None,
850 );
851 layer_surface.set_anchor(Anchor::TOP | Anchor::BOTTOM | Anchor::LEFT | Anchor::RIGHT);
852 layer_surface.set_exclusive_zone(-1); layer_surface.set_keyboard_interactivity(KeyboardInteractivity::None); let compositor = globals
858 .bind::<wl_compositor::WlCompositor, WaylandState, smithay_client_toolkit::globals::GlobalData>(
859 &qh,
860 1..=4,
861 smithay_client_toolkit::globals::GlobalData,
862 )
863 .map_err(|e| DaemonError::StartError(format!("compositor bind failed: {e:?}")))?;
864 let empty_region = compositor.create_region(&qh, ());
865 layer_surface
866 .wl_surface()
867 .set_input_region(Some(&empty_region));
868 layer_surface.commit();
869 empty_region.destroy();
870
871 event_queue
872 .roundtrip(&mut wayland_state)
873 .map_err(|e| DaemonError::StartError(format!("roundtrip failed: {e:?}")))?;
874 event_queue
875 .roundtrip(&mut wayland_state)
876 .map_err(|e| DaemonError::StartError(format!("roundtrip2 failed: {e:?}")))?;
877
878 let scale_factor = if wayland_state.scale_factor > 0 {
879 wayland_state.scale_factor
880 } else {
881 1
882 };
883 layer_surface.wl_surface().set_buffer_scale(scale_factor);
884
885 let width = wayland_state.width * scale_factor as u32;
886 let height = wayland_state.height * scale_factor as u32;
887
888 let raw_surface = layer_surface.wl_surface().id().as_ptr() as *mut std::ffi::c_void;
889 wayland_state.surfaces.push(layer_surface);
890
891 let window_handle = WaylandWindow {
892 display: display_ptr,
893 surface: raw_surface,
894 };
895
896 let wgpu_surface = renderer
897 .instance
898 .create_surface(&window_handle)
899 .map_err(|e| DaemonError::StartError(format!("wgpu surface creation failed: {e:?}")))?;
900
901 let adapter = renderer
902 .instance
903 .request_adapter(&wgpu::RequestAdapterOptions {
904 compatible_surface: Some(&wgpu_surface),
905 power_preference: wgpu::PowerPreference::HighPerformance,
906 force_fallback_adapter: false,
907 })
908 .await;
909 let surf_format = adapter
910 .as_ref()
911 .map(|a| {
912 let caps = wgpu_surface.get_capabilities(a);
913 caps.formats
914 .into_iter()
915 .next()
916 .unwrap_or(wgpu::TextureFormat::Bgra8UnormSrgb)
917 })
918 .unwrap_or(wgpu::TextureFormat::Bgra8UnormSrgb);
919
920 let surf_config = wgpu::SurfaceConfiguration {
921 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
922 format: surf_format,
923 width,
924 height,
925 present_mode: wgpu::PresentMode::Fifo,
926 alpha_mode: wgpu::CompositeAlphaMode::Opaque,
927 view_formats: vec![],
928 desired_maximum_frame_latency: 2,
929 };
930 wgpu_surface.configure(&renderer.device, &surf_config);
931
932 let wgpu_surface: wgpu::Surface<'static> = unsafe { std::mem::transmute(wgpu_surface) };
936 let surface: &'static wgpu::Surface<'static> = Box::leak(Box::new(wgpu_surface));
940
941 let render_state = Arc::new(Mutex::new(RenderState {
942 renderer: std::sync::Arc::new(renderer),
943 surface,
944 render_lock: std::sync::Arc::new(std::sync::Mutex::new(())),
945 playback_gen: std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0)),
946 pacer: std::sync::Arc::new(LivePacer::new()),
947 current_bind: None,
948 current_tex: None,
949 width,
950 height,
951 current_width: 0,
952 current_height: 0,
953 format: surf_format,
954 video_playback: std::sync::Arc::new(crate::video::VideoPlayback::new()),
955 hw_accel: crate::video::HwAccel::from_config(&self.config.video.hw_decode),
956 }));
957
958 {
959 let state_path = dirs::cache_dir()
960 .unwrap_or_else(|| std::path::PathBuf::from("/tmp"))
961 .join("wallr/last_wallpaper");
962 if let Ok(path_str) = std::fs::read_to_string(&state_path) {
963 let p = std::path::Path::new(path_str.trim());
964 if p.exists() {
965 let mut rs = render_state.lock().await;
966 let effect =
967 crate::animation::Effect::Fade(crate::animation::FadeParams::default());
968 let _ = rs.set_wallpaper(p, &effect, 0).await;
969 }
970 }
971 }
972
973 let paused_clone = self.paused.clone();
974 let engine_clone = self.engine.clone();
975 let render_state_clone = render_state.clone();
976
977 {
981 let rs = render_state.clone();
982 let socket_path = socket_path.clone();
983 tokio::spawn(async move {
984 use tokio::signal::unix::{SignalKind, signal};
985 let mut term = signal(SignalKind::terminate()).expect("SIGTERM handler");
986 let mut int = signal(SignalKind::interrupt()).expect("SIGINT handler");
987 let mut hup = signal(SignalKind::hangup()).expect("SIGHUP handler");
988 tokio::select! {
989 _ = term.recv() => {}
990 _ = int.recv() => {}
991 _ = hup.recv() => {}
992 }
993 tracing::info!("Signal received, shutting down gracefully");
994 if let Ok(state) = rs.try_lock() {
995 state.video_playback.stop();
996 }
997 let _ = std::fs::remove_file(&socket_path);
998 std::process::exit(0);
999 });
1000 }
1001
1002 let ipc_socket_path = socket_path.clone();
1003 start_ipc_server(&socket_path, move |cmd| {
1004 let paused = paused_clone.clone();
1005 let engine = engine_clone.clone();
1006 let rs = render_state_clone.clone();
1007 let stop_socket = ipc_socket_path.clone();
1008 async move {
1009 match cmd {
1010 IpcCommand::Pause => {
1011 paused.store(true, Ordering::SeqCst);
1012 let rs_lock = rs.lock().await;
1013 rs_lock.video_playback.pause();
1014 IpcResponse {
1015 success: true,
1016 message: Some("Paused".into()),
1017 }
1018 }
1019 IpcCommand::Resume => {
1020 paused.store(false, Ordering::SeqCst);
1021 let rs_lock = rs.lock().await;
1022 rs_lock.video_playback.resume();
1023 IpcResponse {
1024 success: true,
1025 message: Some("Resumed".into()),
1026 }
1027 }
1028 IpcCommand::Reload => {
1029 let lock = engine.lock().await;
1030 match lock.reload() {
1031 Ok(_) => IpcResponse {
1032 success: true,
1033 message: Some("Reloaded".into()),
1034 },
1035 Err(e) => IpcResponse {
1036 success: false,
1037 message: Some(e.to_string()),
1038 },
1039 }
1040 }
1041 IpcCommand::Preview {
1042 path,
1043 effect,
1044 duration_ms,
1045 no_theme,
1046 theme_override,
1047 monitor,
1048 scaling_mode: _,
1049 } => {
1050 if paused.load(Ordering::SeqCst) {
1051 return IpcResponse {
1052 success: false,
1053 message: Some("Daemon is paused".into()),
1054 };
1055 }
1056 let p = std::path::PathBuf::from(&path);
1057 if !p.exists() {
1058 return IpcResponse {
1059 success: false,
1060 message: Some(format!("File not found: {}", path)),
1061 };
1062 }
1063
1064 let state_path = dirs::cache_dir()
1065 .unwrap_or_else(|| std::path::PathBuf::from("/tmp"))
1066 .join("wallr/last_wallpaper");
1067 if let Some(parent) = state_path.parent() {
1068 let _ = std::fs::create_dir_all(parent);
1069 }
1070 let _ = std::fs::write(&state_path, &path);
1071
1072 let effect = effect.unwrap_or_else(|| {
1073 crate::animation::Effect::Fade(crate::animation::FadeParams::default())
1074 });
1075 let is_video = crate::video::VideoDecoder::is_video_file(&p);
1079 let duration = duration_ms.unwrap_or(if is_video { 150 } else { 2000 });
1080
1081 let rs_clone = rs.clone();
1082 let p_clone = p.clone();
1083 let result = tokio::task::spawn_blocking(move || {
1084 let rt = tokio::runtime::Handle::current();
1085 rt.block_on(async {
1086 let mut lock = rs_clone.lock().await;
1087 lock.set_wallpaper(&p_clone, &effect, duration).await
1088 })
1089 })
1090 .await;
1091
1092 match result {
1093 Ok(Ok(())) => {
1094 let opts = SetOptions {
1095 no_theme,
1096 theme_provider: theme_override,
1097 monitor,
1098 };
1099 let mut eng = engine.lock().await;
1100 match eng.set_wallpaper(&p, &opts).await {
1101 Ok(()) => IpcResponse {
1102 success: true,
1103 message: None,
1104 },
1105 Err(e) => IpcResponse {
1106 success: true,
1107 message: Some(format!(
1108 "Wallpaper set, but hooks/theme failed: {e}"
1109 )),
1110 },
1111 }
1112 }
1113 Ok(Err(e)) => IpcResponse {
1114 success: false,
1115 message: Some(format!("Render failed: {}", e)),
1116 },
1117 Err(e) => IpcResponse {
1118 success: false,
1119 message: Some(format!("Task spawn failed: {}", e)),
1120 },
1121 }
1122 }
1123 IpcCommand::Stop => {
1124 let sp = stop_socket.clone();
1125 tokio::spawn(async move {
1126 tokio::time::sleep(std::time::Duration::from_millis(300)).await;
1127 let _ = std::fs::remove_file(&sp);
1128 std::process::exit(0);
1129 });
1130 IpcResponse {
1131 success: true,
1132 message: Some("Stopping".into()),
1133 }
1134 }
1135 IpcCommand::Status => {
1136 let state = if paused.load(Ordering::SeqCst) {
1137 "paused"
1138 } else {
1139 "running"
1140 };
1141 IpcResponse {
1142 success: true,
1143 message: Some(format!("wallr daemon {}", state)),
1144 }
1145 }
1146 IpcCommand::Seek { timestamp_ms } => {
1147 let rs_lock = rs.lock().await;
1148 match rs_lock
1149 .video_playback
1150 .seek(std::time::Duration::from_millis(timestamp_ms))
1151 {
1152 Ok(()) => IpcResponse {
1153 success: true,
1154 message: Some(format!("Seeked to {}ms", timestamp_ms)),
1155 },
1156 Err(e) => IpcResponse {
1157 success: false,
1158 message: Some(format!("Seek failed: {}", e)),
1159 },
1160 }
1161 }
1162 IpcCommand::Info => {
1163 let rs_lock = rs.lock().await;
1164
1165 let gpu_info =
1167 crate::video::gpu::adapter_diagnostics(&rs_lock.renderer.adapter);
1168
1169 let mut lines = vec![
1170 format!("wallr v{}", env!("CARGO_PKG_VERSION")),
1171 String::new(),
1172 gpu_info,
1173 ];
1174
1175 match rs_lock.video_playback.metadata() {
1176 Some(meta) => {
1177 let decoder_info = rs_lock.video_playback.decoder_info();
1178 let hw = rs_lock.video_playback.hw_accel_in_use();
1179 let state = if rs_lock.video_playback.is_paused() {
1180 "paused"
1181 } else {
1182 "playing"
1183 };
1184 let position = rs_lock
1185 .video_playback
1186 .position()
1187 .map(|p| format!("{:.2}s", p.as_secs_f64()))
1188 .unwrap_or_else(|| "?".to_string());
1189 lines.push(String::new());
1190 lines.push("Video:".into());
1191 lines.push(format!(" Resolution: {}x{}", meta.width, meta.height));
1192 lines.push(format!(" FPS: {:.2}", meta.fps));
1193 lines.push(format!(
1194 " Duration: {:.2}s",
1195 meta.duration.as_secs_f64()
1196 ));
1197 lines.push(format!(
1198 " Codec: {}",
1199 decoder_info
1200 .as_ref()
1201 .map(|d| d.codec_name.as_str())
1202 .unwrap_or("unknown")
1203 ));
1204 lines.push(format!(" Container: {}", meta.format));
1205 lines.push(format!(" Decoder: {}", hw.name()));
1206 lines.push(format!(
1207 " GPU Decode: {}",
1208 if hw == crate::video::HwAccel::Software {
1209 "disabled"
1210 } else {
1211 "enabled"
1212 }
1213 ));
1214 lines.push(format!(" State: {} @ {}", state, position));
1215 }
1216 None => {
1217 lines.push(String::new());
1218 lines.push("Video: none active".into());
1219 lines.push("Decoder: idle".into());
1220 }
1221 }
1222
1223 IpcResponse {
1224 success: true,
1225 message: Some(lines.join("\n")),
1226 }
1227 }
1228 }
1229 }
1230 })
1231 .await?;
1232
1233 if self.config.watch.enabled
1235 && let Some(ref watch_dir) = self.config.watch.dir
1236 {
1237 let watch_path = crate::config::expand_path(watch_dir);
1238 self.start_watcher(watch_path, render_state.clone()).await?;
1239 }
1240
1241 tokio::task::spawn_blocking(move || {
1242 loop {
1243 if let Err(e) = event_queue.blocking_dispatch(&mut wayland_state) {
1244 eprintln!("Wayland dispatch error: {e:?}");
1245 break;
1246 }
1247 }
1248 eprintln!("wallr: Wayland connection lost, exiting");
1252 std::process::exit(1);
1253 });
1254
1255 loop {
1256 tokio::time::sleep(std::time::Duration::from_secs(3600)).await;
1257 }
1258 }
1259
1260 async fn start_watcher(
1261 &self,
1262 dir: PathBuf,
1263 render_state: Arc<Mutex<RenderState>>,
1264 ) -> Result<(), DaemonError> {
1265 let engine = self.engine.clone();
1266 let paused = self.paused.clone();
1267 let debounce = crate::config::parse_duration(&self.config.watch.debounce)
1268 .unwrap_or(std::time::Duration::from_millis(500));
1269
1270 let (tx, mut rx) = tokio::sync::mpsc::channel(100);
1271
1272 let mut watcher = notify::recommended_watcher(move |res: Result<Event, notify::Error>| {
1273 if let Ok(event) = res
1274 && let EventKind::Create(_) = event.kind
1275 {
1276 for path in event.paths {
1277 let _ = tx.blocking_send(path);
1278 }
1279 }
1280 })
1281 .map_err(|e| DaemonError::StartError(e.to_string()))?;
1282
1283 watcher
1284 .watch(&dir, RecursiveMode::NonRecursive)
1285 .map_err(|e| DaemonError::StartError(e.to_string()))?;
1286
1287 tokio::spawn(async move {
1288 let _watcher = watcher;
1289 let mut last: Option<(PathBuf, std::time::Instant)> = None;
1290
1291 while let Some(path) = rx.recv().await {
1292 if paused.load(Ordering::SeqCst) {
1293 continue;
1294 }
1295 if let Some((ref lp, ref lt)) = last
1296 && lp == &path
1297 && lt.elapsed() < debounce
1298 {
1299 continue;
1300 }
1301 let ext = path
1302 .extension()
1303 .unwrap_or_default()
1304 .to_string_lossy()
1305 .to_lowercase();
1306 if !["jpg", "jpeg", "png", "gif", "webp"].contains(&ext.as_str()) {
1307 continue;
1308 }
1309 last = Some((path.clone(), std::time::Instant::now()));
1310
1311 let rs = render_state.clone();
1312 let eng = engine.clone();
1313 let p = path.clone();
1314 tokio::spawn(async move {
1315 let mut lock = rs.lock().await;
1316 let effect =
1317 crate::animation::Effect::Fade(crate::animation::FadeParams::default());
1318 let _ = lock.set_wallpaper(&p, &effect, 600).await;
1319 drop(lock);
1320 let opts = SetOptions {
1321 no_theme: false,
1322 theme_provider: None,
1323 monitor: None,
1324 };
1325 let mut elock = eng.lock().await;
1326 let _ = elock.set_wallpaper(&p, &opts).await;
1327 });
1328 }
1329 });
1330
1331 Ok(())
1332 }
1333}