oxwm

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