gardesk/gar / 0753cf4

Browse files

Add dynamic picom config generation from Lua settings

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
0753cf4c1dba6f00027def72b70cf477a4f3dac1
Parents
77eff09
Tree
d6fca09

9 changed files

StatusFile+-
M gar-session.sh 12 2
M gar/config/default.lua 17 0
A gar/config/picom.conf 140 0
M gar/src/config/lua.rs 68 0
M gar/src/config/mod.rs 252 0
M gar/src/core/mod.rs 11 0
M gar/src/x11/connection.rs 15 0
M gar/src/x11/events.rs 5 0
M start-gar.sh 10 3
gar-session.shmodified
@@ -1,16 +1,26 @@
11
 #!/bin/bash
22
 # gar session wrapper - sets up environment before starting gar
33
 
4
+GAR_DIR="$(cd "$(dirname "$0")" && pwd)"
5
+
46
 # Optional: configure monitor layout
57
 # Uncomment and customize for your setup:
68
 # xrandr --output eDP-1 --mode 2880x1800 --pos 0x0
79
 # xrandr --output DP-1 --mode 1920x1080 --pos 0x0 \
810
 #   --output HDMI-1 --mode 2560x1440 --pos 1920x0
911
 
12
+# Ensure gar config directory exists
13
+mkdir -p ~/.config/gar
14
+
1015
 # Launch compositor before WM (for proper screen repainting)
11
-# --backend glx is required for picom v12+ (no longer has a default)
16
+# gar generates picom.conf on startup and signals picom to reload
1217
 if command -v picom &> /dev/null; then
13
-    picom -b --backend glx --use-ewmh-active-win &
18
+    if [[ -f ~/.config/gar/picom.conf ]]; then
19
+        picom -b --config ~/.config/gar/picom.conf &
20
+    else
21
+        # First run: start with GLX backend, gar will generate config and signal reload
22
+        picom -b --backend glx &
23
+    fi
1424
     sleep 0.1
1525
 fi
1626
 
gar/config/default.luamodified
@@ -18,6 +18,23 @@ gar.set("border_color_unfocused", "#2d2d2d")
1818
 gar.set("gap_inner", 8)
1919
 gar.set("gap_outer", 8)
2020
 
21
+-- Visual Effects (picom compositor)
22
+-- gar auto-generates ~/.config/gar/picom.conf from these settings
23
+-- Changes take effect on reload (Mod+Shift+R) - picom is signaled automatically
24
+-- gar.set("corner_radius", 12)              -- 0 = square corners
25
+-- gar.set("blur_enabled", true)
26
+-- gar.set("blur_method", "dual_kawase")     -- "gaussian", "dual_kawase", "box"
27
+-- gar.set("blur_strength", 5)               -- 1-20 for dual_kawase
28
+-- gar.set("shadow_enabled", true)
29
+-- gar.set("shadow_radius", 12)
30
+-- gar.set("shadow_opacity", 0.75)
31
+-- gar.set("shadow_offset_x", -7)
32
+-- gar.set("shadow_offset_y", -7)
33
+-- gar.set("opacity_focused", 1.0)
34
+-- gar.set("opacity_unfocused", 0.9)         -- Dim unfocused windows
35
+-- gar.set("fade_enabled", true)
36
+-- gar.set("fade_delta", 10)
37
+
2138
 -- Behavior
2239
 gar.set("follow_window_on_move", true)  -- Follow window when using Mod+Shift+number
2340
 
