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 scaling_mode: u32,
271}
272
273struct 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: Option<crate::animated::AnimatedImage>,
288 is_video: bool,
290 generation: u64,
293 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 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 if crate::video::VideoDecoder::is_video_file(path) {
323 tracing::info!("Video file detected: {:?}", path);
324
325 let generation = self.playback_gen.fetch_add(1, Ordering::SeqCst) + 1;
331 self.pacer.notify();
332
333 let metadata = self.video_playback.start(path, self.hw_accel)?;
336
337 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 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 self.video_playback.stop();
395
396 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 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 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#[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 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
551fn 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 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 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; let mut cur_frame = 0usize; let mut next_frame = 0usize; let mut slot = 0usize; 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 _ => return,
693 }
694
695 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
719fn 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 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 let mut uploaded = false;
742
743 loop {
744 if playback_gen.load(Ordering::SeqCst) != commit.generation {
749 return;
750 }
751
752 if let Some(frame) = video_playback.next_frame() {
755 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 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 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); layer_surface.set_keyboard_interactivity(KeyboardInteractivity::None); 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 let wgpu_surface: wgpu::Surface<'static> = unsafe { std::mem::transmute(wgpu_surface) };
951 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 {
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 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 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 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 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}