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
75#[derive(Clone)]
76struct OutputInfo {
77 name: String,
78 width: u32,
79 height: u32,
80 scale_factor: i32,
81 wl_output: wl_output::WlOutput,
82}
83
84struct WaylandState {
85 registry_state: RegistryState,
86 output_state: OutputState,
87 compositor_state: CompositorState,
88 shm: Shm,
89 outputs: std::collections::HashMap<u32, OutputInfo>,
90 surfaces: Vec<(u32, LayerSurface)>,
91 layer_shell: LayerShell,
93 compositor: wl_compositor::WlCompositor,
95 hotplug: Option<DaemonHotplug>,
98}
99
100struct SendDisplayPtr(*mut std::ffi::c_void);
103unsafe impl Send for SendDisplayPtr {}
104
105struct DaemonHotplug {
109 renderer: std::sync::Arc<Renderer>,
110 config: crate::config::WallrConfig,
111 display_ptr: SendDisplayPtr,
112 render_states: std::sync::Arc<
117 tokio::sync::Mutex<
118 std::collections::HashMap<String, std::sync::Arc<tokio::sync::Mutex<RenderState>>>,
119 >,
120 >,
121}
122
123impl ProvidesRegistryState for WaylandState {
124 fn registry(&mut self) -> &mut RegistryState {
125 &mut self.registry_state
126 }
127
128 registry_handlers![OutputState,];
129}
130
131impl CompositorHandler for WaylandState {
132 fn scale_factor_changed(
133 &mut self,
134 _conn: &Connection,
135 _qh: &QueueHandle<Self>,
136 _surface: &wl_surface::WlSurface,
137 _new_factor: i32,
138 ) {
139 }
140 fn transform_changed(
141 &mut self,
142 _conn: &Connection,
143 _qh: &QueueHandle<Self>,
144 _surface: &wl_surface::WlSurface,
145 _new_transform: wl_output::Transform,
146 ) {
147 }
148 fn frame(
149 &mut self,
150 _conn: &Connection,
151 _qh: &QueueHandle<Self>,
152 _surface: &wl_surface::WlSurface,
153 _time: u32,
154 ) {
155 }
156 fn surface_enter(
157 &mut self,
158 _conn: &Connection,
159 _qh: &QueueHandle<Self>,
160 _surface: &wl_surface::WlSurface,
161 _output: &wl_output::WlOutput,
162 ) {
163 }
164 fn surface_leave(
165 &mut self,
166 _conn: &Connection,
167 _qh: &QueueHandle<Self>,
168 _surface: &wl_surface::WlSurface,
169 _output: &wl_output::WlOutput,
170 ) {
171 }
172}
173
174impl wayland_client::Dispatch<wayland_client::protocol::wl_region::WlRegion, ()> for WaylandState {
175 fn event(
176 _state: &mut WaylandState,
177 _region: &wayland_client::protocol::wl_region::WlRegion,
178 _event: wayland_client::protocol::wl_region::Event,
179 _data: &(),
180 _conn: &Connection,
181 _qh: &QueueHandle<WaylandState>,
182 ) {
183 }
184}
185
186impl LayerShellHandler for WaylandState {
187 fn configure(
188 &mut self,
189 _conn: &Connection,
190 _qh: &QueueHandle<Self>,
191 layer: &LayerSurface,
192 _configure: LayerSurfaceConfigure,
193 _serial: u32,
194 ) {
195 layer.commit();
196 }
197
198 fn closed(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _layer: &LayerSurface) {}
199}
200
201impl ShmHandler for WaylandState {
202 fn shm_state(&mut self) -> &mut Shm {
203 &mut self.shm
204 }
205}
206
207impl OutputHandler for WaylandState {
208 fn output_state(&mut self) -> &mut OutputState {
209 &mut self.output_state
210 }
211 fn new_output(
212 &mut self,
213 _conn: &Connection,
214 _qh: &QueueHandle<Self>,
215 output: wl_output::WlOutput,
216 ) {
217 let id = output.id().protocol_id();
218 tracing::info!("Output detected: protocol_id={id}");
219
220 let mut info = OutputInfo {
221 name: format!("output-{id}"),
222 width: 1920,
223 height: 1080,
224 scale_factor: 1,
225 wl_output: output,
226 };
227
228 if let Some(info_data) = self.output_state.info(&info.wl_output) {
230 if let Some(mode) = info_data.modes.iter().find(|m| m.current) {
231 info.width = mode.dimensions.0 as u32;
232 info.height = mode.dimensions.1 as u32;
233 }
234 info.scale_factor = info_data.scale_factor;
235 let resolved = info_data
236 .name
237 .as_deref()
238 .filter(|n| !n.is_empty())
239 .or(info_data.description.as_deref().filter(|n| !n.is_empty()));
240 if let Some(real_name) = resolved {
241 info.name = real_name.to_string();
242 } else if !info_data.make.is_empty() || !info_data.model.is_empty() {
243 let fallback = format!("{} {}", info_data.make, info_data.model)
244 .trim()
245 .to_string();
246 if !fallback.is_empty() {
247 info.name = fallback;
248 }
249 }
250 }
251
252 self.outputs.insert(id, info.clone());
253
254 if self.hotplug.is_some() {
258 let name = info.name.clone();
259 let renderer = self.hotplug.as_ref().unwrap().renderer.clone();
260 let display_ptr = self.hotplug.as_ref().unwrap().display_ptr.0;
261 let config = self.hotplug.as_ref().unwrap().config.clone();
262 let render_states = self.hotplug.as_ref().unwrap().render_states.clone();
263 match create_render_state_for_output_sync(
264 &renderer,
265 display_ptr,
266 self,
267 _qh,
268 &info,
269 &config,
270 ) {
271 Ok(rs) => {
272 let rs = std::sync::Arc::new(tokio::sync::Mutex::new(rs));
273 tokio::spawn(async move {
274 render_states.lock().await.insert(name.clone(), rs.clone());
275
276 let state_path = dirs::cache_dir()
278 .unwrap_or_else(|| std::path::PathBuf::from("/tmp"))
279 .join(format!("wallr/last_wallpaper/{name}"));
280 if let Ok(path_str) = std::fs::read_to_string(&state_path) {
281 let p = std::path::Path::new(path_str.trim());
282 if p.exists() {
283 let mut lock = rs.lock().await;
284 let effect = crate::animation::Effect::Fade(
285 crate::animation::FadeParams::default(),
286 );
287 let _ = lock.set_wallpaper(p, &effect, 0, 0).await;
288 }
289 }
290 tracing::info!("Hotplug: output {name} ready");
291 });
292 }
293 Err(e) => {
294 tracing::error!("Hotplug: failed to create render state for {name}: {e}");
295 }
296 }
297 }
298 }
299 fn update_output(
300 &mut self,
301 _conn: &Connection,
302 _qh: &QueueHandle<Self>,
303 output: wl_output::WlOutput,
304 ) {
305 let id = output.id().protocol_id();
306 if let Some(info) = self.outputs.get_mut(&id) {
307 let mut new_width = info.width;
308 let mut new_height = info.height;
309 let mut new_scale = info.scale_factor;
310
311 if let Some(mode) = self
312 .output_state
313 .info(&output)
314 .and_then(|i| i.modes.iter().find(|m| m.current).cloned())
315 {
316 new_width = mode.dimensions.0 as u32;
317 new_height = mode.dimensions.1 as u32;
318 }
319 if let Some(info_data) = self.output_state.info(&output) {
320 new_scale = info_data.scale_factor;
321 let resolved = info_data
322 .name
323 .as_deref()
324 .filter(|n| !n.is_empty())
325 .or(info_data.description.as_deref().filter(|n| !n.is_empty()));
326 if let Some(real_name) = resolved {
327 if real_name != info.name {
328 tracing::info!(
329 "Output {id}: resolved name '{}' -> '{}'",
330 info.name,
331 real_name
332 );
333 info.name = real_name.to_string();
334 }
335 } else if info.name.starts_with("output-")
336 && (!info_data.make.is_empty() || !info_data.model.is_empty())
337 {
338 let fallback = format!("{} {}", info_data.make, info_data.model)
339 .trim()
340 .to_string();
341 if !fallback.is_empty() {
342 tracing::info!(
343 "Output {id}: fallback name '{}' -> '{}'",
344 info.name,
345 fallback
346 );
347 info.name = fallback;
348 }
349 }
350 }
351
352 let new_w = new_width * new_scale as u32;
353 let new_h = new_height * new_scale as u32;
354 let changed =
355 new_w != info.width || new_h != info.height || new_scale != info.scale_factor;
356
357 info.width = new_width;
358 info.height = new_height;
359 info.scale_factor = new_scale;
360
361 if changed {
363 let name = info.name.clone();
364 if let Some(ref hotplug) = self.hotplug {
365 let render_states = hotplug.render_states.clone();
366 let renderer = hotplug.renderer.clone();
367 tokio::spawn(async move {
368 let states = render_states.lock().await;
369 if let Some(rs) = states.get(&name) {
370 let mut lock = rs.lock().await;
371 lock.width = new_w;
372 lock.height = new_h;
373 let surf_config = wgpu::SurfaceConfiguration {
374 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
375 format: lock.format,
376 width: new_w,
377 height: new_h,
378 present_mode: wgpu::PresentMode::Fifo,
379 alpha_mode: wgpu::CompositeAlphaMode::Opaque,
380 view_formats: vec![],
381 desired_maximum_frame_latency: 2,
382 };
383 lock.surface.configure(&renderer.device, &surf_config);
384 tracing::info!("Hotplug: reconfigured {name} to {new_w}x{new_h}");
385 }
386 });
387 }
388 }
389 }
390 }
391 fn output_destroyed(
392 &mut self,
393 _conn: &Connection,
394 _qh: &QueueHandle<Self>,
395 output: wl_output::WlOutput,
396 ) {
397 let id = output.id().protocol_id();
398 if let Some(info) = self.outputs.remove(&id) {
399 tracing::info!("Output disconnected: {} (protocol_id={})", info.name, id);
400 self.surfaces.retain(|(pid, _)| *pid != id);
402 if let Some(ref hotplug) = self.hotplug {
404 let name = info.name.clone();
405 let render_states = hotplug.render_states.clone();
406 tokio::spawn(async move {
407 if let Some(rs) = render_states.lock().await.remove(&name) {
408 let lock = rs.lock().await;
409 lock.video_playback.stop();
410 tracing::info!("Hotplug: cleaned up render state for {name}");
411 }
412 });
413 }
414 }
415 }
416}
417
418delegate_compositor!(WaylandState);
419delegate_layer!(WaylandState);
420delegate_output!(WaylandState);
421delegate_registry!(WaylandState);
422delegate_shm!(WaylandState);
423
424struct LivePacer {
426 lock: std::sync::Mutex<()>,
427 cond: std::sync::Condvar,
428}
429
430impl LivePacer {
431 fn new() -> Self {
432 Self {
433 lock: std::sync::Mutex::new(()),
434 cond: std::sync::Condvar::new(),
435 }
436 }
437
438 fn notify(&self) {
439 let _guard = self.lock.lock().unwrap();
440 self.cond.notify_all();
441 }
442
443 fn wait_until(&self, deadline: std::time::Instant) {
446 let guard = self.lock.lock().unwrap();
447 let now = std::time::Instant::now();
448 if deadline <= now {
449 return;
450 }
451 let _ = self
452 .cond
453 .wait_timeout_while(guard, deadline - now, |_| true);
454 }
455}
456
457struct RenderState {
458 renderer: std::sync::Arc<Renderer>,
459 surface: &'static wgpu::Surface<'static>,
460 render_lock: std::sync::Arc<std::sync::Mutex<()>>,
464 playback_gen: std::sync::Arc<std::sync::atomic::AtomicU64>,
467 pacer: std::sync::Arc<LivePacer>,
470 current_bind: Option<wgpu::BindGroup>,
471 current_tex: Option<wgpu::Texture>,
472 width: u32,
473 height: u32,
474 current_width: u32,
475 current_height: u32,
476 format: wgpu::TextureFormat,
477 video_playback: std::sync::Arc<crate::video::VideoPlayback>,
479 hw_accel: crate::video::HwAccel,
481 scaling_mode: u32,
483 per_output_uniforms: std::sync::Arc<crate::renderer::PerOutputUniforms>,
485 last_wallpaper: Option<std::path::PathBuf>,
487 pre_blank: Option<(std::path::PathBuf, u32)>,
489 blanked: bool,
491}
492
493struct CommitData {
496 bg_bind: wgpu::BindGroup,
497 new_bind: wgpu::BindGroup,
498 img_width: u32,
499 img_height: u32,
500 old_img_width: u32,
501 old_img_height: u32,
502 format: wgpu::TextureFormat,
503 width: u32,
504 height: u32,
505 animated: Option<crate::animated::AnimatedImage>,
508 is_video: bool,
510 generation: u64,
513 scaling_mode: u32,
515}
516
517impl RenderState {
518 async fn set_wallpaper(
519 &mut self,
520 path: &std::path::Path,
521 effect: &crate::animation::Effect,
522 duration_ms: u32,
523 scaling_mode: u32,
524 ) -> anyhow::Result<()> {
525 self.scaling_mode = scaling_mode;
526 let commit = self.commit_wallpaper(path, scaling_mode)?;
527 self.spawn_transition(commit, effect, duration_ms);
528 Ok(())
529 }
530
531 fn commit_wallpaper(
535 &mut self,
536 path: &std::path::Path,
537 scaling_mode: u32,
538 ) -> anyhow::Result<CommitData> {
539 use image::ImageReader;
540
541 if crate::video::VideoDecoder::is_video_file(path) {
543 tracing::info!("Video file detected: {:?}", path);
544
545 let generation = self.playback_gen.fetch_add(1, Ordering::SeqCst) + 1;
551 self.pacer.notify();
552
553 let metadata = self.video_playback.start(path, self.hw_accel)?;
556
557 let first_frame = self
560 .video_playback
561 .wait_first_frame(std::time::Duration::from_millis(1000));
562
563 let (new_tex, new_bind, img_width, img_height) = if let Some(frame) = first_frame {
564 let (tex, bind) = self.renderer.create_texture(frame.width, frame.height);
565 self.renderer
566 .update_texture(&tex, &frame.data, frame.width, frame.height);
567 (tex, bind, frame.width, frame.height)
568 } else {
569 tracing::warn!("No first frame available, using black texture");
571 let (tex, bind) = self
572 .renderer
573 .create_texture(metadata.width, metadata.height);
574 let black = vec![0u8; (metadata.width * metadata.height * 4) as usize];
575 self.renderer
576 .update_texture(&tex, &black, metadata.width, metadata.height);
577 (tex, bind, metadata.width, metadata.height)
578 };
579
580 let old_bind = self.current_bind.take();
581 let (old_img_width, old_img_height) = if old_bind.is_some() {
582 (self.current_width.max(1), self.current_height.max(1))
583 } else {
584 (img_width, img_height)
585 };
586 let bg_bind = old_bind.unwrap_or_else(|| new_bind.clone());
587
588 drop(self.current_tex.take());
589 self.current_tex = Some(new_tex);
590 self.current_bind = Some(new_bind.clone());
591 self.current_width = img_width;
592 self.current_height = img_height;
593
594 return Ok(CommitData {
595 bg_bind,
596 new_bind,
597 img_width,
598 img_height,
599 old_img_width,
600 old_img_height,
601 format: self.format,
602 width: self.width,
603 height: self.height,
604 animated: None,
605 is_video: true,
606 generation,
607 scaling_mode,
608 });
609 }
610
611 self.video_playback.stop();
615
616 let mut animated = crate::animated::AnimatedImage::decode(path)?;
619 let (new_tex, new_bind, img_width, img_height) = if let Some(anim) = animated.as_mut() {
620 let (w, h) = (anim.width, anim.height);
621 let (tex, bind) = self.renderer.create_texture(w, h);
622 let first = anim.first_frame();
623 if !first.is_empty() {
624 self.renderer.update_texture(&tex, first, w, h);
625 }
626 (tex, bind, w, h)
627 } else {
628 let new_img = ImageReader::open(path)?.decode()?;
629 let (tex, bind) = self.renderer.load_texture(&new_img)?;
630 (tex, bind, new_img.width(), new_img.height())
631 };
632
633 let old_bind = self.current_bind.take();
634 let (old_img_width, old_img_height) = if old_bind.is_some() {
635 (self.current_width.max(1), self.current_height.max(1))
636 } else {
637 (img_width, img_height)
638 };
639 let bg_bind = old_bind.unwrap_or_else(|| new_bind.clone());
644
645 drop(self.current_tex.take());
646 self.current_tex = Some(new_tex);
647 self.current_bind = Some(new_bind.clone());
648 self.current_width = img_width;
649 self.current_height = img_height;
650
651 let generation = self.playback_gen.fetch_add(1, Ordering::SeqCst) + 1;
652 self.pacer.notify();
653
654 Ok(CommitData {
655 bg_bind,
656 new_bind,
657 img_width,
658 img_height,
659 old_img_width,
660 old_img_height,
661 format: self.format,
662 width: self.width,
663 height: self.height,
664 animated,
665 is_video: false,
666 generation,
667 scaling_mode,
668 })
669 }
670
671 fn spawn_transition(
676 &self,
677 commit: CommitData,
678 effect: &crate::animation::Effect,
679 duration_ms: u32,
680 ) {
681 let renderer = self.renderer.clone();
682 let surface: &'static wgpu::Surface<'static> = self.surface;
683 let render_lock = self.render_lock.clone();
684 let playback_gen = self.playback_gen.clone();
685 let pacer = self.pacer.clone();
686 let video_playback = self.video_playback.clone();
687 let per_output_uniforms = std::sync::Arc::clone(&self.per_output_uniforms);
688 let effect = effect.clone();
689 drop(tokio::task::spawn_blocking(move || {
690 render_transition(
691 renderer,
692 surface,
693 render_lock,
694 playback_gen,
695 pacer,
696 video_playback,
697 commit,
698 effect,
699 duration_ms,
700 &per_output_uniforms,
701 );
702 }));
703 }
704}
705
706#[allow(clippy::too_many_arguments)]
714fn render_transition(
715 renderer: std::sync::Arc<Renderer>,
716 surface: &'static wgpu::Surface<'static>,
717 render_lock: std::sync::Arc<std::sync::Mutex<()>>,
718 playback_gen: std::sync::Arc<std::sync::atomic::AtomicU64>,
719 pacer: std::sync::Arc<LivePacer>,
720 video_playback: std::sync::Arc<crate::video::VideoPlayback>,
721 mut commit: CommitData,
722 effect: crate::animation::Effect,
723 duration_ms: u32,
724 per_output_uniforms: &crate::renderer::PerOutputUniforms,
725) {
726 let _guard = render_lock
727 .lock()
728 .unwrap_or_else(|poisoned| poisoned.into_inner());
729
730 let duration = std::time::Duration::from_millis(u64::from(duration_ms.max(1)));
731 let start = std::time::Instant::now();
732 loop {
733 let progress = start.elapsed().as_secs_f32() / duration.as_secs_f32();
734 let uniforms = crate::animation::compute_effect_uniforms(&effect, progress.clamp(0.0, 1.0));
735 let status = renderer.render_frame(
736 crate::renderer::FrameRequest {
737 surface,
738 format: commit.format,
739 bg_bind: &commit.bg_bind,
740 new_bind: &commit.new_bind,
741 effect: &uniforms,
742 width: commit.width,
743 height: commit.height,
744 img_width: commit.img_width,
745 img_height: commit.img_height,
746 old_img_width: commit.old_img_width,
747 old_img_height: commit.old_img_height,
748 scaling_mode: commit.scaling_mode,
749 },
750 per_output_uniforms,
751 );
752 let status = match status {
753 Ok(status) => status,
754 Err(err) => {
755 eprintln!("wallr: transition render failed: {err}");
756 break;
757 }
758 };
759 if progress >= 1.0 || status == crate::renderer::FrameStatus::TimedOut {
760 break;
761 }
762 }
763
764 let mut animated = commit.animated.take();
768 if let Some(animated) = animated.as_mut()
769 && playback_gen.load(Ordering::SeqCst) == commit.generation
770 {
771 play_live(
772 &renderer,
773 surface,
774 &commit,
775 animated,
776 &playback_gen,
777 &pacer,
778 per_output_uniforms,
779 );
780 } else if commit.is_video && playback_gen.load(Ordering::SeqCst) == commit.generation {
781 play_video(
782 &renderer,
783 surface,
784 &commit,
785 &video_playback,
786 &playback_gen,
787 per_output_uniforms,
788 );
789 }
790}
791
792fn play_live(
798 renderer: &Renderer,
799 surface: &'static wgpu::Surface<'static>,
800 commit: &CommitData,
801 animated: &mut crate::animated::AnimatedImage,
802 playback_gen: &std::sync::atomic::AtomicU64,
803 pacer: &LivePacer,
804 per_output_uniforms: &crate::renderer::PerOutputUniforms,
805) {
806 let (tex_a, bind_a) = renderer.create_texture(animated.width, animated.height);
807 let (tex_b, bind_b) = renderer.create_texture(animated.width, animated.height);
808 let (frame_w, frame_h) = (animated.width, animated.height);
809 let (bytes_per_row, rows) = (frame_w * 4, frame_h);
810 let frame_bytes = bytes_per_row as u64 * rows as u64;
811
812 let direct_upload = bytes_per_row % 256 == 0;
815 let staging: Vec<wgpu::Buffer> = if direct_upload {
816 (0..2)
817 .map(|_| {
818 renderer.device.create_buffer(&wgpu::BufferDescriptor {
819 label: Some("wallr-gif-staging"),
820 size: frame_bytes,
821 usage: wgpu::BufferUsages::MAP_WRITE | wgpu::BufferUsages::COPY_SRC,
822 mapped_at_creation: false,
823 })
824 })
825 .collect()
826 } else {
827 Vec::new()
828 };
829
830 let first = animated.first_frame();
831 if !first.is_empty() {
832 renderer.update_texture(&tex_a, first, frame_w, frame_h);
833 renderer.update_texture(&tex_b, first, frame_w, frame_h);
834 }
835 let binds = [bind_a, bind_b];
836 let textures = [tex_a, tex_b];
837
838 let upload = |renderer: &Renderer,
841 tgt: usize,
842 index: usize,
843 slot: usize,
844 animated: &mut crate::animated::AnimatedImage|
845 -> bool {
846 if direct_upload {
847 let buffer = &staging[slot];
848 let slice = buffer.slice(..);
849 slice.map_async(wgpu::MapMode::Write, |_| {});
850 renderer.device.poll(wgpu::Maintain::Wait);
851 let ok = {
852 let mut mapped = slice.get_mapped_range_mut();
853 animated.decompress_into(index, &mut mapped)
854 };
855 buffer.unmap();
856 if ok {
857 let mut encoder = renderer
858 .device
859 .create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
860 encoder.copy_buffer_to_texture(
861 wgpu::TexelCopyBufferInfo {
862 buffer,
863 layout: wgpu::TexelCopyBufferLayout {
864 offset: 0,
865 bytes_per_row: Some(bytes_per_row),
866 rows_per_image: Some(rows),
867 },
868 },
869 wgpu::TexelCopyTextureInfo {
870 texture: &textures[tgt],
871 mip_level: 0,
872 origin: wgpu::Origin3d::ZERO,
873 aspect: wgpu::TextureAspect::All,
874 },
875 wgpu::Extent3d {
876 width: frame_w,
877 height: frame_h,
878 depth_or_array_layers: 1,
879 },
880 );
881 renderer.queue.submit([encoder.finish()]);
882 return true;
883 }
884 } else {
885 let frame = animated.frame_at(index);
886 if !frame.is_empty() {
887 renderer.update_texture(&textures[tgt], frame, frame_w, frame_h);
888 return true;
889 }
890 }
891 false
892 };
893
894 let mut cur = 0usize; let mut cur_frame = 0usize; let mut next_frame = 0usize; let mut slot = 0usize; let start = std::time::Instant::now();
899 let static_effect = crate::animation::Effect::Fade(crate::animation::FadeParams::default());
900 loop {
901 if playback_gen.load(Ordering::SeqCst) != commit.generation {
902 return;
903 }
904 let index = animated.frame_index_at(start.elapsed());
905 if index != cur_frame {
906 if next_frame != index {
907 upload(renderer, cur ^ 1, index, slot, animated);
908 slot ^= 1;
909 next_frame = index;
910 }
911 cur ^= 1;
912 cur_frame = index;
913 }
914 let uniforms = crate::animation::compute_effect_uniforms(&static_effect, 1.0);
915 let status = renderer.render_frame(
916 crate::renderer::FrameRequest {
917 surface,
918 format: commit.format,
919 bg_bind: &binds[cur],
920 new_bind: &binds[cur],
921 effect: &uniforms,
922 width: commit.width,
923 height: commit.height,
924 img_width: animated.width,
925 img_height: animated.height,
926 old_img_width: animated.width,
927 old_img_height: animated.height,
928 scaling_mode: commit.scaling_mode,
929 },
930 per_output_uniforms,
931 );
932 match status {
933 Ok(crate::renderer::FrameStatus::Presented) => {}
934 _ => return,
935 }
936
937 let elapsed = start.elapsed();
945 let total: std::time::Duration = animated.total_duration();
946 let loops = (elapsed.as_millis() / total.as_millis().max(1)) as u64;
947 let next_change = animated.frame_start(index + 1) + total * (loops as u32);
948 let wait = next_change.saturating_sub(elapsed);
949 if wait > std::time::Duration::ZERO {
950 let next = index + 1;
951 if next_frame != next {
952 upload(renderer, cur ^ 1, next, slot, animated);
953 slot ^= 1;
954 next_frame = next;
955 }
956 pacer.wait_until(std::time::Instant::now() + wait);
957 }
958 }
959}
960
961fn play_video(
963 renderer: &Renderer,
964 surface: &'static wgpu::Surface<'static>,
965 commit: &CommitData,
966 video_playback: &std::sync::Arc<crate::video::VideoPlayback>,
967 playback_gen: &std::sync::atomic::AtomicU64,
968 per_output_uniforms: &crate::renderer::PerOutputUniforms,
969) {
970 let (width, height) = match video_playback.metadata() {
972 Some(meta) => (meta.width, meta.height),
973 None => {
974 tracing::warn!("No video metadata available");
975 return;
976 }
977 };
978
979 let (texture, bind) = renderer.create_texture(width, height);
980 let static_effect = crate::animation::Effect::Fade(crate::animation::FadeParams::default());
981
982 let mut uploaded = false;
985
986 loop {
987 if playback_gen.load(Ordering::SeqCst) != commit.generation {
992 return;
993 }
994
995 if let Some(frame) = video_playback.next_frame() {
998 if frame.width != width || frame.height != height {
1001 continue;
1002 }
1003 renderer.update_texture(&texture, &frame.data, frame.width, frame.height);
1004 uploaded = true;
1005 }
1006
1007 if !uploaded {
1008 std::thread::sleep(std::time::Duration::from_millis(2));
1011 continue;
1012 }
1013
1014 let uniforms = crate::animation::compute_effect_uniforms(&static_effect, 1.0);
1015 let status = renderer.render_frame(
1016 crate::renderer::FrameRequest {
1017 surface,
1018 format: commit.format,
1019 bg_bind: &bind,
1020 new_bind: &bind,
1021 effect: &uniforms,
1022 width: commit.width,
1023 height: commit.height,
1024 img_width: width,
1025 img_height: height,
1026 old_img_width: width,
1027 old_img_height: height,
1028 scaling_mode: commit.scaling_mode,
1029 },
1030 per_output_uniforms,
1031 );
1032
1033 match status {
1034 Ok(crate::renderer::FrameStatus::Presented) => {}
1035 other => {
1038 tracing::warn!("Video present failed ({:?}), stopping playback", other);
1039 video_playback.stop();
1040 return;
1041 }
1042 }
1043 }
1044}
1045
1046async fn resolve_targets(
1051 render_states: &std::collections::HashMap<
1052 String,
1053 std::sync::Arc<tokio::sync::Mutex<RenderState>>,
1054 >,
1055 monitor: Option<&str>,
1056) -> Vec<std::sync::Arc<tokio::sync::Mutex<RenderState>>> {
1057 match monitor {
1058 Some(name) => {
1059 if let Some(rs) = render_states.get(name) {
1060 vec![rs.clone()]
1061 } else {
1062 Vec::new()
1063 }
1064 }
1065 None => render_states.values().cloned().collect(),
1066 }
1067}
1068
1069pub struct Daemon {
1070 config: WallrConfig,
1071 paused: Arc<AtomicBool>,
1072 engine: Arc<Mutex<WallpaperEngine>>,
1073}
1074
1075impl Daemon {
1076 pub fn new(config: WallrConfig) -> Result<Self, DaemonError> {
1077 let engine = WallpaperEngine::new(config.clone())?;
1078 Ok(Self {
1079 config,
1080 paused: Arc::new(AtomicBool::new(false)),
1081 engine: Arc::new(Mutex::new(engine)),
1082 })
1083 }
1084
1085 pub async fn start(self) -> Result<(), DaemonError> {
1086 let socket_path = crate::config::expand_path(&self.config.daemon.socket);
1087 if socket_path.exists() {
1088 if tokio::net::UnixStream::connect(&socket_path).await.is_ok() {
1089 return Err(DaemonError::AlreadyRunning(
1090 socket_path.to_string_lossy().to_string(),
1091 ));
1092 }
1093 let _ = std::fs::remove_file(&socket_path);
1094 }
1095
1096 let renderer = Renderer::new()
1097 .await
1098 .map_err(|e| DaemonError::StartError(format!("GPU init failed: {e}")))?;
1099
1100 let conn = Connection::connect_to_env()
1101 .map_err(|e| DaemonError::StartError(format!("Failed to connect to Wayland: {e:?}")))?;
1102 let backend = conn.backend();
1103 let display_ptr = backend.display_ptr() as *mut std::ffi::c_void;
1104
1105 let (globals, mut event_queue) = registry_queue_init(&conn)
1106 .map_err(|e| DaemonError::StartError(format!("registry_queue_init failed: {e:?}")))?;
1107 let qh = event_queue.handle();
1108
1109 let compositor_state = CompositorState::bind(&globals, &qh)
1110 .map_err(|e| DaemonError::StartError(format!("compositor bind failed: {e:?}")))?;
1111 let layer_shell = LayerShell::bind(&globals, &qh)
1112 .map_err(|e| DaemonError::StartError(format!("layer_shell bind failed: {e:?}")))?;
1113 let shm = Shm::bind(&globals, &qh)
1114 .map_err(|e| DaemonError::StartError(format!("shm bind failed: {e:?}")))?;
1115
1116 let compositor = globals
1118 .bind::<wl_compositor::WlCompositor, WaylandState, smithay_client_toolkit::globals::GlobalData>(
1119 &qh,
1120 1..=4,
1121 smithay_client_toolkit::globals::GlobalData,
1122 )
1123 .map_err(|e| DaemonError::StartError(format!("compositor bind failed: {e:?}")))?;
1124
1125 let mut wayland_state = WaylandState {
1126 registry_state: RegistryState::new(&globals),
1127 output_state: OutputState::new(&globals, &qh),
1128 compositor_state,
1129 shm,
1130 outputs: std::collections::HashMap::new(),
1131 surfaces: Vec::new(),
1132 layer_shell,
1133 compositor,
1134 hotplug: None,
1135 };
1136
1137 for i in 0..5 {
1141 event_queue
1142 .roundtrip(&mut wayland_state)
1143 .map_err(|e| DaemonError::StartError(format!("roundtrip {i} failed: {e:?}")))?;
1144 }
1145
1146 if wayland_state.outputs.is_empty() {
1147 return Err(DaemonError::StartError(
1148 "no outputs detected after roundtrip".into(),
1149 ));
1150 }
1151
1152 tracing::info!(
1153 "Detected {} output(s): {:?}",
1154 wayland_state.outputs.len(),
1155 wayland_state
1156 .outputs
1157 .values()
1158 .map(|o| format!("{} ({}x{})", o.name, o.width, o.height))
1159 .collect::<Vec<_>>()
1160 );
1161
1162 let renderer = std::sync::Arc::new(renderer);
1163
1164 let mut render_states_map: std::collections::HashMap<String, Arc<Mutex<RenderState>>> =
1168 std::collections::HashMap::new();
1169
1170 let output_info: Vec<(u32, OutputInfo)> = wayland_state
1173 .outputs
1174 .iter()
1175 .map(|(k, v)| {
1176 (
1177 *k,
1178 OutputInfo {
1179 name: v.name.clone(),
1180 width: v.width,
1181 height: v.height,
1182 scale_factor: v.scale_factor,
1183 wl_output: v.wl_output.clone(),
1184 },
1185 )
1186 })
1187 .collect();
1188
1189 for (proto_id, info) in &output_info {
1190 let name = info.name.clone();
1191 let rs = Self::create_render_state_for_output(
1192 &renderer,
1193 display_ptr,
1194 &mut wayland_state,
1195 &qh,
1196 info,
1197 &self.config,
1198 )
1199 .await?;
1200 let rs = Arc::new(Mutex::new(rs));
1201 render_states_map.insert(name.clone(), rs.clone());
1202
1203 let state_path = dirs::cache_dir()
1205 .unwrap_or_else(|| std::path::PathBuf::from("/tmp"))
1206 .join(format!("wallr/last_wallpaper/{name}"));
1207 if let Ok(path_str) = std::fs::read_to_string(&state_path) {
1208 let p = std::path::Path::new(path_str.trim());
1209 if p.exists() {
1210 let mut lock = rs.lock().await;
1211 let effect =
1212 crate::animation::Effect::Fade(crate::animation::FadeParams::default());
1213 let _ = lock.set_wallpaper(p, &effect, 0, 0).await;
1214 }
1215 }
1216
1217 tracing::info!("Output ready: {name} ({proto_id})");
1218 }
1219
1220 let render_states: std::sync::Arc<
1223 tokio::sync::Mutex<std::collections::HashMap<String, Arc<Mutex<RenderState>>>>,
1224 > = std::sync::Arc::new(tokio::sync::Mutex::new(render_states_map));
1225
1226 wayland_state.hotplug = Some(DaemonHotplug {
1229 renderer: renderer.clone(),
1230 config: self.config.clone(),
1231 display_ptr: SendDisplayPtr(display_ptr),
1232 render_states: render_states.clone(),
1233 });
1234
1235 event_queue
1238 .roundtrip(&mut wayland_state)
1239 .map_err(|e| DaemonError::StartError(format!("hotplug roundtrip failed: {e:?}")))?;
1240
1241 let paused_clone = self.paused.clone();
1242 let engine_clone = self.engine.clone();
1243 let render_states_clone = render_states.clone();
1244
1245 {
1249 let rs_map = render_states.clone();
1250 let socket_path = socket_path.clone();
1251 tokio::spawn(async move {
1252 use tokio::signal::unix::{SignalKind, signal};
1253 let mut term = signal(SignalKind::terminate()).expect("SIGTERM handler");
1254 let mut int = signal(SignalKind::interrupt()).expect("SIGINT handler");
1255 let mut hup = signal(SignalKind::hangup()).expect("SIGHUP handler");
1256 tokio::select! {
1257 _ = term.recv() => {}
1258 _ = int.recv() => {}
1259 _ = hup.recv() => {}
1260 }
1261 tracing::info!("Signal received, shutting down gracefully");
1262 let states = rs_map.lock().await;
1263 for rs in states.values() {
1264 if let Ok(state) = rs.try_lock() {
1265 state.video_playback.stop();
1266 }
1267 }
1268 drop(states);
1269 let _ = std::fs::remove_file(&socket_path);
1270 std::process::exit(0);
1271 });
1272 }
1273
1274 let ipc_socket_path = socket_path.clone();
1275 start_ipc_server(&socket_path, move |cmd| {
1276 let paused = paused_clone.clone();
1277 let engine = engine_clone.clone();
1278 let render_states = render_states_clone.clone();
1279 let stop_socket = ipc_socket_path.clone();
1280 async move {
1281 let render_states = render_states.lock().await;
1282 match cmd {
1283 IpcCommand::Pause { monitor } => {
1284 let targets = resolve_targets(&render_states, monitor.as_deref()).await;
1285 if targets.is_empty() {
1286 return IpcResponse {
1287 success: false,
1288 message: Some("No matching outputs".into()),
1289 };
1290 }
1291 if monitor.is_none() {
1292 paused.store(true, Ordering::SeqCst);
1293 }
1294 for rs in targets {
1295 let rs_lock = rs.lock().await;
1296 rs_lock.video_playback.pause();
1297 }
1298 IpcResponse {
1299 success: true,
1300 message: Some("Paused".into()),
1301 }
1302 }
1303 IpcCommand::Resume { monitor } => {
1304 let targets = resolve_targets(&render_states, monitor.as_deref()).await;
1305 if targets.is_empty() {
1306 return IpcResponse {
1307 success: false,
1308 message: Some("No matching outputs".into()),
1309 };
1310 }
1311 if monitor.is_none() {
1312 paused.store(false, Ordering::SeqCst);
1313 }
1314 for rs in targets {
1315 let rs_lock = rs.lock().await;
1316 rs_lock.video_playback.resume();
1317 }
1318 IpcResponse {
1319 success: true,
1320 message: Some("Resumed".into()),
1321 }
1322 }
1323 IpcCommand::Reload => {
1324 let lock = engine.lock().await;
1325 match lock.reload() {
1326 Ok(_) => IpcResponse {
1327 success: true,
1328 message: Some("Reloaded".into()),
1329 },
1330 Err(e) => IpcResponse {
1331 success: false,
1332 message: Some(e.to_string()),
1333 },
1334 }
1335 }
1336 IpcCommand::Preview {
1337 path,
1338 effect,
1339 duration_ms,
1340 no_theme,
1341 theme_override,
1342 monitor,
1343 scaling_mode,
1344 } => {
1345 if paused.load(Ordering::SeqCst) {
1346 return IpcResponse {
1347 success: false,
1348 message: Some("Daemon is paused".into()),
1349 };
1350 }
1351 let p = std::path::PathBuf::from(&path);
1352 if !p.exists() {
1353 return IpcResponse {
1354 success: false,
1355 message: Some(format!("File not found: {}", path)),
1356 };
1357 }
1358
1359 let targets = resolve_targets(&render_states, monitor.as_deref()).await;
1361 if targets.is_empty() {
1362 return IpcResponse {
1363 success: false,
1364 message: match &monitor {
1365 Some(name) => Some(format!("Unknown monitor: {name}")),
1366 None => Some("No outputs available".into()),
1367 },
1368 };
1369 }
1370
1371 {
1373 let persist_names: Vec<&str> = match &monitor {
1374 Some(name) => vec![name.as_str()],
1375 None => render_states.keys().map(|s| s.as_str()).collect(),
1376 };
1377 for name in persist_names {
1378 let state_path = dirs::cache_dir()
1379 .unwrap_or_else(|| std::path::PathBuf::from("/tmp"))
1380 .join(format!("wallr/last_wallpaper/{name}"));
1381 if let Some(parent) = state_path.parent() {
1382 let _ = std::fs::create_dir_all(parent);
1383 }
1384 let _ = std::fs::write(&state_path, &path);
1385 }
1386 }
1387
1388 let effect = effect.unwrap_or_else(|| {
1389 crate::animation::Effect::Fade(crate::animation::FadeParams::default())
1390 });
1391 let is_video = crate::video::VideoDecoder::is_video_file(&p);
1395 let duration = duration_ms.unwrap_or(if is_video { 150 } else { 2000 });
1396 let sm = scaling_mode.unwrap_or(crate::config::ScalingMode::Fill);
1397 let scaling_mode_u32 = match sm {
1398 crate::config::ScalingMode::Fill => 0u32,
1399 crate::config::ScalingMode::Fit => 1,
1400 crate::config::ScalingMode::Stretch => 2,
1401 crate::config::ScalingMode::Center => 3,
1402 crate::config::ScalingMode::Tile => 4,
1403 };
1404
1405 let mut last_err = None;
1406 for rs in &targets {
1407 let rs_clone = rs.clone();
1408 let p_clone = p.clone();
1409 let effect_clone = effect.clone();
1410 let result = tokio::task::spawn_blocking(move || {
1411 let rt = tokio::runtime::Handle::current();
1412 rt.block_on(async {
1413 let mut lock = rs_clone.lock().await;
1414 lock.set_wallpaper(
1415 &p_clone,
1416 &effect_clone,
1417 duration,
1418 scaling_mode_u32,
1419 )
1420 .await
1421 })
1422 })
1423 .await;
1424
1425 if let Err(e) = result {
1426 last_err = Some(format!("Task spawn failed: {e}"));
1427 continue;
1428 }
1429 if let Err(e) = result.unwrap() {
1430 last_err = Some(format!("Render failed: {e}"));
1431 }
1432 }
1433
1434 match last_err {
1435 Some(e) => IpcResponse {
1436 success: false,
1437 message: Some(e),
1438 },
1439 None => {
1440 let opts = SetOptions {
1441 no_theme,
1442 theme_provider: theme_override,
1443 monitor,
1444 };
1445 let mut eng = engine.lock().await;
1446 match eng.set_wallpaper(&p, &opts).await {
1447 Ok(()) => IpcResponse {
1448 success: true,
1449 message: None,
1450 },
1451 Err(e) => IpcResponse {
1452 success: true,
1453 message: Some(format!(
1454 "Wallpaper set, but hooks/theme failed: {e}"
1455 )),
1456 },
1457 }
1458 }
1459 }
1460 }
1461 IpcCommand::Stop => {
1462 let sp = stop_socket.clone();
1463 tokio::spawn(async move {
1464 tokio::time::sleep(std::time::Duration::from_millis(300)).await;
1465 let _ = std::fs::remove_file(&sp);
1466 std::process::exit(0);
1467 });
1468 IpcResponse {
1469 success: true,
1470 message: Some("Stopping".into()),
1471 }
1472 }
1473 IpcCommand::Status => {
1474 let state = if paused.load(Ordering::SeqCst) {
1475 "paused"
1476 } else {
1477 "running"
1478 };
1479 IpcResponse {
1480 success: true,
1481 message: Some(format!("wallr daemon {}", state)),
1482 }
1483 }
1484 IpcCommand::Seek {
1485 timestamp_ms,
1486 monitor,
1487 } => {
1488 let targets = resolve_targets(&render_states, monitor.as_deref()).await;
1489 if targets.is_empty() {
1490 return IpcResponse {
1491 success: false,
1492 message: match &monitor {
1493 Some(name) => Some(format!("Unknown monitor: {name}")),
1494 None => Some("No outputs available".into()),
1495 },
1496 };
1497 }
1498 let target_rs = targets.into_iter().next().unwrap();
1500 let rs_lock = target_rs.lock().await;
1501 match rs_lock
1502 .video_playback
1503 .seek(std::time::Duration::from_millis(timestamp_ms))
1504 {
1505 Ok(()) => IpcResponse {
1506 success: true,
1507 message: Some(format!("Seeked to {}ms", timestamp_ms)),
1508 },
1509 Err(e) => IpcResponse {
1510 success: false,
1511 message: Some(format!("Seek failed: {}", e)),
1512 },
1513 }
1514 }
1515 IpcCommand::Info { monitor } => {
1516 let targets = resolve_targets(&render_states, monitor.as_deref()).await;
1517 if targets.is_empty() {
1518 return IpcResponse {
1519 success: false,
1520 message: match &monitor {
1521 Some(name) => Some(format!("Unknown monitor: {name}")),
1522 None => Some("No outputs available".into()),
1523 },
1524 };
1525 }
1526
1527 let mut lines = vec![
1528 format!("wallr v{}", env!("CARGO_PKG_VERSION")),
1529 String::new(),
1530 format!("Outputs: {}", render_states.len()),
1531 ];
1532 for name in render_states.keys() {
1533 lines.push(format!(" - {name}"));
1534 }
1535
1536 for (name, rs) in render_states.iter() {
1538 if monitor.is_some() && monitor.as_deref() != Some(name.as_str()) {
1539 continue;
1540 }
1541 let rs_lock = rs.lock().await;
1542 let gpu_info =
1543 crate::video::gpu::adapter_diagnostics(&rs_lock.renderer.adapter);
1544
1545 lines.push(String::new());
1546 lines.push(format!("[{name}] {}x{}", rs_lock.width, rs_lock.height));
1547 lines.push(gpu_info);
1548
1549 match rs_lock.video_playback.metadata() {
1550 Some(meta) => {
1551 let decoder_info = rs_lock.video_playback.decoder_info();
1552 let hw = rs_lock.video_playback.hw_accel_in_use();
1553 let state = if rs_lock.video_playback.is_paused() {
1554 "paused"
1555 } else {
1556 "playing"
1557 };
1558 let position = rs_lock
1559 .video_playback
1560 .position()
1561 .map(|p| format!("{:.2}s", p.as_secs_f64()))
1562 .unwrap_or_else(|| "?".to_string());
1563 lines.push(String::new());
1564 lines.push("Video:".into());
1565 lines.push(format!(
1566 " Resolution: {}x{}",
1567 meta.width, meta.height
1568 ));
1569 lines.push(format!(" FPS: {:.2}", meta.fps));
1570 lines.push(format!(
1571 " Duration: {:.2}s",
1572 meta.duration.as_secs_f64()
1573 ));
1574 lines.push(format!(
1575 " Codec: {}",
1576 decoder_info
1577 .as_ref()
1578 .map(|d| d.codec_name.as_str())
1579 .unwrap_or("unknown")
1580 ));
1581 lines.push(format!(" Container: {}", meta.format));
1582 lines.push(format!(" Decoder: {}", hw.name()));
1583 lines.push(format!(
1584 " GPU Decode: {}",
1585 if hw == crate::video::HwAccel::Software {
1586 "disabled"
1587 } else {
1588 "enabled"
1589 }
1590 ));
1591 lines.push(format!(" State: {} @ {}", state, position));
1592 }
1593 None => {
1594 lines.push(String::new());
1595 lines.push("Video: none active".into());
1596 lines.push("Decoder: idle".into());
1597 }
1598 }
1599 }
1600
1601 IpcResponse {
1602 success: true,
1603 message: Some(lines.join("\n")),
1604 }
1605 }
1606 IpcCommand::MonitorList => {
1607 let mut lines = Vec::new();
1608 for (name, rs) in render_states.iter() {
1609 let lock = rs.lock().await;
1610 lines.push(format!("{}: {}x{}", name, lock.width, lock.height));
1611 }
1612 if lines.is_empty() {
1613 IpcResponse {
1614 success: true,
1615 message: Some("No monitors connected".into()),
1616 }
1617 } else {
1618 IpcResponse {
1619 success: true,
1620 message: Some(lines.join("\n")),
1621 }
1622 }
1623 }
1624 IpcCommand::MonitorCurrent => {
1625 if let Some((name, rs)) = render_states.iter().next() {
1627 let lock = rs.lock().await;
1628 IpcResponse {
1629 success: true,
1630 message: Some(format!("{}: {}x{}", name, lock.width, lock.height)),
1631 }
1632 } else {
1633 IpcResponse {
1634 success: false,
1635 message: Some("No monitors connected".into()),
1636 }
1637 }
1638 }
1639 IpcCommand::Blank { monitor } => {
1640 let targets = resolve_targets(&render_states, monitor.as_deref()).await;
1641 if targets.is_empty() {
1642 return IpcResponse {
1643 success: false,
1644 message: match &monitor {
1645 Some(name) => Some(format!("Unknown monitor: {name}")),
1646 None => Some("No outputs available".into()),
1647 },
1648 };
1649 }
1650 let mut blanked_count = 0u32;
1651 for (name, rs) in render_states.iter() {
1652 if monitor.as_deref() != Some(name.as_str()) && monitor.is_some() {
1653 continue;
1654 }
1655 let mut lock = rs.lock().await;
1656 if lock.blanked {
1657 continue;
1658 }
1659 lock.pre_blank = Some((
1661 lock.last_wallpaper.clone().unwrap_or_default(),
1662 lock.scaling_mode,
1663 ));
1664 lock.blanked = true;
1665 let black_effect = crate::animation::Effect::Fade(
1667 crate::animation::FadeParams::default(),
1668 );
1669 let tmp = std::env::temp_dir().join("wallr_blank.png");
1671 {
1672 let img =
1673 image::RgbaImage::from_pixel(1, 1, image::Rgba([0, 0, 0, 255]));
1674 let _ = img.save(&tmp);
1675 }
1676 let _ = lock.set_wallpaper(&tmp, &black_effect, 200, 0).await;
1677 blanked_count += 1;
1678 }
1679 IpcResponse {
1680 success: true,
1681 message: Some(format!("Blanked {blanked_count} output(s)")),
1682 }
1683 }
1684 IpcCommand::Restore { monitor } => {
1685 let targets = resolve_targets(&render_states, monitor.as_deref()).await;
1686 if targets.is_empty() {
1687 return IpcResponse {
1688 success: false,
1689 message: match &monitor {
1690 Some(name) => Some(format!("Unknown monitor: {name}")),
1691 None => Some("No outputs available".into()),
1692 },
1693 };
1694 }
1695 let mut restored_count = 0u32;
1696 for (name, rs) in render_states.iter() {
1697 if monitor.as_deref() != Some(name.as_str()) && monitor.is_some() {
1698 continue;
1699 }
1700 let mut lock = rs.lock().await;
1701 if !lock.blanked {
1702 continue;
1703 }
1704 lock.blanked = false;
1705 if let Some((ref path, scaling_mode)) = lock.pre_blank.clone() {
1706 let restore_effect = crate::animation::Effect::Fade(
1707 crate::animation::FadeParams::default(),
1708 );
1709 let _ = lock
1710 .set_wallpaper(
1711 std::path::Path::new(&path),
1712 &restore_effect,
1713 200,
1714 scaling_mode,
1715 )
1716 .await;
1717 }
1718 lock.pre_blank = None;
1719 restored_count += 1;
1720 }
1721 IpcResponse {
1722 success: true,
1723 message: Some(format!("Restored {restored_count} output(s)")),
1724 }
1725 }
1726 }
1727 }
1728 })
1729 .await?;
1730
1731 if self.config.watch.enabled
1733 && let Some(ref watch_dir) = self.config.watch.dir
1734 {
1735 let watch_path = crate::config::expand_path(watch_dir);
1736 self.start_watcher(watch_path, render_states.clone())
1737 .await?;
1738 }
1739
1740 tokio::task::spawn_blocking(move || {
1741 loop {
1742 if let Err(e) = event_queue.blocking_dispatch(&mut wayland_state) {
1743 eprintln!("Wayland dispatch error: {e:?}");
1744 break;
1745 }
1746 }
1747 eprintln!("wallr: Wayland connection lost, exiting");
1751 std::process::exit(1);
1752 });
1753
1754 loop {
1755 tokio::time::sleep(std::time::Duration::from_secs(3600)).await;
1756 }
1757 }
1758
1759 async fn start_watcher(
1760 &self,
1761 dir: PathBuf,
1762 render_states: std::sync::Arc<
1763 tokio::sync::Mutex<std::collections::HashMap<String, Arc<Mutex<RenderState>>>>,
1764 >,
1765 ) -> Result<(), DaemonError> {
1766 let engine = self.engine.clone();
1767 let paused = self.paused.clone();
1768 let debounce = crate::config::parse_duration(&self.config.watch.debounce)
1769 .unwrap_or(std::time::Duration::from_millis(500));
1770
1771 let (tx, mut rx) = tokio::sync::mpsc::channel(100);
1772
1773 let mut watcher = notify::recommended_watcher(move |res: Result<Event, notify::Error>| {
1774 if let Ok(event) = res
1775 && let EventKind::Create(_) = event.kind
1776 {
1777 for path in event.paths {
1778 let _ = tx.blocking_send(path);
1779 }
1780 }
1781 })
1782 .map_err(|e| DaemonError::StartError(e.to_string()))?;
1783
1784 watcher
1785 .watch(&dir, RecursiveMode::NonRecursive)
1786 .map_err(|e| DaemonError::StartError(e.to_string()))?;
1787
1788 tokio::spawn(async move {
1789 let _watcher = watcher;
1790 let mut last: Option<(PathBuf, std::time::Instant)> = None;
1791
1792 while let Some(path) = rx.recv().await {
1793 if paused.load(Ordering::SeqCst) {
1794 continue;
1795 }
1796 if let Some((ref lp, ref lt)) = last
1797 && lp == &path
1798 && lt.elapsed() < debounce
1799 {
1800 continue;
1801 }
1802 let ext = path
1803 .extension()
1804 .unwrap_or_default()
1805 .to_string_lossy()
1806 .to_lowercase();
1807 if !["jpg", "jpeg", "png", "gif", "webp"].contains(&ext.as_str()) {
1808 continue;
1809 }
1810 last = Some((path.clone(), std::time::Instant::now()));
1811
1812 let states = render_states.lock().await;
1814 for (name, rs) in states.iter() {
1815 let rs = rs.clone();
1816 let eng = engine.clone();
1817 let p = path.clone();
1818 let name = name.clone();
1819 tokio::spawn(async move {
1820 let mut lock = rs.lock().await;
1821 let effect =
1822 crate::animation::Effect::Fade(crate::animation::FadeParams::default());
1823 let _ = lock.set_wallpaper(&p, &effect, 600, 0).await;
1824 drop(lock);
1825 let opts = SetOptions {
1826 no_theme: false,
1827 theme_provider: None,
1828 monitor: Some(name),
1829 };
1830 let mut elock = eng.lock().await;
1831 let _ = elock.set_wallpaper(&p, &opts).await;
1832 });
1833 }
1834 }
1835 });
1836
1837 Ok(())
1838 }
1839
1840 #[allow(clippy::too_many_arguments)]
1843 async fn create_render_state_for_output(
1844 renderer: &std::sync::Arc<Renderer>,
1845 display_ptr: *mut std::ffi::c_void,
1846 wayland_state: &mut WaylandState,
1847 qh: &QueueHandle<WaylandState>,
1848 output: &OutputInfo,
1849 config: &WallrConfig,
1850 ) -> Result<RenderState, DaemonError> {
1851 let wl_surface = wayland_state.compositor_state.create_surface(qh);
1852 let layer_surface = wayland_state.layer_shell.create_layer_surface(
1853 qh,
1854 wl_surface,
1855 Layer::Background,
1856 Some("wallr"),
1857 Some(&output.wl_output),
1858 );
1859 layer_surface.set_anchor(Anchor::TOP | Anchor::BOTTOM | Anchor::LEFT | Anchor::RIGHT);
1860 layer_surface.set_exclusive_zone(-1);
1861 layer_surface.set_keyboard_interactivity(KeyboardInteractivity::None);
1862
1863 let empty_region = wayland_state.compositor.create_region(qh, ());
1865 layer_surface
1866 .wl_surface()
1867 .set_input_region(Some(&empty_region));
1868 layer_surface.commit();
1869 empty_region.destroy();
1870
1871 let scale_factor = if output.scale_factor > 0 {
1872 output.scale_factor
1873 } else {
1874 1
1875 };
1876 layer_surface.wl_surface().set_buffer_scale(scale_factor);
1877
1878 let width = output.width * scale_factor as u32;
1879 let height = output.height * scale_factor as u32;
1880
1881 let raw_surface = layer_surface.wl_surface().id().as_ptr() as *mut std::ffi::c_void;
1882 wayland_state
1883 .surfaces
1884 .push((output.wl_output.id().protocol_id(), layer_surface));
1885
1886 let window_handle = WaylandWindow {
1887 display: display_ptr,
1888 surface: raw_surface,
1889 };
1890
1891 let wgpu_surface = renderer
1892 .instance
1893 .create_surface(&window_handle)
1894 .map_err(|e| DaemonError::StartError(format!("wgpu surface creation failed: {e:?}")))?;
1895
1896 let adapter = renderer
1897 .instance
1898 .request_adapter(&wgpu::RequestAdapterOptions {
1899 compatible_surface: Some(&wgpu_surface),
1900 power_preference: wgpu::PowerPreference::HighPerformance,
1901 force_fallback_adapter: false,
1902 })
1903 .await;
1904 let surf_format = adapter
1905 .as_ref()
1906 .map(|a| {
1907 let caps = wgpu_surface.get_capabilities(a);
1908 caps.formats
1909 .into_iter()
1910 .next()
1911 .unwrap_or(wgpu::TextureFormat::Bgra8UnormSrgb)
1912 })
1913 .unwrap_or(wgpu::TextureFormat::Bgra8UnormSrgb);
1914
1915 let surf_config = wgpu::SurfaceConfiguration {
1916 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
1917 format: surf_format,
1918 width,
1919 height,
1920 present_mode: wgpu::PresentMode::Fifo,
1921 alpha_mode: wgpu::CompositeAlphaMode::Opaque,
1922 view_formats: vec![],
1923 desired_maximum_frame_latency: 2,
1924 };
1925 wgpu_surface.configure(&renderer.device, &surf_config);
1926
1927 let wgpu_surface: wgpu::Surface<'static> = unsafe { std::mem::transmute(wgpu_surface) };
1930 let surface: &'static wgpu::Surface<'static> = Box::leak(Box::new(wgpu_surface));
1931
1932 Ok(RenderState {
1933 renderer: renderer.clone(),
1934 surface,
1935 render_lock: std::sync::Arc::new(std::sync::Mutex::new(())),
1936 playback_gen: std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0)),
1937 pacer: std::sync::Arc::new(LivePacer::new()),
1938 current_bind: None,
1939 current_tex: None,
1940 width,
1941 height,
1942 current_width: 0,
1943 current_height: 0,
1944 format: surf_format,
1945 video_playback: std::sync::Arc::new(crate::video::VideoPlayback::new()),
1946 hw_accel: crate::video::HwAccel::from_config(&config.video.hw_decode),
1947 scaling_mode: 0,
1948 per_output_uniforms: std::sync::Arc::new(renderer.create_per_output_uniforms()),
1949 last_wallpaper: None,
1950 pre_blank: None,
1951 blanked: false,
1952 })
1953 }
1954}
1955
1956#[allow(clippy::too_many_arguments)]
1960fn create_render_state_for_output_sync(
1961 renderer: &std::sync::Arc<Renderer>,
1962 display_ptr: *mut std::ffi::c_void,
1963 wayland_state: &mut WaylandState,
1964 qh: &QueueHandle<WaylandState>,
1965 output: &OutputInfo,
1966 config: &WallrConfig,
1967) -> Result<RenderState, DaemonError> {
1968 let wl_surface = wayland_state.compositor_state.create_surface(qh);
1969 let layer_surface = wayland_state.layer_shell.create_layer_surface(
1970 qh,
1971 wl_surface,
1972 Layer::Background,
1973 Some("wallr"),
1974 Some(&output.wl_output),
1975 );
1976 layer_surface.set_anchor(Anchor::TOP | Anchor::BOTTOM | Anchor::LEFT | Anchor::RIGHT);
1977 layer_surface.set_exclusive_zone(-1);
1978 layer_surface.set_keyboard_interactivity(KeyboardInteractivity::None);
1979
1980 let empty_region = wayland_state.compositor.create_region(qh, ());
1981 layer_surface
1982 .wl_surface()
1983 .set_input_region(Some(&empty_region));
1984 layer_surface.commit();
1985 empty_region.destroy();
1986
1987 let scale_factor = if output.scale_factor > 0 {
1988 output.scale_factor
1989 } else {
1990 1
1991 };
1992 layer_surface.wl_surface().set_buffer_scale(scale_factor);
1993
1994 let width = output.width * scale_factor as u32;
1995 let height = output.height * scale_factor as u32;
1996
1997 let raw_surface = layer_surface.wl_surface().id().as_ptr() as *mut std::ffi::c_void;
1998 wayland_state
1999 .surfaces
2000 .push((output.wl_output.id().protocol_id(), layer_surface));
2001
2002 let window_handle = WaylandWindow {
2003 display: display_ptr,
2004 surface: raw_surface,
2005 };
2006
2007 let wgpu_surface = renderer
2008 .instance
2009 .create_surface(&window_handle)
2010 .map_err(|e| DaemonError::StartError(format!("wgpu surface creation failed: {e:?}")))?;
2011
2012 let surf_format = {
2013 let caps = wgpu_surface.get_capabilities(&renderer.adapter);
2014 caps.formats
2015 .into_iter()
2016 .next()
2017 .unwrap_or(wgpu::TextureFormat::Bgra8UnormSrgb)
2018 };
2019
2020 let surf_config = wgpu::SurfaceConfiguration {
2021 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
2022 format: surf_format,
2023 width,
2024 height,
2025 present_mode: wgpu::PresentMode::Fifo,
2026 alpha_mode: wgpu::CompositeAlphaMode::Opaque,
2027 view_formats: vec![],
2028 desired_maximum_frame_latency: 2,
2029 };
2030 wgpu_surface.configure(&renderer.device, &surf_config);
2031
2032 let wgpu_surface: wgpu::Surface<'static> = unsafe { std::mem::transmute(wgpu_surface) };
2033 let surface: &'static wgpu::Surface<'static> = Box::leak(Box::new(wgpu_surface));
2034
2035 Ok(RenderState {
2036 renderer: renderer.clone(),
2037 surface,
2038 render_lock: std::sync::Arc::new(std::sync::Mutex::new(())),
2039 playback_gen: std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0)),
2040 pacer: std::sync::Arc::new(LivePacer::new()),
2041 current_bind: None,
2042 current_tex: None,
2043 width,
2044 height,
2045 current_width: 0,
2046 current_height: 0,
2047 format: surf_format,
2048 video_playback: std::sync::Arc::new(crate::video::VideoPlayback::new()),
2049 hw_accel: crate::video::HwAccel::from_config(&config.video.hw_decode),
2050 scaling_mode: 0,
2051 per_output_uniforms: std::sync::Arc::new(renderer.create_per_output_uniforms()),
2052 last_wallpaper: None,
2053 pre_blank: None,
2054 blanked: false,
2055 })
2056}