gar/config/picom.confadded
@@ -0,0 +1,140 @@
1
+# picom.conf - Compositor configuration for gar window manager
2
+# Copy to ~/.config/gar/picom.conf and customize
3
+#
4
+# This config enables modern visual effects similar to Hyprland:
5
+# - Rounded corners
6
+# - Background blur (dual_kawase)
7
+# - Window shadows
8
+# - Fade animations
9
+#
10
+# Requires picom v12+ with GLX backend
11
+
12
+# =============================================================================
13
+# Backend Configuration
14
+# =============================================================================
15
+backend = "glx";
16
+vsync = true;
17
+
18
+# Use EWMH _NET_ACTIVE_WINDOW for focus detection (required for gar)
19
+use-ewmh-active-win = true;
20
+
21
+# GLX backend optimizations
22
+glx-no-stencil = true;
23
+glx-no-rebind-pixmap = true;
24
+
25
+# =============================================================================
26
+# Rounded Corners
27
+# =============================================================================
28
+corner-radius = 12;
29
+
30
+rounded-corners-exclude = [
31
+    "window_type = 'dock'",
32
+    "window_type = 'desktop'",
33
+    "window_type = 'tooltip'",
34
+    "window_type = 'menu'",
35
+    "window_type = 'dropdown_menu'",
36
+    "window_type = 'popup_menu'",
37
+    # Exclude fullscreen windows
38
+    "_NET_WM_STATE@:32a *= '_NET_WM_STATE_FULLSCREEN'"
39
+];
40
+
41
+# =============================================================================
42
+# Blur
43
+# =============================================================================
44
+blur-method = "dual_kawase";
45
+blur-strength = 5;
46
+blur-background = true;
47
+blur-background-frame = false;
48
+blur-kern = "3x3box";
49
+
50
+blur-background-exclude = [
51
+    "window_type = 'dock'",
52
+    "window_type = 'desktop'",
53
+    "window_type = 'menu'",
54
+    "window_type = 'dropdown_menu'",
55
+    "window_type = 'popup_menu'",
56
+    # Windows requesting compositor bypass (fullscreen games/video)
57
+    "_NET_WM_BYPASS_COMPOSITOR@:32c = 1"
58
+];
59
+
60
+# =============================================================================
61
+# Shadows
62
+# =============================================================================
63
+shadow = true;
64
+shadow-radius = 12;
65
+shadow-opacity = 0.75;
66
+shadow-offset-x = -7;
67
+shadow-offset-y = -7;
68
+
69
+shadow-exclude = [
70
+    "window_type = 'dock'",
71
+    "window_type = 'desktop'",
72
+    "window_type = 'menu'",
73
+    "window_type = 'dropdown_menu'",
74
+    "window_type = 'popup_menu'",
75
+    "window_type = 'tooltip'",
76
+    # Exclude fullscreen windows
77
+    "_NET_WM_STATE@:32a *= '_NET_WM_STATE_FULLSCREEN'",
78
+    "_NET_WM_BYPASS_COMPOSITOR@:32c = 1"
79
+];
80
+
81
+# =============================================================================
82
+# Fading / Animations
83
+# =============================================================================
84
+fading = true;
85
+fade-in-step = 0.028;
86
+fade-out-step = 0.03;
87
+fade-delta = 10;
88
+
89
+# Don't fade destroyed windows (can cause visual glitches)
90
+no-fading-destroyed-argb = true;
91
+
92
+fade-exclude = [
93
+    "window_type = 'menu'",
94
+    "window_type = 'dropdown_menu'",
95
+    "window_type = 'popup_menu'"
96
+];
97
+
98
+# =============================================================================
99
+# Focus Opacity (Optional)
100
+# =============================================================================
101
+# Uncomment to dim unfocused windows
102
+# inactive-opacity = 0.9;
103
+# active-opacity = 1.0;
104
+# frame-opacity = 1.0;
105
+
106
+# =============================================================================
107
+# Window Type Settings
108
+# =============================================================================
109
+wintypes:
110
+{
111
+    tooltip = {
112
+        fade = true;
113
+        shadow = false;
114
+        opacity = 0.95;
115
+        focus = true;
116
+        blur-background = false;
117
+    };
118
+    dock = {
119
+        shadow = false;
120
+        clip-shadow-above = true;
121
+    };
122
+    dnd = {
123
+        shadow = false;
124
+    };
125
+    popup_menu = {
126
+        opacity = 0.95;
127
+        shadow = false;
128
+    };
129
+    dropdown_menu = {
130
+        opacity = 0.95;
131
+        shadow = false;
132
+    };
133
+};
134
+
135
+# =============================================================================
136
+# Experimental / Debug
137
+# =============================================================================
138
+# Useful for debugging rendering issues
139
+# log-level = "warn";
140
+# log-file = "/tmp/picom.log";
gar/src/config/lua.rsmodified
@@ -294,6 +294,74 @@ impl LuaConfig {
294294
                         state.config.bar_height = v as u32;
295295
                     }
