oxwm

https://git.tonybtw.com/oxwm.git git://git.tonybtw.com/oxwm.git
21,572 bytes raw
1
use crate::bar::{BlockCommand, BlockConfig};
2
use crate::errors::ConfigError;
3
use crate::keyboard::handlers::{KeyBinding, KeyPress};
4
use crate::keyboard::keysyms::{self, Keysym};
5
use crate::keyboard::{Arg, KeyAction};
6
use crate::{ColorScheme, LayoutSymbolOverride};
7
use mlua::{Lua, Table, Value};
8
use x11rb::protocol::xproto::KeyButMask;
9
10
use super::lua_api;
11
12
pub fn parse_lua_config(
13
    input: &str,
14
    config_dir: Option<&std::path::Path>,
15
) -> Result<crate::Config, ConfigError> {
16
    let lua = Lua::new();
17
18
    if let Some(dir) = config_dir {
19
        if let Some(dir_str) = dir.to_str() {
20
            let setup_code = format!("package.path = '{}/?.lua;' .. package.path", dir_str);
21
            lua.load(&setup_code)
22
                .exec()
23
                .map_err(|e| ConfigError::LuaError(format!("Failed to set package.path: {}", e)))?;
24
        }
25
    }
26
27
    let builder = lua_api::register_api(&lua)?;
28
29
    lua.load(input)
30
        .exec()
31
        .map_err(|e| ConfigError::LuaError(format!("{}", e)))?;
32
33
    let builder_data = builder.borrow().clone();
34
35
    return Ok(crate::Config {
36
        border_width: builder_data.border_width,
37
        border_focused: builder_data.border_focused,
38
        border_unfocused: builder_data.border_unfocused,
39
        font: builder_data.font,
40
        gaps_enabled: builder_data.gaps_enabled,
41
        gap_inner_horizontal: builder_data.gap_inner_horizontal,
42
        gap_inner_vertical: builder_data.gap_inner_vertical,
43
        gap_outer_horizontal: builder_data.gap_outer_horizontal,
44
        gap_outer_vertical: builder_data.gap_outer_vertical,
45
        terminal: builder_data.terminal,
46
        modkey: builder_data.modkey,
47
        tags: builder_data.tags,
48
        layout_symbols: builder_data.layout_symbols,
49
        keybindings: builder_data.keybindings,
50
        status_blocks: builder_data.status_blocks,
51
        scheme_normal: builder_data.scheme_normal,
52
        scheme_occupied: builder_data.scheme_occupied,
53
        scheme_selected: builder_data.scheme_selected,
54
        autostart: builder_data.autostart,
55
    });
56
57
    #[allow(unreachable_code)]
58
    {
59
    let config: Table = lua
60
        .load(input)
61
        .eval()
62
        .map_err(|e| ConfigError::LuaError(format!("{}", e)))?;
63
    let border_width: u32 = get_table_field(&config, "border_width")?;
64
    let border_focused: u32 = parse_color(&config, "border_focused")?;
65
    let border_unfocused: u32 = parse_color(&config, "border_unfocused")?;
66
    let font: String = get_table_field(&config, "font")?;
67
68
    let gaps_enabled: bool = get_table_field(&config, "gaps_enabled")?;
69
    let gap_inner_horizontal: u32 = get_table_field(&config, "gap_inner_horizontal")?;
70
    let gap_inner_vertical: u32 = get_table_field(&config, "gap_inner_vertical")?;
71
    let gap_outer_horizontal: u32 = get_table_field(&config, "gap_outer_horizontal")?;
72
    let gap_outer_vertical: u32 = get_table_field(&config, "gap_outer_vertical")?;
73
74
    let terminal: String = get_table_field(&config, "terminal")?;
75
    let modkey = parse_modkey(&config)?;
76
77
    let tags = parse_tags(&config)?;
78
    let layout_symbols = parse_layout_symbols(&config)?;
79
    let keybindings = parse_keybindings(&config, modkey)?;
80
    let status_blocks = parse_status_blocks(&config)?;
81
82
    let scheme_normal = parse_color_scheme(&config, "scheme_normal")?;
83
    let scheme_occupied = parse_color_scheme(&config, "scheme_occupied")?;
84
    let scheme_selected = parse_color_scheme(&config, "scheme_selected")?;
85
86
    let autostart = parse_autostart(&config)?;
87
88
    Ok(crate::Config {
89
        border_width,
90
        border_focused,
91
        border_unfocused,
92
        font,
93
        gaps_enabled,
94
        gap_inner_horizontal,
95
        gap_inner_vertical,
96
        gap_outer_horizontal,
97
        gap_outer_vertical,
98
        terminal,
99
        modkey,
100
        tags,
101
        layout_symbols,
102
        keybindings,
103
        status_blocks,
104
        scheme_normal,
105
        scheme_occupied,
106
        scheme_selected,
107
        autostart,
108
    })
109
    }
110
}
111
112
fn get_table_field<T>(table: &Table, field: &str) -> Result<T, ConfigError>
113
where
114
    T: mlua::FromLua,
