| 1 | mod lua; |
| 2 | |
| 3 | pub use lua::{Action, Keybind, LuaConfig, LuaState, RuleActions, WindowMatch, WindowRule}; |
| 4 | |
| 5 | #[derive(Debug, Clone)] |
| 6 | pub struct Config { |
| 7 | pub border_width: u32, |
| 8 | pub border_color_focused: u32, |
| 9 | pub border_color_unfocused: u32, |
| 10 | pub border_color_urgent: u32, |
| 11 | pub gap_inner: u32, |
| 12 | pub gap_outer: u32, |
| 13 | // Title bar settings |
| 14 | pub titlebar_enabled: bool, |
| 15 | pub titlebar_height: u32, |
| 16 | pub titlebar_color_focused: u32, |
| 17 | pub titlebar_color_unfocused: u32, |
| 18 | pub titlebar_text_color: u32, |
| 19 | // Behavior settings |
| 20 | pub follow_window_on_move: bool, |
| 21 | pub mouse_follows_focus: bool, |
| 22 | // Manual bar/panel reserved space (overrides struts) |
| 23 | 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 | } |
| 262 | } |
| 263 | |
| 264 | impl Default for Config { |
| 265 | fn default() -> Self { |
| 266 | Self { |
| 267 | border_width: 2, |
| 268 | border_color_focused: 0x5294e2, |
| 269 | border_color_unfocused: 0x2d2d2d, |
| 270 | border_color_urgent: 0xff5555, // Red for urgent windows |
| 271 | gap_inner: 0, |
| 272 | gap_outer: 0, |
| 273 | // Title bars disabled by default |
| 274 | titlebar_enabled: false, |
| 275 | titlebar_height: 20, |
| 276 | titlebar_color_focused: 0x3d3d3d, |
| 277 | titlebar_color_unfocused: 0x2d2d2d, |
| 278 | titlebar_text_color: 0xffffff, |
| 279 | // Behavior: follow window when moving to another workspace |
| 280 | follow_window_on_move: false, |
| 281 | // Behavior: warp mouse pointer to center of focused window |
| 282 | mouse_follows_focus: false, |
| 283 | // Manual bar height (0 = use struts from dock windows) |
| 284 | 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, |
| 299 | } |
| 300 | } |
| 301 | } |
| 302 |