gardesk/garbg / 6b29169

Browse files

Default to per-monitor wallpapers, add --span flag

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
6b291697fcea577f413c6d527377f342a1aa76ed
Parents
aff0b13
Tree
51d5d37

3 changed files

StatusFile+-
M garbg/src/daemon/state.rs 48 20
M garbg/src/ipc/protocol.rs 3 0
M garbg/src/main.rs 42 13
garbg/src/daemon/state.rsmodified
@@ -578,7 +578,7 @@ impl Daemon {
578578
     /// Handle an IPC command
579579
     fn handle_command(&mut self, cmd: Command) -> Response {
580580
         match cmd {
581
-            Command::Set { source, mode, monitor: _, interval_secs, shuffle, animate, max_fps } => {
581
+            Command::Set { source, mode, monitor: _, interval_secs, shuffle, animate, max_fps, span } => {
582582
                 let scale_mode = mode.unwrap_or(self.state.config.general.mode);
583583
 
584584
                 // Stop any existing animation first
@@ -629,7 +629,7 @@ impl Daemon {
629629
                 }
630630
 
631631
                 // Set up slideshow with the new source (static image)
632
-                match self.set_wallpaper_with_options(&source, scale_mode, shuffle, interval_secs) {
632
+                match self.set_wallpaper_with_options(&source, scale_mode, shuffle, interval_secs, span) {
633633
                     Ok(_) => {
634634
                         // Update slideshow interval
635635
                         self.state.slideshow_interval = interval_secs.map(Duration::from_secs);
@@ -645,9 +645,9 @@ impl Daemon {
645645
             }
646646
             Command::SetWorkspace { workspace, source, mode } => {
647647
                 let scale_mode = mode.unwrap_or(self.state.config.general.mode);
648
-                // Only set if we're on this workspace
648
+                // Only set if we're on this workspace (default to per-monitor)
649649
                 if self.state.current_workspace == workspace {
650
-                    match self.set_wallpaper_from_source(&source, scale_mode, false) {
650
+                    match self.set_wallpaper_from_source(&source, scale_mode, false, false) {
651651
                         Ok(_) => Response::ok(),
652652
                         Err(e) => Response::error(e.to_string()),
653653
                     }
@@ -853,7 +853,8 @@ impl Daemon {
853853
             return Ok(());
854854
         }
855855
 
856
-        self.set_wallpaper_from_source(&source, mode, shuffle)
856
+        // Default to per-monitor (span = false)
857
+        self.set_wallpaper_from_source(&source, mode, shuffle, false)
857858
     }
858859
 
859860
     /// Start an animated image playback (GIF, WebP, APNG, or video)
@@ -1084,12 +1085,13 @@ impl Daemon {
10841085
         mode: ScaleMode,
10851086
         shuffle: bool,
10861087
         _interval_secs: Option<u64>,
1088
+        span: bool,
10871089
     ) -> Result<()> {
1088
-        self.set_wallpaper_from_source(source, mode, shuffle)
1090
+        self.set_wallpaper_from_source(source, mode, shuffle, span)
10891091
     }
10901092
 
10911093
     /// Set wallpaper from a source (file, directory, or URL)
1092
-    fn set_wallpaper_from_source(&mut self, source: &str, mode: ScaleMode, shuffle: bool) -> Result<()> {
1094
+    fn set_wallpaper_from_source(&mut self, source: &str, mode: ScaleMode, shuffle: bool, span: bool) -> Result<()> {
10931095
         // Expand path
10941096
         let expanded = shellexpand::tilde(source);
10951097
         let path = std::path::Path::new(expanded.as_ref());
@@ -1118,7 +1120,7 @@ impl Daemon {
11181120
             playlist.save()?;
11191121
             self.state.playlist = Some(playlist);
11201122
 
1121
-            self.set_wallpaper(&first, mode)?;
1123
+            self.set_wallpaper_with_span(&first, mode, span)?;
11221124
 
11231125
             tracing::info!(
11241126
                 "Playlist loaded: {} images{}",
@@ -1128,14 +1130,10 @@ impl Daemon {
11281130
         } else if source.starts_with("http://") || source.starts_with("https://") {
11291131
             // Remote URL
11301132
             let image = self.fetch_image(source)?;
1131
-            let conn = self.conn_mut()?;
1132
-            let (width, height) = conn.screen_dimensions();
1133
-            let scaled = scale_image(&image, width as u32, height as u32, mode);
1134
-            conn.set_wallpaper(&scaled)?;
1135
-            tracing::info!("Wallpaper set: {} (mode: {})", source, mode);
1133
+            self.set_image_with_span(&image, source, mode, span)?;
11361134
         } else {
11371135
             // Single file
1138
-            self.set_wallpaper(source, mode)?;
1136
+            self.set_wallpaper_with_span(source, mode, span)?;
11391137
         }
11401138
 
11411139
         Ok(())
@@ -1143,14 +1141,43 @@ impl Daemon {
11431141
 
11441142
     /// Set wallpaper from a local file
11451143
     pub fn set_wallpaper(&mut self, source: &str, mode: ScaleMode) -> Result<()> {
1144
+        self.set_wallpaper_with_span(source, mode, false)
1145
+    }
1146
+
1147
+    /// Set wallpaper from a local file with span option
1148
+    pub fn set_wallpaper_with_span(&mut self, source: &str, mode: ScaleMode, span: bool) -> Result<()> {
11461149
         let expanded = shellexpand::tilde(source);
11471150
         let image = ImageLoader::load_file(expanded.as_ref())?;
1148
-        let conn = self.conn_mut()?;
1149
-        let (width, height) = conn.screen_dimensions();
1150
-        let scaled = scale_image(&image, width as u32, height as u32, mode);
1151
-        conn.set_wallpaper(&scaled)?;
1151
+        self.set_image_with_span(&image, source, mode, span)
1152
+    }
1153
+
1154
+    /// Set wallpaper from an image with span option
1155
+    fn set_image_with_span(&mut self, image: &image::RgbaImage, source: &str, mode: ScaleMode, span: bool) -> Result<()> {
1156
+        let conn = self.conn()?;
1157
+        let monitors = Monitor::get_all(conn).unwrap_or_default();
11521158
 
1153
-        tracing::info!("Wallpaper set: {} (mode: {})", source, mode);
1159
+        if !span && monitors.len() > 1 {
1160
+            // Per-monitor mode: scale wallpaper to each monitor individually
1161
+            let compositor = Compositor::new(&monitors);
1162
+            let wallpapers = Compositor::create_wallpapers_uniform(&monitors, image, mode);
1163
+            let composited = compositor.composite(&wallpapers);
1164
+            self.conn_mut()?.set_wallpaper(&composited)?;
1165
+
1166
+            tracing::info!(
1167
+                "Wallpaper set on {} monitors: {} (mode: {})",
1168
+                monitors.len(),
1169
+                source,
1170
+                mode
1171
+            );
1172
+        } else {
1173
+            // Span mode or single monitor: scale to full screen
1174
+            let conn = self.conn_mut()?;
1175
+            let (width, height) = conn.screen_dimensions();
1176
+            let scaled = scale_image(image, width as u32, height as u32, mode);
1177
+            conn.set_wallpaper(&scaled)?;
1178
+
1179
+            tracing::info!("Wallpaper set: {} (mode: {})", source, mode);
1180
+        }
11541181
 
11551182
         Ok(())
11561183
     }
@@ -1262,7 +1289,8 @@ impl Daemon {
12621289
 
12631290
         if let Some(config) = ws_config {
12641291
             let mode = config.mode.unwrap_or(self.state.config.general.mode);
1265
-            self.set_wallpaper_from_source(&config.source, mode, false)?;
1292
+            // Default to per-monitor (span = false)
1293
+            self.set_wallpaper_from_source(&config.source, mode, false, false)?;
12661294
         }
12671295
 
12681296
         Ok(())
garbg/src/ipc/protocol.rsmodified
@@ -32,6 +32,9 @@ pub enum Command {
3232
         /// Max FPS for animations (default: 60)
3333
         #[serde(default = "default_max_fps")]
3434
         max_fps: u32,
35
+        /// Span wallpaper across all monitors (default: false, per-monitor)
36
+        #[serde(default)]
37
+        span: bool,
3538
     },
3639
 
3740
     /// Set wallpaper for a specific workspace
garbg/src/main.rsmodified
@@ -53,6 +53,10 @@ enum Commands {
5353
         /// Max FPS for animations (default: 60)
5454
         #[arg(long, default_value = "60")]
5555
         max_fps: u32,
56
+
57
+        /// Span wallpaper across all monitors (default: per-monitor)
58
+        #[arg(long)]
59
+        span: bool,
5660
     },
5761
 
5862
     /// Advance to the next image in the playlist
@@ -142,8 +146,8 @@ fn main() -> Result<()> {
142146
         .init();
143147
 
144148
     match cli.command {
145
-        Commands::Set { source, mode, monitor, random, interval, animate, max_fps } => {
146
-            set_wallpaper(&source, &mode, monitor.as_deref(), random, interval, animate, max_fps)?;
149
+        Commands::Set { source, mode, monitor, random, interval, animate, max_fps, span } => {
150
+            set_wallpaper(&source, &mode, monitor.as_deref(), random, interval, animate, max_fps, span)?;
147151
         }
148152
         Commands::Next => {
149153
             cmd_next()?;
@@ -220,6 +224,7 @@ fn set_wallpaper(
220224
     interval: Option<Duration>,
221225
     animate: bool,
222226
     max_fps: u32,
227
+    span: bool,
223228
 ) -> Result<()> {
224229
     use garbg::config::ScaleMode;
225230
     use garbg::ipc::send_command_blocking;
@@ -240,6 +245,7 @@ fn set_wallpaper(
240245
         shuffle: random,
241246
         animate,
242247
         max_fps,
248
+        span,
243249
     };
244250
 
245251
     // Try to send to daemon (should be ready if systemd started it correctly)
@@ -316,7 +322,7 @@ fn set_wallpaper(
316322
     }
317323
 
318324
     // Set the initial wallpaper (handles animated GIFs automatically)
319
-    set_single_wallpaper(&resolved_source, scale_mode, animate, max_fps)?;
325
+    set_single_wallpaper(&resolved_source, scale_mode, animate, max_fps, span)?;
320326
 
321327
     // If interval specified and daemon not running, enter foreground rotation loop
322328
     // Note: --interval mode doesn't support animations (would require stopping animation to rotate)
@@ -338,7 +344,7 @@ fn set_wallpaper(
338344
                 playlist_state.save()?;
339345
 
340346
                 // Static mode for rotation (animation would conflict)
341
-                set_single_wallpaper(&next_img, playlist_state.mode, false, 60)?;
347
+                set_single_wallpaper(&next_img, playlist_state.mode, false, 60, span)?;
342348
                 tracing::info!(
343349
                     "Rotated to [{}/{}]: {}",
344350
                     playlist_state.current_index + 1,
@@ -358,15 +364,19 @@ fn set_wallpaper(
358364
 ///
359365
 /// If `animate` is true and the source is an animated GIF, this will block
360366
 /// and play the animation until interrupted (Ctrl+C).
367
+///
368
+/// If `span` is true, the wallpaper is stretched across all monitors.
369
+/// If `span` is false (default), the wallpaper is scaled to each monitor individually.
361370
 fn set_single_wallpaper(
362371
     source: &str,
363372
     mode: garbg::config::ScaleMode,
364373
     animate: bool,
365374
     max_fps: u32,
375
+    span: bool,
366376
 ) -> Result<()> {
367377
     use garbg::daemon::{AnimationConfig, AnimationLoop};
368378
     use garbg::media::{AnimatedGif, ImageLoader};
369
-    use garbg::x11::Connection;
379
+    use garbg::x11::{Connection, Compositor, Monitor};
370380
 
371381
     tracing::info!("Setting wallpaper: {}", source);
372382
 
@@ -453,11 +463,30 @@ fn set_single_wallpaper(
453463
         ImageLoader::load_file(source)?
454464
     };
455465
 
456
-    let (width, height) = conn.screen_dimensions();
457
-    let scaled = garbg::media::scale_image(&image, width as u32, height as u32, mode);
458
-    conn.set_wallpaper(&scaled)?;
466
+    // Check for multiple monitors
467
+    let monitors = Monitor::get_all(&conn).unwrap_or_default();
468
+
469
+    if !span && monitors.len() > 1 {
470
+        // Per-monitor mode: scale wallpaper to each monitor individually
471
+        let compositor = Compositor::new(&monitors);
472
+        let wallpapers = Compositor::create_wallpapers_uniform(&monitors, &image, mode);
473
+        let composited = compositor.composite(&wallpapers);
474
+        conn.set_wallpaper(&composited)?;
475
+
476
+        tracing::info!(
477
+            "Wallpaper set on {} monitors: {} (mode: {:?})",
478
+            monitors.len(),
479
+            source,
480
+            mode
481
+        );
482
+    } else {
483
+        // Span mode or single monitor: scale to full screen
484
+        let (width, height) = conn.screen_dimensions();
485
+        let scaled = garbg::media::scale_image(&image, width as u32, height as u32, mode);
486
+        conn.set_wallpaper(&scaled)?;
459487
 
460
-    tracing::info!("Wallpaper set: {} ({}x{}, mode: {:?})", source, width, height, mode);
488
+        tracing::info!("Wallpaper set: {} ({}x{}, mode: {:?})", source, width, height, mode);
489
+    }
461490
 
462491
     Ok(())
463492
 }
@@ -484,8 +513,8 @@ fn cmd_next() -> Result<()> {
484513
     let next_image = state.next().to_string();
485514
     state.save()?;
486515
 
487
-    // No animation for quick navigation commands
488
-    set_single_wallpaper(&next_image, state.mode, false, 60)?;
516
+    // No animation for quick navigation commands, default to per-monitor
517
+    set_single_wallpaper(&next_image, state.mode, false, 60, false)?;
489518
 
490519
     tracing::info!(
491520
         "Next [{}/{}]: {}",
@@ -519,8 +548,8 @@ fn cmd_prev() -> Result<()> {
519548
     let prev_image = state.prev().to_string();
520549
     state.save()?;
521550
 
522
-    // No animation for quick navigation commands
523
-    set_single_wallpaper(&prev_image, state.mode, false, 60)?;
551
+    // No animation for quick navigation commands, default to per-monitor
552
+    set_single_wallpaper(&prev_image, state.mode, false, 60, false)?;
524553
 
525554
     tracing::info!(
526555
         "Prev [{}/{}]: {}",