gardesk/gar / 66eb4cf

Browse files

Add Lua configuration system

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
66eb4cf8ffac776e5c33a0b4adb8604382ab1aa1
Parents
5a3c40f
Tree
1a01e59

3 changed files

StatusFile+-
A gar/config/default.lua 69 0
A gar/src/config/lua.rs 431 0
M gar/src/config/mod.rs 3 1
gar/config/default.luaadded
@@ -0,0 +1,69 @@
1
+-- gar default configuration
2
+-- Copy to ~/.config/gar/init.lua to customize
3
+
4
+-- Appearance
5
+gar.set("border_width", 2)
6
+gar.set("border_color_focused", "#5294e2")
7
+gar.set("border_color_unfocused", "#2d2d2d")
8
+gar.set("gap_inner", 0)
9
+gar.set("gap_outer", 0)
10
+
11
+-- Mod key: "mod" = Super/Win, "alt" = Alt
12
+-- Using alt for testing in nested X (i3 grabs super)
13
+local mod = "alt"
14
+
15
+-- Terminal
16
+gar.bind(mod .. "+Return", function()
17
+    gar.exec("alacritty || kitty || foot || xterm")
18
+end)
19
+
20
+-- Close window
21
+gar.bind(mod .. "+q", gar.close_window)
22
+
23
+-- Reload config
24
+gar.bind(mod .. "+shift+r", gar.reload)
25
+
26
+-- Focus navigation (arrow keys)
27
+gar.bind(mod .. "+Left", gar.focus("left"))
28
+gar.bind(mod .. "+Right", gar.focus("right"))
29
+gar.bind(mod .. "+Up", gar.focus("up"))
30
+gar.bind(mod .. "+Down", gar.focus("down"))
31
+
32
+-- Focus navigation (vim keys)
33
+gar.bind(mod .. "+h", gar.focus("left"))
34
+gar.bind(mod .. "+l", gar.focus("right"))
35
+gar.bind(mod .. "+k", gar.focus("up"))
36
+gar.bind(mod .. "+j", gar.focus("down"))
37
+
38
+-- Swap windows
39
+gar.bind(mod .. "+shift+Left", gar.swap("left"))
40
+gar.bind(mod .. "+shift+Right", gar.swap("right"))
41
+gar.bind(mod .. "+shift+Up", gar.swap("up"))
42
+gar.bind(mod .. "+shift+Down", gar.swap("down"))
43
+
44
+gar.bind(mod .. "+shift+h", gar.swap("left"))
45
+gar.bind(mod .. "+shift+l", gar.swap("right"))
46
+gar.bind(mod .. "+shift+k", gar.swap("up"))
47
+gar.bind(mod .. "+shift+j", gar.swap("down"))
48
+
49
+-- Resize
50
+gar.bind(mod .. "+ctrl+Left", gar.resize("left", 0.05))
51
+gar.bind(mod .. "+ctrl+Right", gar.resize("right", 0.05))
52
+gar.bind(mod .. "+ctrl+Up", gar.resize("up", 0.05))
53
+gar.bind(mod .. "+ctrl+Down", gar.resize("down", 0.05))
54
+
55
+gar.bind(mod .. "+ctrl+h", gar.resize("left", 0.05))
56
+gar.bind(mod .. "+ctrl+l", gar.resize("right", 0.05))
57
+gar.bind(mod .. "+ctrl+k", gar.resize("up", 0.05))
58
+gar.bind(mod .. "+ctrl+j", gar.resize("down", 0.05))
59
+
60
+-- Equalize splits
61
+gar.bind(mod .. "+e", gar.equalize)
62
+
63
+-- Workspaces
64
+for i = 1, 9 do
65
+    gar.bind(mod .. "+" .. i, gar.workspace(i))
66
+    gar.bind(mod .. "+shift+" .. i, gar.move_to_workspace(i))
67
+end
68
+gar.bind(mod .. "+0", gar.workspace(10))
69
+gar.bind(mod .. "+shift+0", gar.move_to_workspace(10))
gar/src/config/lua.rsadded
@@ -0,0 +1,431 @@
1
+use std::path::PathBuf;
2
+use std::sync::{Arc, Mutex};
3
+
4
+use mlua::{Function, Lua, Result as LuaResult, Table, Value};
5
+use x11rb::protocol::xproto::ModMask;
6
+
7
+use super::Config;
8
+
9
+/// Actions that can be triggered by keybinds
10
+#[derive(Debug, Clone)]
11
+pub enum Action {
12
+    Exec(String),
13
+    Focus(String),
14
+    Swap(String),
15
+    Resize(String, f32),
16
+    CloseWindow,
17
+    Workspace(usize),
18
+    MoveToWorkspace(usize),
19
+    Equalize,
20
+    Reload,
21
+    Exit,
22
+    LuaCallback(usize), // Index into callback registry
23
+}
24
+
25
+/// A registered keybind
26
+#[derive(Debug, Clone)]
27
+pub struct Keybind {
28
+    pub modifiers: ModMask,
29
+    pub keysym: u32,
30
+    pub action: Action,
31
+}
32
+
33
+/// Shared state between Lua and Rust
34
+pub struct LuaState {
35
+    pub config: Config,
36
+    pub keybinds: Vec<Keybind>,
37
+    pub callbacks: Vec<mlua::RegistryKey>,
38
+}
39
+
40
+impl Default for LuaState {
41
+    fn default() -> Self {
42
+        Self {
43
+            config: Config::default(),
44
+            keybinds: Vec::new(),
45
+            callbacks: Vec::new(),
46
+        }
47
+    }
48
+}
49
+
50
+pub struct LuaConfig {
51
+    lua: Lua,
52
+    state: Arc<Mutex<LuaState>>,
53
+}
54
+
55
+impl LuaConfig {
56
+    pub fn new() -> LuaResult<Self> {
57
+        let lua = Lua::new();
58
+        let state = Arc::new(Mutex::new(LuaState::default()));
59
+
60
+        Ok(Self { lua, state })
61
+    }
62
+
63
+    /// Get the shared state
64
+    pub fn state(&self) -> Arc<Mutex<LuaState>> {
65
+        Arc::clone(&self.state)
66
+    }
67
+
68
+    /// Load and execute configuration
69
+    pub fn load(&self) -> LuaResult<()> {
70
+        // Set up the gar global table with all APIs
71
+        self.setup_api()?;
72
+
73
+        // Find config file
74
+        let config_path = Self::find_config();
75
+
76
+        let source = match config_path {
77
+            Some(path) => {
78
+                tracing::info!("Loading config from {:?}", path);
79
+                std::fs::read_to_string(&path).map_err(|e| mlua::Error::external(e))?
80
+            }
81
+            None => {
82
+                tracing::info!("Using default config");
83
+                include_str!("../../config/default.lua").to_string()
84
+            }
85
+        };
86
+
87
+        self.lua.load(&source).exec()?;
88
+
89
+        let state = self.state.lock().unwrap();
90
+        tracing::info!(
91
+            "Config loaded: {} keybinds registered",
92
+            state.keybinds.len()
93
+        );
94
+
95
+        Ok(())
96
+    }
97
+
98
+    /// Reload configuration (clears existing keybinds)
99
+    pub fn reload(&self) -> LuaResult<()> {
100
+        {
101
+            let mut state = self.state.lock().unwrap();
102
+            state.keybinds.clear();
103
+            state.callbacks.clear();
104
+            state.config = Config::default();
105
+        }
106
+        self.load()
107
+    }
108
+
109
+    /// Execute a Lua callback by registry index
110
+    pub fn execute_callback(&self, index: usize) -> LuaResult<()> {
111
+        let state = self.state.lock().unwrap();
112
+        if let Some(key) = state.callbacks.get(index) {
113
+            let func: Function = self.lua.registry_value(key)?;
114
+            drop(state); // Release lock before calling
115
+            func.call::<()>(())?;
116
+        }
117
+        Ok(())
118
+    }
119
+
120
+    fn find_config() -> Option<PathBuf> {
121
+        dirs::config_dir()
122
+            .map(|p| p.join("gar/init.lua"))
123
+            .filter(|p| p.exists())
124
+    }
125
+
126
+    fn setup_api(&self) -> LuaResult<()> {
127
+        let gar = self.lua.create_table()?;
128
+
129
+        // gar.set(key, value)
130
+        self.register_set(&gar)?;
131
+
132
+        // gar.bind(keyspec, callback)
133
+        self.register_bind(&gar)?;
134
+
135
+        // gar.exec(cmd)
136
+        self.register_exec(&gar)?;
137
+
138
+        // Built-in action functions
139
+        self.register_actions(&gar)?;
140
+
141
+        self.lua.globals().set("gar", gar)?;
142
+        Ok(())
143
+    }
144
+
145
+    fn register_set(&self, gar: &Table) -> LuaResult<()> {
146
+        let state = Arc::clone(&self.state);
147
+        let set_fn = self.lua.create_function(move |_, (key, value): (String, Value)| {
148
+            let mut state = state.lock().unwrap();
149
+            match key.as_str() {
150
+                "border_width" => {
151
+                    if let Value::Integer(v) = value {
152
+                        state.config.border_width = v as u32;
153
+                    }
154
+                }
155
+                "border_color_focused" => {
156
+                    if let Value::String(s) = value {
157
+                        if let Ok(str_val) = s.to_str() {
158
+                            if let Some(color) = parse_color(&str_val) {
159
+                                state.config.border_color_focused = color;
160
+                            }
161
+                        }
162
+                    }
163
+                }
164
+                "border_color_unfocused" => {
165
+                    if let Value::String(s) = value {
166
+                        if let Ok(str_val) = s.to_str() {
167
+                            if let Some(color) = parse_color(&str_val) {
168
+                                state.config.border_color_unfocused = color;
169
+                            }
170
+                        }
171
+                    }
172
+                }
173
+                "gap_inner" => {
174
+                    if let Value::Integer(v) = value {
175
+                        state.config.gap_inner = v as u32;
176
+                    }
177
+                }
178
+                "gap_outer" => {
179
+                    if let Value::Integer(v) = value {
180
+                        state.config.gap_outer = v as u32;
181
+                    }
182
+                }
183
+                _ => {
184
+                    tracing::warn!("Unknown config key: {}", key);
185
+                }
186
+            }
187
+            Ok(())
188
+        })?;
189
+        gar.set("set", set_fn)
190
+    }
191
+
192
+    fn register_bind(&self, gar: &Table) -> LuaResult<()> {
193
+        let state = Arc::clone(&self.state);
194
+        let lua_weak = self.lua.clone();
195
+
196
+        let bind_fn = self.lua.create_function(move |_, (keyspec, callback): (String, Value)| {
197
+            let (modifiers, keysym) = match parse_keyspec(&keyspec) {
198
+                Some(k) => k,
199
+                None => {
200
+                    tracing::warn!("Invalid keyspec: {}", keyspec);
201
+                    return Ok(());
202
+                }
203
+            };
204
+
205
+            let action = match callback {
206
+                Value::Function(f) => {
207
+                    // Store callback in registry
208
+                    let key = lua_weak.create_registry_value(f)?;
209
+                    let mut state = state.lock().unwrap();
210
+                    let index = state.callbacks.len();
211
+                    state.callbacks.push(key);
212
+                    Action::LuaCallback(index)
213
+                }
214
+                Value::Table(t) => {
215
+                    // Check if it's a built-in action table
216
+                    if let Ok(action_type) = t.get::<String>("action") {
217
+                        match action_type.as_str() {
218
+                            "close_window" => Action::CloseWindow,
219
+                            "reload" => Action::Reload,
220
+                            "exit" => Action::Exit,
221
+                            "equalize" => Action::Equalize,
222
+                            "focus" => {
223
+                                let dir: String = t.get("direction").unwrap_or_default();
224
+                                Action::Focus(dir)
225
+                            }
226
+                            "swap" => {
227
+                                let dir: String = t.get("direction").unwrap_or_default();
228
+                                Action::Swap(dir)
229
+                            }
230
+                            "resize" => {
231
+                                let dir: String = t.get("direction").unwrap_or_default();
232
+                                let amount: f32 = t.get("amount").unwrap_or(0.05);
233
+                                Action::Resize(dir, amount)
234
+                            }
235
+                            "workspace" => {
236
+                                let n: usize = t.get("workspace").unwrap_or(1);
237
+                                Action::Workspace(n)
238
+                            }
239
+                            "move_to_workspace" => {
240
+                                let n: usize = t.get("workspace").unwrap_or(1);
241
+                                Action::MoveToWorkspace(n)
242
+                            }
243
+                            _ => {
244
+                                tracing::warn!("Unknown action type: {}", action_type);
245
+                                return Ok(());
246
+                            }
247
+                        }
248
+                    } else {
249
+                        return Ok(());
250
+                    }
251
+                }
252
+                _ => return Ok(()),
253
+            };
254
+
255
+            let mut state = state.lock().unwrap();
256
+            state.keybinds.push(Keybind {
257
+                modifiers,
258
+                keysym,
259
+                action,
260
+            });
261
+
262
+            tracing::debug!("Bound {:?}+{:x} to {:?}", modifiers, keysym, state.keybinds.last().unwrap().action);
263
+            Ok(())
264
+        })?;
265
+        gar.set("bind", bind_fn)
266
+    }
267
+
268
+    fn register_exec(&self, gar: &Table) -> LuaResult<()> {
269
+        let exec_fn = self.lua.create_function(|_, cmd: String| {
270
+            tracing::debug!("exec: {}", cmd);
271
+            std::process::Command::new("sh")
272
+                .arg("-c")
273
+                .arg(&cmd)
274
+                .spawn()
275
+                .ok();
276
+            Ok(())
277
+        })?;
278
+        gar.set("exec", exec_fn)
279
+    }
280
+
281
+    fn register_actions(&self, gar: &Table) -> LuaResult<()> {
282
+        // gar.close_window - returns a table that bind() recognizes
283
+        let close_window = self.lua.create_table()?;
284
+        close_window.set("action", "close_window")?;
285
+        gar.set("close_window", close_window)?;
286
+
287
+        // gar.reload
288
+        let reload = self.lua.create_table()?;
289
+        reload.set("action", "reload")?;
290
+        gar.set("reload", reload)?;
291
+
292
+        // gar.exit
293
+        let exit = self.lua.create_table()?;
294
+        exit.set("action", "exit")?;
295
+        gar.set("exit", exit)?;
296
+
297
+        // gar.equalize
298
+        let equalize = self.lua.create_table()?;
299
+        equalize.set("action", "equalize")?;
300
+        gar.set("equalize", equalize)?;
301
+
302
+        // gar.focus(direction) - creates action
303
+        let focus_fn = self.lua.create_function(|lua, direction: String| {
304
+            let t = lua.create_table()?;
305
+            t.set("action", "focus")?;
306
+            t.set("direction", direction)?;
307
+            Ok(t)
308
+        })?;
309
+        gar.set("focus", focus_fn)?;
310
+
311
+        // gar.swap(direction)
312
+        let swap_fn = self.lua.create_function(|lua, direction: String| {
313
+            let t = lua.create_table()?;
314
+            t.set("action", "swap")?;
315
+            t.set("direction", direction)?;
316
+            Ok(t)
317
+        })?;
318
+        gar.set("swap", swap_fn)?;
319
+
320
+        // gar.resize(direction, amount)
321
+        let resize_fn = self.lua.create_function(|lua, (direction, amount): (String, f32)| {
322
+            let t = lua.create_table()?;
323
+            t.set("action", "resize")?;
324
+            t.set("direction", direction)?;
325
+            t.set("amount", amount)?;
326
+            Ok(t)
327
+        })?;
328
+        gar.set("resize", resize_fn)?;
329
+
330
+        // gar.workspace(n)
331
+        let workspace_fn = self.lua.create_function(|lua, n: usize| {
332
+            let t = lua.create_table()?;
333
+            t.set("action", "workspace")?;
334
+            t.set("workspace", n)?;
335
+            Ok(t)
336
+        })?;
337
+        gar.set("workspace", workspace_fn)?;
338
+
339
+        // gar.move_to_workspace(n)
340
+        let move_fn = self.lua.create_function(|lua, n: usize| {
341
+            let t = lua.create_table()?;
342
+            t.set("action", "move_to_workspace")?;
343
+            t.set("workspace", n)?;
344
+            Ok(t)
345
+        })?;
346
+        gar.set("move_to_workspace", move_fn)?;
347
+
348
+        Ok(())
349
+    }
350
+}
351
+
352
+/// Parse a hex color string like "#5294e2" to u32
353
+fn parse_color(s: &str) -> Option<u32> {
354
+    let s = s.trim_start_matches('#');
355
+    u32::from_str_radix(s, 16).ok()
356
+}
357
+
358
+/// Parse a keyspec like "mod+shift+q" into (ModMask, keysym)
359
+fn parse_keyspec(spec: &str) -> Option<(ModMask, u32)> {
360
+    let lowercase = spec.to_lowercase();
361
+    let parts: Vec<&str> = lowercase.split('+').collect();
362
+    if parts.is_empty() {
363
+        return None;
364
+    }
365
+
366
+    let mut modifiers = ModMask::from(0u16);
367
+    let mut key_part = None;
368
+
369
+    for part in &parts {
370
+        match *part {
371
+            "mod" | "super" | "mod4" => modifiers |= ModMask::M4,
372
+            "alt" | "mod1" => modifiers |= ModMask::M1,
373
+            "shift" => modifiers |= ModMask::SHIFT,
374
+            "ctrl" | "control" => modifiers |= ModMask::CONTROL,
375
+            _ => key_part = Some(*part),
376
+        }
377
+    }
378
+
379
+    let key = key_part?;
380
+    let keysym = match key {
381
+        "return" | "enter" => 0xff0d,
382
+        "escape" | "esc" => 0xff1b,
383
+        "tab" => 0xff09,
384
+        "space" => 0x20,
385
+        "backspace" => 0xff08,
386
+        "delete" => 0xffff,
387
+        "left" => 0xff51,
388
+        "up" => 0xff52,
389
+        "right" => 0xff53,
390
+        "down" => 0xff54,
391
+        "home" => 0xff50,
392
+        "end" => 0xff57,
393
+        "page_up" | "pageup" => 0xff55,
394
+        "page_down" | "pagedown" => 0xff56,
395
+        "f1" => 0xffbe,
396
+        "f2" => 0xffbf,
397
+        "f3" => 0xffc0,
398
+        "f4" => 0xffc1,
399
+        "f5" => 0xffc2,
400
+        "f6" => 0xffc3,
401
+        "f7" => 0xffc4,
402
+        "f8" => 0xffc5,
403
+        "f9" => 0xffc6,
404
+        "f10" => 0xffc7,
405
+        "f11" => 0xffc8,
406
+        "f12" => 0xffc9,
407
+        // Numbers
408
+        "0" => 0x30,
409
+        "1" => 0x31,
410
+        "2" => 0x32,
411
+        "3" => 0x33,
412
+        "4" => 0x34,
413
+        "5" => 0x35,
414
+        "6" => 0x36,
415
+        "7" => 0x37,
416
+        "8" => 0x38,
417
+        "9" => 0x39,
418
+        // Letters (lowercase keysyms)
419
+        s if s.len() == 1 => {
420
+            let c = s.chars().next()?;
421
+            if c.is_ascii_lowercase() {
422
+                c as u32
423
+            } else {
424
+                return None;
425
+            }
426
+        }
427
+        _ => return None,
428
+    };
429
+
430
+    Some((modifiers, keysym))
431
+}
gar/src/config/mod.rsmodified
@@ -1,4 +1,6 @@
1
-// Configuration module - Lua integration comes in Sprint 4
1
+mod lua;
2
+
3
+pub use lua::{Action, Keybind, LuaConfig, LuaState};
24
 
35
 #[derive(Debug, Clone)]
46
 pub struct Config {