oxwm

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