296296
                 }
297
+                // Compositor visual settings (picom)
298
+                "corner_radius" => {
299
+                    if let Value::Integer(v) = value {
300
+                        state.config.corner_radius = v as u32;
301
+                    }
302
+                }
303
+                "blur_enabled" => {
304
+                    if let Value::Boolean(v) = value {
305
+                        state.config.blur_enabled = v;
306
+                    }
307
+                }
308
+                "blur_method" => {
309
+                    if let Value::String(s) = value {
310
+                        if let Ok(str_val) = s.to_str() {
311
+                            state.config.blur_method = str_val.to_string();
312
+                        }
313
+                    }
314
+                }
315
+                "blur_strength" => {
316
+                    if let Value::Integer(v) = value {
317
+                        state.config.blur_strength = v as u32;
318
+                    }
319
+                }
320
+                "shadow_enabled" => {
321
+                    if let Value::Boolean(v) = value {
322
+                        state.config.shadow_enabled = v;
323
+                    }
324
+                }
325
+                "shadow_radius" => {
326
+                    if let Value::Integer(v) = value {
327
+                        state.config.shadow_radius = v as u32;
328
+                    }
329
+                }
330
+                "shadow_opacity" => {
331
+                    if let Value::Number(v) = value {
332
+                        state.config.shadow_opacity = v;
333
+                    }
334
+                }
335
+                "shadow_offset_x" => {
336
+                    if let Value::Integer(v) = value {
337
+                        state.config.shadow_offset_x = v as i32;
338
+                    }
339
+                }
340
+                "shadow_offset_y" => {
341
+                    if let Value::Integer(v) = value {
342
+                        state.config.shadow_offset_y = v as i32;
343
+                    }
344
+                }
345
+                "opacity_focused" => {
346
+                    if let Value::Number(v) = value {
347
+                        state.config.opacity_focused = v;
348
+                    }
349
+                }
350
+                "opacity_unfocused" => {
351
+                    if let Value::Number(v) = value {
352
+                        state.config.opacity_unfocused = v;
353
+                    }
354
+                }
355
+                "fade_enabled" => {
356
+                    if let Value::Boolean(v) = value {
357
+                        state.config.fade_enabled = v;
358
+                    }
359
+                }
360
+                "fade_delta" => {
361
+                    if let Value::Integer(v) = value {
362
+                        state.config.fade_delta = v as u32;
363
+                    }
364
+                }
297365
                 _ => {
298366
                     tracing::warn!("Unknown config key: {}", key);
299367
                 }
