oxwm

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