115
{
116
    table
117
        .get::<T>(field)
118
        .map_err(|e| ConfigError::LuaError(format!("Failed to get field '{}': {}", field, e)))
119
}
120
121
fn parse_color(table: &Table, field: &str) -> Result<u32, ConfigError> {
122
    let value: Value = table.get(field).map_err(|e| {
123
        ConfigError::LuaError(format!("Failed to get color field '{}': {}", field, e))
124
    })?;
125
126
    match value {
127
        Value::String(s) => {
128
            let s = s.to_str().map_err(|e| {
129
                ConfigError::LuaError(format!("Invalid UTF-8 in color string: {}", e))
130
            })?;
131
            parse_color_string(&s)
132
        }
133
        Value::Integer(i) => Ok(i as u32),
134
        Value::Number(n) => Ok(n as u32),
135
        _ => Err(ConfigError::LuaError(format!(
136
            "Color field '{}' must be a string or number",
137
            field
138
        ))),
139
    }
140
}
141
142
fn parse_color_string(s: &str) -> Result<u32, ConfigError> {
143
    let s = s.trim();
144
    if s.starts_with('#') {
145
        u32::from_str_radix(&s[1..], 16)
146
            .map_err(|e| ConfigError::LuaError(format!("Invalid hex color '{}': {}", s, e)))
147
    } else if s.starts_with("0x") {
148
        u32::from_str_radix(&s[2..], 16)
149
            .map_err(|e| ConfigError::LuaError(format!("Invalid hex color '{}': {}", s, e)))
150
    } else {
151
        s.parse::<u32>()
152
            .map_err(|e| ConfigError::LuaError(format!("Invalid color '{}': {}", s, e)))
153
    }
154
}
155
156
fn parse_modkey(config: &Table) -> Result<KeyButMask, ConfigError> {
157
    let modkey_str: String = get_table_field(config, "modkey")?;
158
    parse_modkey_string(&modkey_str)
159
}
160
161
fn parse_modkey_string(s: &str) -> Result<KeyButMask, ConfigError> {
162
    match s {
163
        "Mod1" => Ok(KeyButMask::MOD1),
164
        "Mod2" => Ok(KeyButMask::MOD2),
165
        "Mod3" => Ok(KeyButMask::MOD3),
166
        "Mod4" => Ok(KeyButMask::MOD4),
167
        "Mod5" => Ok(KeyButMask::MOD5),
168
        "Shift" => Ok(KeyButMask::SHIFT),
169
        "Control" => Ok(KeyButMask::CONTROL),
170
        _ => Err(ConfigError::InvalidModkey(s.to_string())),
171
    }
172
}
173
174
fn parse_tags(config: &Table) -> Result<Vec<String>, ConfigError> {
175
    let tags_table: Table = get_table_field(config, "tags")?;
176
    let mut tags = Vec::new();
177
178
    for i in 1..=tags_table
179
        .len()
180
        .map_err(|e| ConfigError::LuaError(format!("Failed to get tags length: {}", e)))?
181
    {
182
        let tag: String = tags_table.get(i).map_err(|e| {
183
            ConfigError::LuaError(format!("Failed to get tag at index {}: {}", i, e))
184
        })?;
185
        tags.push(tag);
186
    }
187
188
    Ok(tags)
189
}
190
191
fn parse_layout_symbols(config: &Table) -> Result<Vec<LayoutSymbolOverride>, ConfigError> {
192
    let layout_symbols_result: Result<Table, _> = config.get("layout_symbols");
193
194
    match layout_symbols_result {
195
        Ok(layout_symbols_table) => {
196
            let mut layout_symbols = Vec::new();
197
198
            for i in 1..=layout_symbols_table.len().map_err(|e| {
199
                ConfigError::LuaError(format!("Failed to get layout_symbols length: {}", e))
200
            })? {
201
                let entry: Table = layout_symbols_table.get(i).map_err(|e| {
202
                    ConfigError::LuaError(format!(
203
                        "Failed to get layout_symbol at index {}: {}",
204
                        i, e
205
                    ))
206
                })?;
207
208
                let name: String = get_table_field(&entry, "name")?;
209
                let symbol: String = get_table_field(&entry, "symbol")?;
210
211
                layout_symbols.push(LayoutSymbolOverride { name, symbol });
212
            }
213
214
            Ok(layout_symbols)
215
        }
216
        Err(_) => Ok(Vec::new()),
217
    }
218
}
219
220
fn parse_keybindings(config: &Table, modkey: KeyButMask) -> Result<Vec<KeyBinding>, ConfigError> {
221
    let keybindings_table: Table = get_table_field(config, "keybindings")?;
222
    let mut keybindings = Vec::new();
223
224
    for i in 1..=keybindings_table
225
        .len()
226
        .map_err(|e| ConfigError::LuaError(format!("Failed to get keybindings length: {}", e)))?
227
    {
228
        let kb_table: Table = keybindings_table.get(i).map_err(|e| {
229
            ConfigError::LuaError(format!("Failed to get keybinding at index {}: {}", i, e))
230
        })?;
231
232
        let keys = parse_keypress_list(&kb_table, modkey)?;
233
        let action = parse_key_action(&kb_table)?;
234
        let arg = parse_arg(&kb_table)?;
235
236
        keybindings.push(KeyBinding::new(keys, action, arg));
237
    }
238
239
    Ok(keybindings)
240
}
241
242
fn parse_keypress_list(kb_table: &Table, modkey: KeyButMask) -> Result<Vec<KeyPress>, ConfigError> {
243
    let keys_result: Result<Table, _> = kb_table.get("keys");
244
245
    if let Ok(keys_table) = keys_result {
246
        let mut keys = Vec::new();
247
        for i in 1..=keys_table
248
            .len()
249
            .map_err(|e| ConfigError::LuaError(format!("Failed to get keys length: {}", e)))?
250
        {
251
            let key_entry: Table = keys_table.get(i).map_err(|e| {
252
                ConfigError::LuaError(format!("Failed to get key at index {}: {}", i, e))
253
            })?;
254
255
            let modifiers = parse_modifiers(&key_entry, "modifiers", modkey)?;
256
            let keysym = parse_keysym(&key_entry, "key")?;
257
258
            keys.push(KeyPress { modifiers, keysym });
259
        }
260
        Ok(keys)
261
    } else {
262
        let modifiers = parse_modifiers(kb_table, "modifiers", modkey)?;
263
        let keysym = parse_keysym(kb_table, "key")?;
264
265
        Ok(vec![KeyPress { modifiers, keysym }])
266
    }
267
}
268
269
fn parse_modifiers(
270
    table: &Table,
271
    field: &str,
272
    modkey: KeyButMask,
273
) -> Result<Vec<KeyButMask>, ConfigError> {
274
    let mods_table: Table = get_table_field(table, field)?;
275
    let mut modifiers = Vec::new();
276
277
    for i in 1..=mods_table
278
        .len()
279
        .map_err(|e| ConfigError::LuaError(format!("Failed to get modifiers length: {}", e)))?
280
    {
281
        let mod_str: String = mods_table.get(i).map_err(|e| {
282
            ConfigError::LuaError(format!("Failed to get modifier at index {}: {}", i, e))
283
        })?;
284
285
        let modifier = if mod_str == "Mod" {
286
            modkey
287
        } else {
288
            parse_modkey_string(&mod_str)?
289
        };
290
291
        modifiers.push(modifier);
292
    }
293
294
    Ok(modifiers)
295
}
296
297
fn parse_keysym(table: &Table, field: &str) -> Result<Keysym, ConfigError> {
298
    let key_str: String = get_table_field(table, field)?;
299
    string_to_keysym(&key_str)
300
}
301
302
fn string_to_keysym(s: &str) -> Result<Keysym, ConfigError> {
303
    let keysym = match s {
304
        "Return" => keysyms::XK_RETURN,
305
        "Q" => keysyms::XK_Q,
306
        "Escape" => keysyms::XK_ESCAPE,
307
        "Space" => keysyms::XK_SPACE,
308
        "Tab" => keysyms::XK_TAB,
309
        "Backspace" => keysyms::XK_BACKSPACE,
310
        "Delete" => keysyms::XK_DELETE,
311
        "F1" => keysyms::XK_F1,
312
        "F2" => keysyms::XK_F2,
313
        "F3" => keysyms::XK_F3,
314
        "F4" => keysyms::XK_F4,
315
        "F5" => keysyms::XK_F5,
316
        "F6" => keysyms::XK_F6,
317
        "F7" => keysyms::XK_F7,
318
        "F8" => keysyms::XK_F8,
319
        "F9" => keysyms::XK_F9,
320
        "F10" => keysyms::XK_F10,
321
        "F11" => keysyms::XK_F11,
322
        "F12" => keysyms::XK_F12,
323
        "A" => keysyms::XK_A,
324
        "B" => keysyms::XK_B,
325
        "C" => keysyms::XK_C,
326
        "D" => keysyms::XK_D,
327
        "E" => keysyms::XK_E,
328
        "F" => keysyms::XK_F,
329
        "G" => keysyms::XK_G,
330
        "H" => keysyms::XK_H,
331
        "I" => keysyms::XK_I,
332
        "J" => keysyms::XK_J,
333
        "K" => keysyms::XK_K,
334
        "L" => keysyms::XK_L,
335
        "M" => keysyms::XK_M,
336
        "N" => keysyms::XK_N,
337
        "O" => keysyms::XK_O,
338
        "P" => keysyms::XK_P,
339
        "R" => keysyms::XK_R,
340
        "S" => keysyms::XK_S,
341
        "T" => keysyms::XK_T,
342
        "U" => keysyms::XK_U,
343
        "V" => keysyms::XK_V,
344
        "W" => keysyms::XK_W,
345
        "X" => keysyms::XK_X,
346
        "Y" => keysyms::XK_Y,
347
        "Z" => keysyms::XK_Z,
348
        "0" => keysyms::XK_0,
349
        "1" => keysyms::XK_1,
350
        "2" => keysyms::XK_2,
351
        "3" => keysyms::XK_3,
352
        "4" => keysyms::XK_4,
353
        "5" => keysyms::XK_5,
354
        "6" => keysyms::XK_6,
355
        "7" => keysyms::XK_7,
356
        "8" => keysyms::XK_8,
357
        "9" => keysyms::XK_9,
358
        "Left" => keysyms::XK_LEFT,
359
        "Right" => keysyms::XK_RIGHT,
360
        "Up" => keysyms::XK_UP,
361
        "Down" => keysyms::XK_DOWN,
362
        "Home" => keysyms::XK_HOME,
363
        "End" => keysyms::XK_END,
364
        "PageUp" => keysyms::XK_PAGE_UP,
365
        "PageDown" => keysyms::XK_PAGE_DOWN,
366
        "Insert" => keysyms::XK_INSERT,
367
        "Minus" => keysyms::XK_MINUS,
368
        "Equal" => keysyms::XK_EQUAL,
369
        "BracketLeft" => keysyms::XK_LEFT_BRACKET,
370
        "BracketRight" => keysyms::XK_RIGHT_BRACKET,
371
        "Semicolon" => keysyms::XK_SEMICOLON,
372
        "Apostrophe" => keysyms::XK_APOSTROPHE,
373
        "Grave" => keysyms::XK_GRAVE,
374
        "Backslash" => keysyms::XK_BACKSLASH,
375
        "Comma" => keysyms::XK_COMMA,
376
        "Period" => keysyms::XK_PERIOD,
377
        "Slash" => keysyms::XK_SLASH,
378
        "AudioRaiseVolume" => keysyms::XF86_AUDIO_RAISE_VOLUME,
379
        "AudioLowerVolume" => keysyms::XF86_AUDIO_LOWER_VOLUME,
380
        "AudioMute" => keysyms::XF86_AUDIO_MUTE,
381
        "MonBrightnessUp" => keysyms::XF86_MON_BRIGHTNESS_UP,
382
        "MonBrightnessDown" => keysyms::XF86_MON_BRIGHTNESS_DOWN,
383
        _ => return Err(ConfigError::UnknownKey(s.to_string())),
384
    };
385
386
    Ok(keysym)
387
}
388
389
fn parse_key_action(kb_table: &Table) -> Result<KeyAction, ConfigError> {
390
    let action_str: String = get_table_field(kb_table, "action")?;
391
    string_to_key_action(&action_str)
392
}
393
394
fn string_to_key_action(s: &str) -> Result<KeyAction, ConfigError> {
395
    let action = match s {
396
        "Spawn" => KeyAction::Spawn,
397
        "SpawnTerminal" => KeyAction::SpawnTerminal,
398
        "KillClient" => KeyAction::KillClient,
399
        "FocusStack" => KeyAction::FocusStack,
400
        "FocusDirection" => KeyAction::FocusDirection,
401
        "SwapDirection" => KeyAction::SwapDirection,
402
        "Quit" => KeyAction::Quit,
403
        "Restart" => KeyAction::Restart,
404
        "Recompile" => KeyAction::Recompile,
405
        "ViewTag" => KeyAction::ViewTag,
406
        "ToggleGaps" => KeyAction::ToggleGaps,
407
        "ToggleFullScreen" => KeyAction::ToggleFullScreen,
408
        "ToggleFloating" => KeyAction::ToggleFloating,
409
        "ChangeLayout" => KeyAction::ChangeLayout,
410
        "CycleLayout" => KeyAction::CycleLayout,
411
        "MoveToTag" => KeyAction::MoveToTag,
412
        "FocusMonitor" => KeyAction::FocusMonitor,
413
        "SmartMoveWin" => KeyAction::SmartMoveWin,
414
        "ExchangeClient" => KeyAction::ExchangeClient,
415
        "ShowKeybindOverlay" => KeyAction::ShowKeybindOverlay,
416
        "None" => KeyAction::None,
417
        _ => return Err(ConfigError::UnknownAction(s.to_string())),
418
    };
419
420
    Ok(action)
421
}
422
423
fn parse_arg(kb_table: &Table) -> Result<Arg, ConfigError> {
424
    let arg_result: Result<Value, _> = kb_table.get("arg");
425
426
    match arg_result {
427
        Ok(Value::Nil) | Err(_) => Ok(Arg::None),
428
        Ok(Value::String(s)) => {
429
            let s = s
430
                .to_str()
431
                .map_err(|e| ConfigError::LuaError(format!("Invalid UTF-8 in arg: {}", e)))?;
432
            Ok(Arg::Str(s.to_string()))
433
        }
434
        Ok(Value::Integer(i)) => Ok(Arg::Int(i as i32)),
435
        Ok(Value::Number(n)) => Ok(Arg::Int(n as i32)),
436
        Ok(Value::Table(t)) => {
437
            let mut arr = Vec::new();
438
            for i in 1..=t.len().map_err(|e| {
439
                ConfigError::LuaError(format!("Failed to get arg array length: {}", e))
440
            })? {
441
                let item: String = t.get(i).map_err(|e| {
442
                    ConfigError::LuaError(format!(
443
                        "Failed to get arg array item at index {}: {}",
444
                        i, e
445
                    ))
446
                })?;
447
                arr.push(item);
448
            }
449
            Ok(Arg::Array(arr))
450
        }
451
        Ok(_) => Err(ConfigError::LuaError(
452
            "Arg must be nil, string, number, or array".to_string(),
453
        )),
454
    }
455
}
456
457
fn parse_status_blocks(config: &Table) -> Result<Vec<BlockConfig>, ConfigError> {
458
    let blocks_table: Table = get_table_field(config, "status_blocks")?;
459
    let mut blocks = Vec::new();
460
461
    for i in 1..=blocks_table
462
        .len()
463
        .map_err(|e| ConfigError::LuaError(format!("Failed to get status_blocks length: {}", e)))?
464
    {
465
        let block_table: Table = blocks_table.get(i).map_err(|e| {
466
            ConfigError::LuaError(format!("Failed to get status_block at index {}: {}", i, e))
467
        })?;
468
469
        let format: String = get_table_field(&block_table, "format")?;
470
        let command_str: String = get_table_field(&block_table, "command")?;
471
472
        let interval_secs: u64 = {
473
            let value: Value = block_table.get("interval_secs").map_err(|e| {
474
                ConfigError::LuaError(format!("Failed to get interval_secs: {}", e))
475
            })?;
476
            match value {
477
                Value::Integer(i) => i as u64,
478
                Value::Number(n) => n as u64,
479
                _ => {
480
                    return Err(ConfigError::LuaError(
481
                        "interval_secs must be a number".to_string(),
482
                    ));
483
                }
484
            }
485
        };
486
487
        let color: u32 = parse_color(&block_table, "color")?;
488
        let underline: bool = get_table_field(&block_table, "underline")?;
489
490
        let command = match command_str.as_str() {
491
            "DateTime" => {
492
                let fmt: String = get_table_field(&block_table, "command_arg")?;
493
                BlockCommand::DateTime(fmt)
494
            }
495
            "Shell" => {
496
                let cmd: String = get_table_field(&block_table, "command_arg")?;
497
                BlockCommand::Shell(cmd)
498
            }
499
            "Ram" => BlockCommand::Ram,
500
            "Static" => {
501
                let text_result: Result<String, _> = block_table.get("command_arg");
502
                let text = text_result.unwrap_or_default();
503
                BlockCommand::Static(text)
504
            }
505
            "Battery" => {
506
                let formats_table: Table = get_table_field(&block_table, "battery_formats")?;
507
                let format_charging: String = get_table_field(&formats_table, "charging")?;
508
                let format_discharging: String = get_table_field(&formats_table, "discharging")?;
509
                let format_full: String = get_table_field(&formats_table, "full")?;
510
511
                BlockCommand::Battery {
512
                    format_charging,
513
                    format_discharging,
514
                    format_full,
515
                }
516
            }
517
            _ => return Err(ConfigError::UnknownBlockCommand(command_str)),
518
        };
519
520
        blocks.push(BlockConfig {
521
            format,
522
            command,
523
            interval_secs,
524
            color,
525
            underline,
526
        });
527
    }
528
529
    Ok(blocks)
530
}
531
532
fn parse_color_scheme(config: &Table, field: &str) -> Result<ColorScheme, ConfigError> {
533
    let scheme_table: Table = get_table_field(config, field)?;
534
535
    let foreground = parse_color(&scheme_table, "foreground")?;
536
    let background = parse_color(&scheme_table, "background")?;
537
    let underline = parse_color(&scheme_table, "underline")?;
538
539
    Ok(ColorScheme {
540
        foreground,
541
        background,
542
        underline,
543
    })
544
}
545
546
fn parse_autostart(config: &Table) -> Result<Vec<String>, ConfigError> {
547
    let autostart_result: Result<Table, _> = config.get("autostart");
548
549
    match autostart_result {
550
        Ok(autostart_table) => {
551
            let mut autostart = Vec::new();
552
553
            for i in 1..=autostart_table.len().map_err(|e| {
554
                ConfigError::LuaError(format!("Failed to get autostart length: {}", e))
555
            })? {
556
                let cmd: String = autostart_table.get(i).map_err(|e| {
557
                    ConfigError::LuaError(format!(
558
                        "Failed to get autostart command at index {}: {}",
559
                        i, e
560
                    ))
561
                })?;
562
                autostart.push(cmd);
563
            }
564
565
            Ok(autostart)
566
        }
567
        Err(_) => Ok(Vec::new()),
568
    }
569
}
570
571
#[cfg(test)]
572
mod tests {
573
    use super::*;
574
575
    #[test]
576
    fn test_parse_minimal_lua_config() {
577
        let config_str = r#"
578
return {
579
    border_width = 2,
580
    border_focused = 0x6dade3,
581
    border_unfocused = 0xbbbbbb,
582
    font = "monospace:style=Bold:size=10",
583
584
    gaps_enabled = true,
585
    gap_inner_horizontal = 5,
586
    gap_inner_vertical = 5,
587
    gap_outer_horizontal = 5,
588
    gap_outer_vertical = 5,
589
590
    modkey = "Mod4",
591
    terminal = "st",
592
593
    tags = {"1", "2", "3"},
594
595
    keybindings = {
596
        {modifiers = {"Mod4"}, key = "Return", action = "Spawn", arg = "st"},
597
        {modifiers = {"Mod4"}, key = "Q", action = "KillClient"},
598
    },
599
600
    status_blocks = {
601
        {format = "{}", command = "DateTime", command_arg = "%H:%M", interval_secs = 1, color = 0xffffff, underline = true},
602
    },
603
604
    scheme_normal = {foreground = 0xffffff, background = 0x000000, underline = 0x444444},
605
    scheme_occupied = {foreground = 0xffffff, background = 0x000000, underline = 0x444444},
606
    scheme_selected = {foreground = 0xffffff, background = 0x000000, underline = 0x444444},
607
608
    autostart = {},
609
}
610
"#;
611
612
        let config = parse_lua_config(config_str, None).expect("Failed to parse config");
613
614
        assert_eq!(config.border_width, 2);
615
        assert_eq!(config.border_focused, 0x6dade3);
616
        assert_eq!(config.terminal, "st");
617
        assert_eq!(config.tags.len(), 3);
618
        assert_eq!(config.keybindings.len(), 2);
619
        assert_eq!(config.status_blocks.len(), 1);
620
        assert!(config.gaps_enabled);
621
    }
622
}