gar/src/config/mod.rsmodified
@@ -21,6 +21,244 @@ pub struct Config {
2121
     pub mouse_follows_focus: bool,
2222
     // Manual bar/panel reserved space (overrides struts)
2323
     pub bar_height: u32,
24
+    // Compositor visual settings (picom)
25
+    // These are stored for reference and potential dynamic picom config generation
26
+    pub corner_radius: u32,
27
+    pub blur_enabled: bool,
28
+    pub blur_method: String,
29
+    pub blur_strength: u32,
30
+    pub shadow_enabled: bool,
31
+    pub shadow_radius: u32,
32
+    pub shadow_opacity: f64,
33
+    pub shadow_offset_x: i32,
34
+    pub shadow_offset_y: i32,
35
+    pub opacity_focused: f64,
36
+    pub opacity_unfocused: f64,
37
+    pub fade_enabled: bool,
38
+    pub fade_delta: u32,
39
+}
40
+
41
+impl Config {
42
+    /// Generate picom.conf content from current config settings.
43
+    pub fn generate_picom_config(&self) -> String {
44
+        let blur_section = if self.blur_enabled {
45
+            format!(
46
+                r#"# Blur
47
+blur-method = "{}";
48
+blur-strength = {};
49
+blur-background = true;
50
+blur-background-frame = false;
51
+blur-kern = "3x3box";
52
+
53
+blur-background-exclude = [
54
+    "window_type = 'dock'",
55
+    "window_type = 'desktop'",
56
+    "window_type = 'menu'",
57
+    "window_type = 'dropdown_menu'",
58
+    "window_type = 'popup_menu'",
59
+    "_NET_WM_BYPASS_COMPOSITOR@:32c = 1"
60
+];"#,
61
+                self.blur_method, self.blur_strength
62
+            )
63
+        } else {
64
+            "# Blur disabled".to_string()
65
+        };
66
+
67
+        let shadow_section = if self.shadow_enabled {
68
+            format!(
69
+                r#"# Shadows
70
+shadow = true;
71
+shadow-radius = {};
72
+shadow-opacity = {:.2};
73
+shadow-offset-x = {};
74
+shadow-offset-y = {};
75
+
76
+shadow-exclude = [
77
+    "window_type = 'dock'",
78
+    "window_type = 'desktop'",
79
+    "window_type = 'menu'",
80
+    "window_type = 'dropdown_menu'",
81
+    "window_type = 'popup_menu'",
82
+    "window_type = 'tooltip'",
83
+    "_NET_WM_STATE@:32a *= '_NET_WM_STATE_FULLSCREEN'",
84
+    "_NET_WM_BYPASS_COMPOSITOR@:32c = 1"
85
+];"#,
86
+                self.shadow_radius,
87
+                self.shadow_opacity,
88
+                self.shadow_offset_x,
89
+                self.shadow_offset_y
90
+            )
91
+        } else {
92
+            "# Shadows disabled\nshadow = false;".to_string()
93
+        };
94
+
95
+        let fade_section = if self.fade_enabled {
96
+            format!(
97
+                r#"# Fading / Animations
98
+fading = true;
99
+fade-in-step = 0.028;
100
+fade-out-step = 0.03;
101
+fade-delta = {};
102
+
103
+no-fading-destroyed-argb = true;
104
+
105
+fade-exclude = [
106
+    "window_type = 'menu'",
107
+    "window_type = 'dropdown_menu'",
108
+    "window_type = 'popup_menu'"
109
+];"#,
110
+                self.fade_delta
111
+            )
112
+        } else {
113
+            "# Fading disabled\nfading = false;".to_string()
114
+        };
115
+
116
+        let opacity_section = if self.opacity_unfocused < 1.0 {
117
+            format!(
118
+                r#"# Focus Opacity
119
+active-opacity = {:.2};
120
+inactive-opacity = {:.2};
121
+frame-opacity = 1.0;"#,
122
+                self.opacity_focused, self.opacity_unfocused
123
+            )
124
+        } else {
125
+            "# Focus opacity: all windows fully opaque".to_string()
126
+        };
127
+
128
+        format!(
129
+            r#"# picom.conf - Auto-generated by gar window manager
130
+# DO NOT EDIT MANUALLY - changes will be overwritten on reload
131
+# Edit ~/.config/gar/init.lua instead and reload with Mod+Shift+R
132
+
133
+# Backend Configuration
134
+backend = "glx";
135
+vsync = true;
136
+use-ewmh-active-win = true;
137
+glx-no-stencil = true;
138
+glx-no-rebind-pixmap = true;
139
+
140
+# Rounded Corners
141
+corner-radius = {};
142
+
143
+rounded-corners-exclude = [
144
+    "window_type = 'dock'",
145
+    "window_type = 'desktop'",
146
+    "window_type = 'tooltip'",
147
+    "window_type = 'menu'",
148
+    "window_type = 'dropdown_menu'",
149
+    "window_type = 'popup_menu'",
150
+    "_NET_WM_STATE@:32a *= '_NET_WM_STATE_FULLSCREEN'"
151
+];
152
+
153
+{}
154
+
155
+{}
156
+
157
+{}
158
+
159
+{}
160
+
161
+# Window Type Settings
162
+wintypes:
163
+{{
164
+    tooltip = {{
165
+        fade = true;
166
+        shadow = false;
167
+        opacity = 0.95;
168
+        focus = true;
169
+        blur-background = false;
170
+    }};
171
+    dock = {{
172
+        shadow = false;
173
+        clip-shadow-above = true;
174
+    }};
175
+    dnd = {{
176
+        shadow = false;
177
+    }};
178
+    popup_menu = {{
179
+        opacity = 0.95;
180
+        shadow = false;
181
+    }};
182
+    dropdown_menu = {{
183
+        opacity = 0.95;
184
+        shadow = false;
185
+    }};
186
+}};
187
+"#,
188
+            self.corner_radius,
189
+            blur_section,
190
+            shadow_section,
191
+            fade_section,
192
+            opacity_section
193
+        )
194
+    }
195
+
196
+    /// Write picom config to ~/.config/gar/picom.conf and signal picom to reload.
197
+    pub fn write_picom_config(&self) -> std::io::Result<()> {
198
+        let config_dir = dirs::config_dir()
199
+            .ok_or_else(|| std::io::Error::new(
200
+                std::io::ErrorKind::NotFound,
201
+                "Could not determine config directory"
202
+            ))?
203
+            .join("gar");
204
+
205
+        // Ensure directory exists
206
+        std::fs::create_dir_all(&config_dir)?;
207
+
208
+        let config_path = config_dir.join("picom.conf");
209
+        let content = self.generate_picom_config();
210
+
211
+        std::fs::write(&config_path, &content)?;
212
+        tracing::info!("Generated picom config at {:?}", config_path);
213
+
214
+        // Signal picom to reload
215
+        Self::reload_picom();
216
+
217
+        Ok(())
218
+    }
219
+
220
+    /// Restart picom to apply new configuration.
221
+    /// Picom doesn't support config reload via signal, so we kill and restart it.
222
+    fn reload_picom() {
223
+        use std::process::Command;
224
+        use std::thread;
225
+        use std::time::Duration;
226
+
227
+        // Kill existing picom
228
+        match Command::new("pkill").arg("picom").status() {
229
+            Ok(status) if status.success() => {
230
+                tracing::info!("Killed picom for restart");
231
+            }
232
+            Ok(_) => {
233
+                tracing::debug!("picom was not running");
234
+            }
235
+            Err(e) => {
236
+                tracing::warn!("Failed to kill picom: {}", e);
237
+                return;
238
+            }
239
+        }
240
+
241
+        // Brief pause to let picom fully exit
242
+        thread::sleep(Duration::from_millis(100));
243
+
244
+        // Restart picom with the new config
245
+        let config_path = dirs::config_dir()
246
+            .map(|d| d.join("gar").join("picom.conf"))
247
+            .unwrap_or_default();
248
+
249
+        match Command::new("picom")
250
+            .args(["-b", "--config"])
251
+            .arg(&config_path)
252
+            .spawn()
253
+        {
254
+            Ok(_) => {
255
+                tracing::info!("Restarted picom with config {:?}", config_path);
256
+            }
257
+            Err(e) => {
258
+                tracing::warn!("Failed to restart picom: {}", e);
259
+            }
260
+        }
261
+    }
24262
 }
