oxwm

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