oxwm

https://git.tonybtw.com/oxwm.git git://git.tonybtw.com/oxwm.git
30,292 bytes raw
1
use mlua::{Lua, Table, Value};
2
use std::cell::RefCell;
3
use std::rc::Rc;
4
5
use crate::bar::BlockConfig;
6
use crate::errors::ConfigError;
7
use crate::keyboard::handlers::{Arg, KeyAction, KeyBinding, KeyPress};
8
use crate::keyboard::keysyms::{self, Keysym};
9
use crate::ColorScheme;
10
use x11rb::protocol::xproto::KeyButMask;
11
12
#[derive(Clone)]
13
pub struct ConfigBuilder {
14
    pub border_width: u32,
15
    pub border_focused: u32,
16
    pub border_unfocused: u32,
17
    pub font: String,
18
    pub gaps_enabled: bool,
19
    pub gap_inner_horizontal: u32,
20
    pub gap_inner_vertical: u32,
21
    pub gap_outer_horizontal: u32,
22
    pub gap_outer_vertical: u32,
23
    pub terminal: String,
24
    pub modkey: KeyButMask,
25
    pub tags: Vec<String>,
26
    pub layout_symbols: Vec<crate::LayoutSymbolOverride>,
27
    pub keybindings: Vec<KeyBinding>,
28
    pub status_blocks: Vec<BlockConfig>,
29
    pub scheme_normal: ColorScheme,
30
    pub scheme_occupied: ColorScheme,
31
    pub scheme_selected: ColorScheme,
32
    pub autostart: Vec<String>,
33
}
34
35
impl Default for ConfigBuilder {
36
    fn default() -> Self {
37
        Self {
38
            border_width: 2,
39
            border_focused: 0x6dade3,
40
            border_unfocused: 0xbbbbbb,
41
            font: "monospace:style=Bold:size=10".to_string(),
42
            gaps_enabled: true,
43
            gap_inner_horizontal: 5,
44
            gap_inner_vertical: 5,
45
            gap_outer_horizontal: 5,
46
            gap_outer_vertical: 5,
47
            terminal: "st".to_string(),
48
            modkey: KeyButMask::MOD4,
49
            tags: vec!["1".into(), "2".into(), "3".into()],
50
            layout_symbols: Vec::new(),
51
            keybindings: Vec::new(),
52
            status_blocks: Vec::new(),
53
            scheme_normal: ColorScheme {
54
                foreground: 0xffffff,
55
                background: 0x000000,
56
                underline: 0x444444,
57
            },
58
            scheme_occupied: ColorScheme {
59
                foreground: 0xffffff,
60
                background: 0x000000,
61
                underline: 0x444444,
62
            },
63
            scheme_selected: ColorScheme {
64
                foreground: 0xffffff,
65
                background: 0x000000,
66
                underline: 0x444444,
67
            },
68
            autostart: Vec::new(),
69
        }
70
    }
71
}
72
73
type SharedBuilder = Rc<RefCell<ConfigBuilder>>;
74
75
pub fn register_api(lua: &Lua) -> Result<SharedBuilder, ConfigError> {
76
    let builder = Rc::new(RefCell::new(ConfigBuilder::default()));
77
78
    let oxwm_table = lua.create_table()?;
79
80
    register_spawn(&lua, &oxwm_table, builder.clone())?;
81
    register_key_module(&lua, &oxwm_table, builder.clone())?;
82
    register_gaps_module(&lua, &oxwm_table, builder.clone())?;
83
    register_border_module(&lua, &oxwm_table, builder.clone())?;
84
    register_client_module(&lua, &oxwm_table)?;
85
    register_layout_module(&lua, &oxwm_table)?;
86
    register_tag_module(&lua, &oxwm_table)?;
87
    register_bar_module(&lua, &oxwm_table, builder.clone())?;
88
    register_misc(&lua, &oxwm_table, builder.clone())?;
89
90
    lua.globals().set("oxwm", oxwm_table)?;
91
92
    Ok(builder)
93
}
94
95
fn register_spawn(lua: &Lua, parent: &Table, _builder: SharedBuilder) -> Result<(), ConfigError> {
96
    let spawn = lua.create_function(|lua, cmd: Value| {
97
        create_action_table(lua, "Spawn", cmd)
98
    })?;
99
    let spawn_terminal = lua.create_function(|lua, ()| {
100
        create_action_table(lua, "SpawnTerminal", Value::Nil)
101
    })?;
102
    parent.set("spawn", spawn)?;
103
    parent.set("spawn_terminal", spawn_terminal)?;
104
    Ok(())
105
}
106
107
fn register_key_module(lua: &Lua, parent: &Table, builder: SharedBuilder) -> Result<(), ConfigError> {
108
    let key_table = lua.create_table()?;
109
110
    let builder_clone = builder.clone();
111
    let bind = lua.create_function(move |lua, (mods, key, action): (Value, String, Value)| {
112
        let modifiers = parse_modifiers_value(lua, mods)?;
113
        let keysym = parse_keysym(&key)?;
114
        let (key_action, arg) = parse_action_value(lua, action)?;
115
116
        let binding = KeyBinding::single_key(modifiers, keysym, key_action, arg);
117
        builder_clone.borrow_mut().keybindings.push(binding);
118
119
        Ok(())
120
    })?;
121
122
    let builder_clone = builder.clone();
123
    let chord = lua.create_function(move |lua, (keys, action): (Table, Value)| {
124
        let mut key_presses = Vec::new();
125
126
        for i in 1..=keys.len()? {
127
            let key_spec: Table = keys.get(i)?;
128
            let mods: Value = key_spec.get(1)?;
129
            let key: String = key_spec.get(2)?;
130
131
            let modifiers = parse_modifiers_value(lua, mods)?;
132
            let keysym = parse_keysym(&key)?;
133
134
            key_presses.push(KeyPress { modifiers, keysym });
135
        }
136
137
        let (key_action, arg) = parse_action_value(lua, action)?;
138
        let binding = KeyBinding::new(key_presses, key_action, arg);
139
        builder_clone.borrow_mut().keybindings.push(binding);
140
141
        Ok(())
142
    })?;
143
144
    key_table.set("bind", bind)?;
145
    key_table.set("chord", chord)?;
146
    parent.set("key", key_table)?;
147
    Ok(())
148
}
149
150
fn register_gaps_module(lua: &Lua, parent: &Table, builder: SharedBuilder) -> Result<(), ConfigError> {
151
    let gaps_table = lua.create_table()?;
152
153
    let builder_clone = builder.clone();
154
    let set_enabled = lua.create_function(move |_, enabled: bool| {
155
        builder_clone.borrow_mut().gaps_enabled = enabled;
156
        Ok(())
157
    })?;
158
159
    let builder_clone = builder.clone();
160
    let enable = lua.create_function(move |_, ()| {
161
        builder_clone.borrow_mut().gaps_enabled = true;
162
        Ok(())
163
    })?;
164
165
    let builder_clone = builder.clone();
166
    let disable = lua.create_function(move |_, ()| {
167
        builder_clone.borrow_mut().gaps_enabled = false;
168
        Ok(())
169
    })?;
170
171
    let builder_clone = builder.clone();
172
    let set_inner = lua.create_function(move |_, (h, v): (u32, u32)| {
173
        let mut b = builder_clone.borrow_mut();
174
        b.gap_inner_horizontal = h;
175
        b.gap_inner_vertical = v;
176
        Ok(())
177
    })?;
178
179
    let builder_clone = builder.clone();
180
    let set_outer = lua.create_function(move |_, (h, v): (u32, u32)| {
181
        let mut b = builder_clone.borrow_mut();
182
        b.gap_outer_horizontal = h;
183
        b.gap_outer_vertical = v;
184
        Ok(())
185
    })?;
186
187
    gaps_table.set("set_enabled", set_enabled)?;
188
    gaps_table.set("enable", enable)?;
189
    gaps_table.set("disable", disable)?;
190
    gaps_table.set("set_inner", set_inner)?;
191
    gaps_table.set("set_outer", set_outer)?;
192
    parent.set("gaps", gaps_table)?;
193
    Ok(())
194
}
195
196
fn register_border_module(lua: &Lua, parent: &Table, builder: SharedBuilder) -> Result<(), ConfigError> {
197
    let border_table = lua.create_table()?;
198
199
    let builder_clone = builder.clone();
200
    let set_width = lua.create_function(move |_, width: u32| {
201
        builder_clone.borrow_mut().border_width = width;
202
        Ok(())
203
    })?;
204
205
    let builder_clone = builder.clone();
206
    let set_focused_color = lua.create_function(move |_, color: Value| {
207
        let color_u32 = parse_color_value(color)?;
208
        builder_clone.borrow_mut().border_focused = color_u32;
209
        Ok(())
210
    })?;
211
212
    let builder_clone = builder.clone();
213
    let set_unfocused_color = lua.create_function(move |_, color: Value| {
214
        let color_u32 = parse_color_value(color)?;
215
        builder_clone.borrow_mut().border_unfocused = color_u32;
216
        Ok(())
217
    })?;
218
219
    border_table.set("set_width", set_width)?;
220
    border_table.set("set_focused_color", set_focused_color)?;
221
    border_table.set("set_unfocused_color", set_unfocused_color)?;
222
    parent.set("border", border_table)?;
223
    Ok(())
224
}
225
226
fn register_client_module(lua: &Lua, parent: &Table) -> Result<(), ConfigError> {
227
    let client_table = lua.create_table()?;
228
229
    let kill = lua.create_function(|lua, ()| {
230
        create_action_table(lua, "KillClient", Value::Nil)
231
    })?;
232
233
    let toggle_fullscreen = lua.create_function(|lua, ()| {
234
        create_action_table(lua, "ToggleFullScreen", Value::Nil)
235
    })?;
236
237
    let toggle_floating = lua.create_function(|lua, ()| {
238
        create_action_table(lua, "ToggleFloating", Value::Nil)
239
    })?;
240
241
    let focus_stack = lua.create_function(|lua, dir: i32| {
242
        create_action_table(lua, "FocusStack", Value::Integer(dir as i64))
243
    })?;
244
245
    let focus_direction = lua.create_function(|lua, dir: String| {
246
        let dir_int = direction_string_to_int(&dir)?;
247
        create_action_table(lua, "FocusDirection", Value::Integer(dir_int))
248
    })?;
249
250
    let swap_direction = lua.create_function(|lua, dir: String| {
251
        let dir_int = direction_string_to_int(&dir)?;
252
        create_action_table(lua, "SwapDirection", Value::Integer(dir_int))
253
    })?;
254
255
    let smart_move = lua.create_function(|lua, dir: String| {
256
        let dir_int = direction_string_to_int(&dir)?;
257
        create_action_table(lua, "SmartMoveWin", Value::Integer(dir_int))
258
    })?;
259
260
    let exchange = lua.create_function(|lua, ()| {
261
        create_action_table(lua, "ExchangeClient", Value::Nil)
262
    })?;
263
264
    client_table.set("kill", kill)?;
265
    client_table.set("toggle_fullscreen", toggle_fullscreen)?;
266
    client_table.set("toggle_floating", toggle_floating)?;
267
    client_table.set("focus_stack", focus_stack)?;
268
    client_table.set("focus_direction", focus_direction)?;
269
    client_table.set("swap_direction", swap_direction)?;
270
    client_table.set("smart_move", smart_move)?;
271
    client_table.set("exchange", exchange)?;
272
273
    parent.set("client", client_table)?;
274
    Ok(())
275
}
276
277
fn register_layout_module(lua: &Lua, parent: &Table) -> Result<(), ConfigError> {
278
    let layout_table = lua.create_table()?;
279
280
    let cycle = lua.create_function(|lua, ()| {
281
        create_action_table(lua, "CycleLayout", Value::Nil)
282
    })?;
283
284
    let set = lua.create_function(|lua, name: String| {
285
        create_action_table(lua, "ChangeLayout", Value::String(lua.create_string(&name)?))
286
    })?;
287
288
    layout_table.set("cycle", cycle)?;
289
    layout_table.set("set", set)?;
290
    parent.set("layout", layout_table)?;
291
    Ok(())
292
}
293
294
fn register_tag_module(lua: &Lua, parent: &Table) -> Result<(), ConfigError> {
295
    let tag_table = lua.create_table()?;
296
297
    let view = lua.create_function(|lua, idx: i32| {
298
        create_action_table(lua, "ViewTag", Value::Integer(idx as i64))
299
    })?;
300
301
    let move_to = lua.create_function(|lua, idx: i32| {
302
        create_action_table(lua, "MoveToTag", Value::Integer(idx as i64))
303
    })?;
304
305
    tag_table.set("view", view)?;
306
    tag_table.set("move_to", move_to)?;
307
    parent.set("tag", tag_table)?;
308
    Ok(())
309
}
310
311
fn register_bar_module(lua: &Lua, parent: &Table, builder: SharedBuilder) -> Result<(), ConfigError> {
312
    let bar_table = lua.create_table()?;
313
314
    let builder_clone = builder.clone();
315
    let set_font = lua.create_function(move |_, font: String| {
316
        builder_clone.borrow_mut().font = font;
317
        Ok(())
318
    })?;
319
320
    let block_table = lua.create_table()?;
321
322
    let ram = lua.create_function(|lua, config: Table| {
323
        create_block_config(lua, config, "Ram", None)
324
    })?;
325
326
    let datetime = lua.create_function(|lua, config: Table| {
327
        let date_format: String = config.get("date_format")
328
            .map_err(|_| mlua::Error::RuntimeError("oxwm.bar.block.datetime: 'date_format' field is required (e.g., '%H:%M')".into()))?;
329
        create_block_config(lua, config, "DateTime", Some(Value::String(lua.create_string(&date_format)?)))
330
    })?;
331
332
    let shell = lua.create_function(|lua, config: Table| {
333
        let command: String = config.get("command")
334
            .map_err(|_| mlua::Error::RuntimeError("oxwm.bar.block.shell: 'command' field is required".into()))?;
335
        create_block_config(lua, config, "Shell", Some(Value::String(lua.create_string(&command)?)))
336
    })?;
337
338
    let static_block = lua.create_function(|lua, config: Table| {
339
        let text: String = config.get("text")
340
            .map_err(|_| mlua::Error::RuntimeError("oxwm.bar.block.static: 'text' field is required".into()))?;
341
        create_block_config(lua, config, "Static", Some(Value::String(lua.create_string(&text)?)))
342
    })?;
343
344
    let battery = lua.create_function(|lua, config: Table| {
345
        let charging: String = config.get("charging")
346
            .map_err(|_| mlua::Error::RuntimeError("oxwm.bar.block.battery: 'charging' field is required".into()))?;
347
        let discharging: String = config.get("discharging")
348
            .map_err(|_| mlua::Error::RuntimeError("oxwm.bar.block.battery: 'discharging' field is required".into()))?;
349
        let full: String = config.get("full")
350
            .map_err(|_| mlua::Error::RuntimeError("oxwm.bar.block.battery: 'full' field is required".into()))?;
351
352
        let formats_table = lua.create_table()?;
353
        formats_table.set("charging", charging)?;
354
        formats_table.set("discharging", discharging)?;
355
        formats_table.set("full", full)?;
356
357
        create_block_config(lua, config, "Battery", Some(Value::Table(formats_table)))
358
    })?;
359
360
    block_table.set("ram", ram)?;
361
    block_table.set("datetime", datetime)?;
362
    block_table.set("shell", shell)?;
363
    block_table.set("static", static_block)?;
364
    block_table.set("battery", battery)?;
365
366
    // Deprecated add_block() function for backwards compatibility
367
    // This allows old configs to still work, but users should migrate to set_blocks()
368
    let builder_clone = builder.clone();
369
    let add_block = lua.create_function(move |_, (format, block_type, arg, interval, color, underline): (String, String, Value, u64, Value, Option<bool>)| -> mlua::Result<()> {
370
        eprintln!("WARNING: oxwm.bar.add_block() is deprecated. Please migrate to oxwm.bar.set_blocks() with block constructors.");
371
        eprintln!("See the migration guide for details.");
372
373
        let cmd = match block_type.as_str() {
374
            "DateTime" => {
375
                let fmt = if let Value::String(s) = arg {
376
                    s.to_str()?.to_string()
377
                } else {
378
                    return Err(mlua::Error::RuntimeError("DateTime block requires format string as third argument".into()));
379
                };
380
                crate::bar::BlockCommand::DateTime(fmt)
381
            }
382
            "Shell" => {
383
                let cmd_str = if let Value::String(s) = arg {
384
                    s.to_str()?.to_string()
385
                } else {
386
                    return Err(mlua::Error::RuntimeError("Shell block requires command string as third argument".into()));
387
                };
388
                crate::bar::BlockCommand::Shell(cmd_str)
389
            }
390
            "Ram" => crate::bar::BlockCommand::Ram,
391
            "Static" => {
392
                let text = if let Value::String(s) = arg {
393
                    s.to_str()?.to_string()
394
                } else {
395
                    String::new()
396
                };
397
                crate::bar::BlockCommand::Static(text)
398
            }
399
            "Battery" => {
400
                return Err(mlua::Error::RuntimeError(
401
                    "Battery block is not supported with add_block(). Please use oxwm.bar.set_blocks() with oxwm.bar.block.battery()".into()
402
                ));
403
            }
404
            _ => return Err(mlua::Error::RuntimeError(format!("Unknown block type '{}'", block_type))),
405
        };
406
407
        let color_u32 = parse_color_value(color)?;
408
409
        let block = crate::bar::BlockConfig {
410
            format,
411
            command: cmd,
412
            interval_secs: interval,
413
            color: color_u32,
414
            underline: underline.unwrap_or(false),
415
        };
416
417
        builder_clone.borrow_mut().status_blocks.push(block);
418
        Ok(())
419
    })?;
420
421
    let builder_clone = builder.clone();
422
    let set_blocks = lua.create_function(move |_, blocks: Table| {
423
        use crate::bar::BlockCommand;
424
425
        let mut block_configs = Vec::new();
426
427
        for i in 1..=blocks.len()? {
428
            let block_table: Table = blocks.get(i)?;
429
            let block_type: String = block_table.get("__block_type")?;
430
            let format: String = block_table.get("format").unwrap_or_default();
431
            let interval: u64 = block_table.get("interval")?;
432
            let color_val: Value = block_table.get("color")?;
433
            let underline: bool = block_table.get("underline").unwrap_or(false);
434
            let arg: Option<Value> = block_table.get("__arg").ok();
435
436
            let cmd = match block_type.as_str() {
437
                "DateTime" => {
438
                    let fmt = arg.and_then(|v| {
439
                        if let Value::String(s) = v {
440
                            s.to_str().ok().map(|s| s.to_string())
441
                        } else {
442
                            None
443
                        }
444
                    }).ok_or_else(|| mlua::Error::RuntimeError("DateTime block missing format".into()))?;
445
                    BlockCommand::DateTime(fmt)
446
                }
447
                "Shell" => {
448
                    let cmd_str = arg.and_then(|v| {
449
                        if let Value::String(s) = v {
450
                            s.to_str().ok().map(|s| s.to_string())
451
                        } else {
452
                            None
453
                        }
454
                    }).ok_or_else(|| mlua::Error::RuntimeError("Shell block missing command".into()))?;
455
                    BlockCommand::Shell(cmd_str)
456
                }
457
                "Ram" => BlockCommand::Ram,
458
                "Static" => {
459
                    let text = arg.and_then(|v| {
460
                        if let Value::String(s) = v {
461
                            s.to_str().ok().map(|s| s.to_string())
462
                        } else {
463
                            None
464
                        }
465
                    }).unwrap_or_default();
466
                    BlockCommand::Static(text)
467
                }
468
                "Battery" => {
469
                    let formats = arg.and_then(|v| {
470
                        if let Value::Table(t) = v {
471
                            Some(t)
472
                        } else {
473
                            None
474
                        }
475
                    }).ok_or_else(|| mlua::Error::RuntimeError("Battery block missing formats".into()))?;
476
477
                    let charging: String = formats.get("charging")?;
478
                    let discharging: String = formats.get("discharging")?;
479
                    let full: String = formats.get("full")?;
480
481
                    BlockCommand::Battery {
482
                        format_charging: charging,
483
                        format_discharging: discharging,
484
                        format_full: full,
485
                    }
486
                }
487
                _ => return Err(mlua::Error::RuntimeError(format!("Unknown block type '{}'", block_type))),
488
            };
489
490
            let color_u32 = parse_color_value(color_val)?;
491
492
            let block = crate::bar::BlockConfig {
493
                format,
494
                command: cmd,
495
                interval_secs: interval,
496
                color: color_u32,
497
                underline,
498
            };
499
500
            block_configs.push(block);
501
        }
502
503
        builder_clone.borrow_mut().status_blocks = block_configs;
504
        Ok(())
505
    })?;
506
507
    let builder_clone = builder.clone();
508
    let set_scheme_normal = lua.create_function(move |_, (fg, bg, ul): (Value, Value, Value)| {
509
        let foreground = parse_color_value(fg)?;
510
        let background = parse_color_value(bg)?;
511
        let underline = parse_color_value(ul)?;
512
513
        builder_clone.borrow_mut().scheme_normal = ColorScheme {
514
            foreground,
515
            background,
516
            underline,
517
        };
518
        Ok(())
519
    })?;
520
521
    let builder_clone = builder.clone();
522
    let set_scheme_occupied = lua.create_function(move |_, (fg, bg, ul): (Value, Value, Value)| {
523
        let foreground = parse_color_value(fg)?;
524
        let background = parse_color_value(bg)?;
525
        let underline = parse_color_value(ul)?;
526
527
        builder_clone.borrow_mut().scheme_occupied = ColorScheme {
528
            foreground,
529
            background,
530
            underline,
531
        };
532
        Ok(())
533
    })?;
534
535
    let builder_clone = builder.clone();
536
    let set_scheme_selected = lua.create_function(move |_, (fg, bg, ul): (Value, Value, Value)| {
537
        let foreground = parse_color_value(fg)?;
538
        let background = parse_color_value(bg)?;
539
        let underline = parse_color_value(ul)?;
540
541
        builder_clone.borrow_mut().scheme_selected = ColorScheme {
542
            foreground,
543
            background,
544
            underline,
545
        };
546
        Ok(())
547
    })?;
548
549
    bar_table.set("set_font", set_font)?;
550
    bar_table.set("block", block_table)?;
551
    bar_table.set("add_block", add_block)?;  // Deprecated, for backwards compatibility
552
    bar_table.set("set_blocks", set_blocks)?;
553
    bar_table.set("set_scheme_normal", set_scheme_normal)?;
554
    bar_table.set("set_scheme_occupied", set_scheme_occupied)?;
555
    bar_table.set("set_scheme_selected", set_scheme_selected)?;
556
    parent.set("bar", bar_table)?;
557
    Ok(())
558
}
559
560
fn register_misc(lua: &Lua, parent: &Table, builder: SharedBuilder) -> Result<(), ConfigError> {
561
    let builder_clone = builder.clone();
562
    let set_terminal = lua.create_function(move |_, term: String| {
563
        builder_clone.borrow_mut().terminal = term;
564
        Ok(())
565
    })?;
566
567
    let builder_clone = builder.clone();
568
    let set_modkey = lua.create_function(move |_, modkey_str: String| {
569
        let modkey = parse_modkey_string(&modkey_str)
570
            .map_err(|e| mlua::Error::RuntimeError(format!("{}", e)))?;
571
        builder_clone.borrow_mut().modkey = modkey;
572
        Ok(())
573
    })?;
574
575
    let builder_clone = builder.clone();
576
    let set_tags = lua.create_function(move |_, tags: Vec<String>| {
577
        builder_clone.borrow_mut().tags = tags;
578
        Ok(())
579
    })?;
580
581
    let quit = lua.create_function(|lua, ()| {
582
        create_action_table(lua, "Quit", Value::Nil)
583
    })?;
584
585
    let restart = lua.create_function(|lua, ()| {
586
        create_action_table(lua, "Restart", Value::Nil)
587
    })?;
588
589
    let recompile = lua.create_function(|lua, ()| {
590
        create_action_table(lua, "Recompile", Value::Nil)
591
    })?;
592
593
    let toggle_gaps = lua.create_function(|lua, ()| {
594
        create_action_table(lua, "ToggleGaps", Value::Nil)
595
    })?;
596
597
    let show_keybinds = lua.create_function(|lua, ()| {
598
        create_action_table(lua, "ShowKeybindOverlay", Value::Nil)
599
    })?;
600
601
    let focus_monitor = lua.create_function(|lua, idx: i32| {
602
        create_action_table(lua, "FocusMonitor", Value::Integer(idx as i64))
603
    })?;
604
605
    let builder_clone = builder.clone();
606
    let set_layout_symbol = lua.create_function(move |_, (name, symbol): (String, String)| {
607
        builder_clone.borrow_mut().layout_symbols.push(crate::LayoutSymbolOverride {
608
            name,
609
            symbol,
610
        });
611
        Ok(())
612
    })?;
613
614
    let builder_clone = builder.clone();
615
    let autostart = lua.create_function(move |_, cmd: String| {
616
        builder_clone.borrow_mut().autostart.push(cmd);
617
        Ok(())
618
    })?;
619
620
    parent.set("set_terminal", set_terminal)?;
621
    parent.set("set_modkey", set_modkey)?;
622
    parent.set("set_tags", set_tags)?;
623
    parent.set("set_layout_symbol", set_layout_symbol)?;
624
    parent.set("autostart", autostart)?;
625
    parent.set("quit", quit)?;
626
    parent.set("restart", restart)?;
627
    parent.set("recompile", recompile)?;
628
    parent.set("toggle_gaps", toggle_gaps)?;
629
    parent.set("show_keybinds", show_keybinds)?;
630
    parent.set("focus_monitor", focus_monitor)?;
631
    Ok(())
632
}
633
634
fn parse_modifiers_value(_lua: &Lua, value: Value) -> mlua::Result<Vec<KeyButMask>> {
635
    match value {
636
        Value::Table(t) => {
637
            let mut mods = Vec::new();
638
            for i in 1..=t.len()? {
639
                let mod_str: String = t.get(i)?;
640
                let mask = parse_modkey_string(&mod_str)
641
                    .map_err(|e| mlua::Error::RuntimeError(format!("oxwm.key.bind: invalid modifier - {}", e)))?;
642
                mods.push(mask);
643
            }
644
            Ok(mods)
645
        }
646
        Value::String(s) => {
647
            let s_str = s.to_str()?;
648
            let mask = parse_modkey_string(&s_str)
649
                .map_err(|e| mlua::Error::RuntimeError(format!("oxwm.key.bind: invalid modifier - {}", e)))?;
650
            Ok(vec![mask])
651
        }
652
        _ => Err(mlua::Error::RuntimeError(
653
            "oxwm.key.bind: first argument must be a table of modifiers like {\"Mod4\"} or {\"Mod4\", \"Shift\"}".into(),
654
        )),
655
    }
656
}
657
658
fn parse_modkey_string(s: &str) -> Result<KeyButMask, ConfigError> {
659
    match s {
660
        "Mod1" => Ok(KeyButMask::MOD1),
661
        "Mod2" => Ok(KeyButMask::MOD2),
662
        "Mod3" => Ok(KeyButMask::MOD3),
663
        "Mod4" => Ok(KeyButMask::MOD4),
664
        "Mod5" => Ok(KeyButMask::MOD5),
665
        "Shift" => Ok(KeyButMask::SHIFT),
666
        "Control" => Ok(KeyButMask::CONTROL),
667
        _ => Err(ConfigError::InvalidModkey(format!("'{}' is not a valid modifier. Use one of: Mod1, Mod4, Shift, Control", s))),
668
    }
669
}
670
671
fn parse_keysym(key: &str) -> mlua::Result<Keysym> {
672
    keysyms::keysym_from_str(key)
673
        .ok_or_else(|| mlua::Error::RuntimeError(format!("unknown key '{}'. valid keys include: Return, Space, A-Z, 0-9, F1-F12, Left, Right, Up, Down, etc. check oxwm.lua type definitions for the complete list", key)))
674
}
675
676
fn parse_action_value(_lua: &Lua, value: Value) -> mlua::Result<(KeyAction, Arg)> {
677
    match value {
678
        Value::Function(_) => {
679
            Err(mlua::Error::RuntimeError(
680
                "action must be a function call, not a function reference. did you forget ()? example: oxwm.spawn('st') not oxwm.spawn".into()
681
            ))
682
        }
683
        Value::Table(t) => {
684
            if let Ok(action_name) = t.get::<String>("__action") {
685
                let action = string_to_action(&action_name)?;
686
                let arg = if let Ok(arg_val) = t.get::<Value>("__arg") {
687
                    value_to_arg(arg_val)?
688
                } else {
689
                    Arg::None
690
                };
691
                return Ok((action, arg));
692
            }
693
694
            Err(mlua::Error::RuntimeError(
695
                "action must be a table returned by oxwm functions like oxwm.spawn(), oxwm.client.kill(), oxwm.quit(), etc.".into(),
696
            ))
697
        }
698
        _ => Err(mlua::Error::RuntimeError(
699
            "action must be a table returned by oxwm functions like oxwm.spawn(), oxwm.client.kill(), oxwm.quit(), etc.".into(),
700
        )),
701
    }
702
}
703
704
fn string_to_action(s: &str) -> mlua::Result<KeyAction> {
705
    match s {
706
        "Spawn" => Ok(KeyAction::Spawn),
707
        "SpawnTerminal" => Ok(KeyAction::SpawnTerminal),
708
        "KillClient" => Ok(KeyAction::KillClient),
709
        "FocusStack" => Ok(KeyAction::FocusStack),
710
        "FocusDirection" => Ok(KeyAction::FocusDirection),
711
        "SwapDirection" => Ok(KeyAction::SwapDirection),
712
        "Quit" => Ok(KeyAction::Quit),
713
        "Restart" => Ok(KeyAction::Restart),
714
        "Recompile" => Ok(KeyAction::Recompile),
715
        "ViewTag" => Ok(KeyAction::ViewTag),
716
        "ToggleGaps" => Ok(KeyAction::ToggleGaps),
717
        "ToggleFullScreen" => Ok(KeyAction::ToggleFullScreen),
718
        "ToggleFloating" => Ok(KeyAction::ToggleFloating),
719
        "ChangeLayout" => Ok(KeyAction::ChangeLayout),
720
        "CycleLayout" => Ok(KeyAction::CycleLayout),
721
        "MoveToTag" => Ok(KeyAction::MoveToTag),
722
        "FocusMonitor" => Ok(KeyAction::FocusMonitor),
723
        "SmartMoveWin" => Ok(KeyAction::SmartMoveWin),
724
        "ExchangeClient" => Ok(KeyAction::ExchangeClient),
725
        "ShowKeybindOverlay" => Ok(KeyAction::ShowKeybindOverlay),
726
        _ => Err(mlua::Error::RuntimeError(format!("unknown action '{}'. this is an internal error, please report it", s))),
727
    }
728
}
729
730
fn value_to_arg(value: Value) -> mlua::Result<Arg> {
731
    match value {
732
        Value::Nil => Ok(Arg::None),
733
        Value::String(s) => Ok(Arg::Str(s.to_str()?.to_string())),
734
        Value::Integer(i) => Ok(Arg::Int(i as i32)),
735
        Value::Number(n) => Ok(Arg::Int(n as i32)),
736
        Value::Table(t) => {
737
            let mut arr = Vec::new();
738
            for i in 1..=t.len()? {
739
                let item: String = t.get(i)?;
740
                arr.push(item);
741
            }
742
            Ok(Arg::Array(arr))
743
        }
744
        _ => Ok(Arg::None),
745
    }
746
}
747
748
fn create_action_table(lua: &Lua, action_name: &str, arg: Value) -> mlua::Result<Table> {
749
    let table = lua.create_table()?;
750
    table.set("__action", action_name)?;
751
    table.set("__arg", arg)?;
752
    Ok(table)
753
}
754
755
fn direction_string_to_int(dir: &str) -> mlua::Result<i64> {
756
    match dir {
757
        "up" => Ok(0),
758
        "down" => Ok(1),
759
        "left" => Ok(2),
760
        "right" => Ok(3),
761
        _ => Err(mlua::Error::RuntimeError(
762
            format!("invalid direction '{}'. must be one of: up, down, left, right", dir)
763
        )),
764
    }
765
}
766
767
fn parse_color_value(value: Value) -> mlua::Result<u32> {
768
    match value {
769
        Value::Integer(i) => Ok(i as u32),
770
        Value::Number(n) => Ok(n as u32),
771
        Value::String(s) => {
772
            let s = s.to_str()?;
773
            if s.starts_with('#') {
774
                u32::from_str_radix(&s[1..], 16)
775
                    .map_err(|e| mlua::Error::RuntimeError(format!("invalid hex color '{}': {}. use format like #ff0000 or 0xff0000", s, e)))
776
            } else if s.starts_with("0x") {
777
                u32::from_str_radix(&s[2..], 16)
778
                    .map_err(|e| mlua::Error::RuntimeError(format!("invalid hex color '{}': {}. use format like 0xff0000 or #ff0000", s, e)))
779
            } else {
780
                s.parse::<u32>()
781
                    .map_err(|e| mlua::Error::RuntimeError(format!("invalid color '{}': {}. use hex format like 0xff0000 or #ff0000", s, e)))
782
            }
783
        }
784
        _ => Err(mlua::Error::RuntimeError(
785
            "color must be a number (0xff0000) or string ('#ff0000' or '0xff0000')".into(),
786
        )),
787
    }
788
}
789
790
fn create_block_config(lua: &Lua, config: Table, block_type: &str, arg: Option<Value>) -> mlua::Result<Table> {
791
    let table = lua.create_table()?;
792
    table.set("__block_type", block_type)?;
793
794
    let format: String = config.get("format").unwrap_or_default();
795
    let interval: u64 = config.get("interval")?;
796
    let color: Value = config.get("color")?;
797
    let underline: bool = config.get("underline").unwrap_or(false);
798
799
    table.set("format", format)?;
800
    table.set("interval", interval)?;
801
    table.set("color", color)?;
802
    table.set("underline", underline)?;
803
804
    if let Some(arg_val) = arg {
805
        table.set("__arg", arg_val)?;
806
    }
807
808
    Ok(table)
809
}