25263
 
26264
 impl Default for Config {
@@ -44,6 +282,20 @@ impl Default for Config {
44282
             mouse_follows_focus: false,
45283
             // Manual bar height (0 = use struts from dock windows)
46284
             bar_height: 0,
285
+            // Compositor settings (picom) - matching picom.conf defaults
286
+            corner_radius: 12,
287
+            blur_enabled: true,
288
+            blur_method: "dual_kawase".to_string(),
289
+            blur_strength: 5,
290
+            shadow_enabled: true,
291
+            shadow_radius: 12,
292
+            shadow_opacity: 0.75,
293
+            shadow_offset_x: -7,
294
+            shadow_offset_y: -7,
295
+            opacity_focused: 1.0,
296
+            opacity_unfocused: 1.0, // No unfocused dimming by default
297
+            fade_enabled: true,
298
+            fade_delta: 10,
47299
         }
48300
     }
49301
 }
gar/src/core/mod.rsmodified
@@ -65,6 +65,11 @@ impl WindowManager {
6565
         // Get config values from Lua state
6666
         let config = lua_state.lock().unwrap().config.clone();
6767
 
68
+        // Generate picom config from settings
69
+        if let Err(e) = config.write_picom_config() {
70
+            tracing::warn!("Failed to generate picom config: {}", e);
71
+        }
72
+
6873
         // Initialize IPC server (optional - graceful failure)
6974
         let ipc_server = match IpcServer::new() {
7075
             Ok(server) => Some(server),
@@ -568,6 +573,9 @@ impl WindowManager {
568573
 
569574
             // Clear EWMH fullscreen state
570575
             let _ = self.conn.set_window_state(window, &[]);
576
+
577
+            // Tell compositor to re-apply effects (blur, shadows, etc.)
578
+            let _ = self.conn.set_bypass_compositor(window, false);
571579
         } else {
572580
             // Enter fullscreen
573581
             tracing::info!("Window {} entering fullscreen", window);
@@ -578,6 +586,9 @@ impl WindowManager {
578586
 
579587
             // Set EWMH fullscreen state
580588
             let _ = self.conn.set_window_state(window, &[self.conn.net_wm_state_fullscreen]);
589
+
590
+            // Tell compositor to bypass effects (better performance for games/video)
591
+            let _ = self.conn.set_bypass_compositor(window, true);
581592
         }
582593
 
583594
         // Re-apply layout (fullscreen windows get special treatment in apply_layout)
gar/src/x11/connection.rsmodified
@@ -1160,6 +1160,21 @@ impl Connection {
11601160
         Ok(())
11611161
     }
11621162
 
1163
+    /// Set _NET_WM_BYPASS_COMPOSITOR on a window.
1164
+    /// Value: 0 = no preference, 1 = bypass compositor, 2 = don't bypass
1165
+    /// Setting to 1 tells picom to not apply blur/shadows/rounded corners to this window.
1166
+    pub fn set_bypass_compositor(&self, window: Window, bypass: bool) -> Result<(), Error> {
1167
+        let value: u32 = if bypass { 1 } else { 0 };
1168
+        self.conn.change_property32(
1169
+            x11rb::protocol::xproto::PropMode::REPLACE,
1170
+            window,
1171
+            self.net_wm_bypass_compositor,
1172
+            AtomEnum::CARDINAL,
1173
+            &[value],
1174
+        )?;
1175
+        Ok(())
1176
+    }
1177
+
11631178
     /// Detect connected monitors via RandR.
11641179
     pub fn detect_monitors(&self) -> Result<Vec<crate::core::Monitor>, Error> {
11651180
         use x11rb::protocol::randr::{self, ConnectionExt as RandrExt};
gar/src/x11/events.rsmodified
@@ -1470,6 +1470,11 @@ impl WindowManager {
14701470
             self.config = state.config.clone();
14711471
         }
14721472
 
1473
+        // Regenerate picom config and signal picom to reload
1474
+        if let Err(e) = self.config.write_picom_config() {
1475
+            tracing::warn!("Failed to regenerate picom config: {}", e);
1476
+        }
1477
+
14731478
         // Re-register keybinds
14741479
         self.setup_grabs()?;
14751480
 
start-gar.shmodified
@@ -84,11 +84,18 @@ xrandr \\
8484
   --output HDMI-1 --mode 2560x1440 --pos 1920x0 \\
8585
   --output HDMI-0 --mode 3840x2160 --pos 4480x0
8686
 
87
+# Ensure gar config directory exists
88
+mkdir -p ~/.config/gar
89
+
8790
 # Launch compositor before WM (for proper screen repainting)
88
-# --use-ewmh-active-win uses _NET_ACTIVE_WINDOW for focus detection
89
-# --backend glx is required for picom v12+ (no longer has a default)
91
+# gar generates picom.conf on startup and signals picom to reload
9092
 if command -v picom > /dev/null 2>&1; then
91
-    picom -b --backend glx --use-ewmh-active-win &
93
+    if [[ -f ~/.config/gar/picom.conf ]]; then
94
+        picom -b --config ~/.config/gar/picom.conf &
95
+    else
96
+        # First run: start with GLX backend, gar will generate config and signal reload
97
+        picom -b --backend glx &
98
+    fi
9299
     sleep 0.1
93100
 fi
94101