oxwm

https://git.tonybtw.com/oxwm.git git://git.tonybtw.com/oxwm.git
93,807 bytes raw
1
const std = @import("std");
2
const VERSION = "v0.11.2";
3
const display_mod = @import("x11/display.zig");
4
const events = @import("x11/events.zig");
5
const xlib = @import("x11/xlib.zig");
6
const client_mod = @import("client.zig");
7
const monitor_mod = @import("monitor.zig");
8
const tiling = @import("layouts/tiling.zig");
9
const monocle = @import("layouts/monocle.zig");
10
const floating = @import("layouts/floating.zig");
11
const scrolling = @import("layouts/scrolling.zig");
12
const grid = @import("layouts/grid.zig");
13
const animations = @import("animations.zig");
14
const bar_mod = @import("bar/bar.zig");
15
const blocks_mod = @import("bar/blocks/blocks.zig");
16
const config_mod = @import("config/config.zig");
17
const lua = @import("config/lua.zig");
18
const overlay_mod = @import("overlay.zig");
19
20
const Display = display_mod.Display;
21
const Client = client_mod.Client;
22
const Monitor = monitor_mod.Monitor;
23
24
var running: bool = true;
25
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
26
27
var wm_protocols: xlib.Atom = 0;
28
var wm_delete: xlib.Atom = 0;
29
var wm_state: xlib.Atom = 0;
30
var wm_take_focus: xlib.Atom = 0;
31
32
var net_supported: xlib.Atom = 0;
33
var net_wm_name: xlib.Atom = 0;
34
var net_wm_state: xlib.Atom = 0;
35
var net_wm_check: xlib.Atom = 0;
36
var net_wm_state_fullscreen: xlib.Atom = 0;
37
var net_active_window: xlib.Atom = 0;
38
var net_wm_window_type: xlib.Atom = 0;
39
var net_wm_window_type_dialog: xlib.Atom = 0;
40
var net_client_list: xlib.Atom = 0;
41
42
var wm_check_window: xlib.Window = 0;
43
44
var border_color_focused: c_ulong = 0x6dade3;
45
var border_color_unfocused: c_ulong = 0x444444;
46
var border_width: i32 = 2;
47
var gap_outer_v: i32 = 5;
48
var gap_outer_h: i32 = 5;
49
var gap_inner_h: i32 = 5;
50
var gap_inner_v: i32 = 5;
51
52
var tags: [9][]const u8 = .{ "1", "2", "3", "4", "5", "6", "7", "8", "9" };
53
54
var cursor_normal: xlib.Cursor = 0;
55
var cursor_resize: xlib.Cursor = 0;
56
var cursor_move: xlib.Cursor = 0;
57
58
const NormalState: c_long = 1;
59
const WithdrawnState: c_long = 0;
60
const IconicState: c_long = 3;
61
const IsViewable: c_int = 2;
62
const snap_distance: i32 = 32;
63
64
var numlock_mask: c_uint = 0;
65
66
var config: config_mod.Config = undefined;
67
var display_global: ?*Display = null;
68
var config_path_global: ?[]const u8 = null;
69
70
var scroll_animation: animations.Scroll_Animation = .{};
71
var animation_config: animations.Animation_Config = .{ .duration_ms = 150, .easing = .ease_out };
72
73
var chord_keys: [4]config_mod.Key_Press = [_]config_mod.Key_Press{.{}} ** 4;
74
var chord_index: u8 = 0;
75
var chord_timestamp: i64 = 0;
76
const chord_timeout_ms: i64 = 1000;
77
var keyboard_grabbed: bool = false;
78
79
var keybind_overlay: ?*overlay_mod.Keybind_Overlay = null;
80
81
fn print_help() void {
82
    std.debug.print(
83
        \\oxwm - A window manager
84
        \\
85
        \\USAGE:
86
        \\    oxwm [OPTIONS]
87
        \\
88
        \\OPTIONS:
89
        \\    --init              Create default config in ~/.config/oxwm/config.lua
90
        \\    --config <PATH>     Use custom config file
91
        \\    --validate          Validate config file without starting window manager
92
        \\    --version           Print version information
93
        \\    --help              Print this help message
94
        \\
95
        \\CONFIG:
96
        \\    Location: ~/.config/oxwm/config.lua
97
        \\    Edit the config file and use Mod+Shift+R to reload
98
        \\    No compilation needed - instant hot-reload!
99
        \\
100
        \\FIRST RUN:
101
        \\    Run 'oxwm --init' to create a config file
102
        \\    Or just start oxwm and it will create one automatically
103
        \\
104
    , .{});
105
}
106
107
fn get_config_path(allocator: std.mem.Allocator) ![]u8 {
108
    const config_home = std.posix.getenv("XDG_CONFIG_HOME") orelse blk: {
109
        const home = std.posix.getenv("HOME") orelse return error.CouldNotGetHomeDir;
110
        break :blk try std.fs.path.join(allocator, &.{ home, ".config" });
111
    };
112
    defer if (std.posix.getenv("XDG_CONFIG_HOME") == null) allocator.free(config_home);
113
114
    const config_path = try std.fs.path.join(allocator, &.{ config_home, "oxwm", "config.lua" });
115
    return config_path;
116
}
117
118
fn init_config(allocator: std.mem.Allocator) void {
119
    const config_path = get_config_path(allocator) catch return;
120
    defer allocator.free(config_path);
121
122
    const template = @embedFile("templates/config.lua");
123
124
    if (std.fs.path.dirname(config_path)) |dir_path| {
125
        var root = std.fs.openDirAbsolute("/", .{}) catch |err| {
126
            std.debug.print("error: could not open root directory: {}\n", .{err});
127
            return;
128
        };
129
        defer root.close();
130
131
        const relative_path = std.mem.trimLeft(u8, dir_path, "/");
132
        root.makePath(relative_path) catch |err| {
133
            std.debug.print("error: could not create config directory: {}\n", .{err});
134
            return;
135
        };
136
    }
137
138
    const file = std.fs.createFileAbsolute(config_path, .{}) catch |err| {
139
        std.debug.print("error: could not create config file: {}\n", .{err});
140
        return;
141
    };
142
    defer file.close();
143
144
    _ = file.writeAll(template) catch |err| {
145
        std.debug.print("error: could not write config file: {}\n", .{err});
146
        return;
147
    };
148
149
    std.debug.print("Config created at {s}\n", .{config_path});
150
    std.debug.print("Edit the file and reload with Mod+Shift+R\n", .{});
151
    std.debug.print("No compilation needed - changes take effect immediately!\n", .{});
152
}
153
154
fn validate_config(allocator: std.mem.Allocator, config_path: []const u8) !void {
155
    config = config_mod.Config.init(allocator);
156
    defer config.deinit();
157
158
    if (!lua.init(&config)) {
159
        std.debug.print("error: failed to initialize lua\n", .{});
160
        std.process.exit(1);
161
    }
162
    defer lua.deinit();
163
164
    _ = std.fs.cwd().statFile(config_path) catch |err| {
165
        std.debug.print("error: config file not found: {s}\n", .{config_path});
166
        std.debug.print("  {}\n", .{err});
167
        std.process.exit(1);
168
    };
169
170
    if (lua.load_file(config_path)) {
171
        std.debug.print("✓ config valid: {s}\n", .{config_path});
172
        std.process.exit(0);
173
    } else {
174
        std.debug.print("✗ config validation failed\n", .{});
175
        std.process.exit(1);
176
    }
177
}
178
179
pub fn main() !void {
180
    const allocator = gpa.allocator();
181
    defer _ = gpa.deinit();
182
183
    const default_config_path = try get_config_path(allocator);
184
    defer allocator.free(default_config_path);
185
186
    var config_path: []const u8 = default_config_path;
187
    var validate_mode: bool = false;
188
    var args = std.process.args();
189
    _ = args.skip();
190
    while (args.next()) |arg| {
191
        if (std.mem.eql(u8, arg, "-c") or std.mem.eql(u8, arg, "--config")) {
192
            if (args.next()) |path| config_path = path;
193
        } else if (std.mem.eql(u8, arg, "-h") or std.mem.eql(u8, arg, "--help")) {
194
            print_help();
195
            return;
196
        } else if (std.mem.eql(u8, arg, "-v") or std.mem.eql(u8, arg, "--version")) {
197
            std.debug.print("{s}\n", .{VERSION});
198
            return;
199
        } else if (std.mem.eql(u8, arg, "--init")) {
200
            init_config(allocator);
201
            return;
202
        } else if (std.mem.eql(u8, arg, "--validate")) {
203
            validate_mode = true;
204
        }
205
    }
206
207
    if (validate_mode) {
208
        try validate_config(allocator, config_path);
209
        return;
210
    }
211
212
    std.debug.print("oxwm starting\n", .{});
213
214
    config = config_mod.Config.init(allocator);
215
    defer config.deinit();
216
    config_mod.set_config(&config);
217
218
    if (lua.init(&config)) {
219
        const loaded = if (std.fs.cwd().statFile(config_path)) |_|
220
            lua.load_file(config_path)
221
        else |_| blk: {
222
            init_config(allocator);
223
            break :blk lua.load_config();
224
        };
225
226
        if (loaded) {
227
            config_path_global = config_path;
228
            std.debug.print("loaded config from {s}\n", .{config_path});
229
            apply_config_values();
230
        } else {
231
            std.debug.print("no config found, using defaults\n", .{});
232
            initialize_default_config();
233
        }
234
    } else {
235
        std.debug.print("failed to init lua, using defaults\n", .{});
236
        initialize_default_config();
237
    }
238
239
    var display = Display.open() catch |err| {
240
        std.debug.print("failed to open display: {}\n", .{err});
241
        return;
242
    };
243
    defer display.close();
244
245
    display_global = &display;
246
247
    std.debug.print("display opened: screen={d} root=0x{x}\n", .{ display.screen, display.root });
248
    std.debug.print("screen size: {d}x{d}\n", .{ display.screen_width(), display.screen_height() });
249
250
    display.become_window_manager() catch |err| {
251
        std.debug.print("failed to become window manager: {}\n", .{err});
252
        return;
253
    };
254
255
    std.debug.print("successfully became window manager\n", .{});
256
257
    setup_atoms(&display);
258
    setup_cursors(&display);
259
    client_mod.init(allocator);
260
    monitor_mod.init(allocator);
261
    monitor_mod.set_root_window(display.root, display.handle);
262
    tiling.set_display(display.handle);
263
    tiling.set_screen_size(display.screen_width(), display.screen_height());
264
265
    setup_monitors(&display);
266
    setup_bars(allocator, &display);
267
    setup_overlay(allocator, &display);
268
    grab_keybinds(&display);
269
    scan_existing_windows(&display);
270
271
    try run_autostart_commands(allocator, config.autostart.items);
272
    std.debug.print("entering event loop\n", .{});
273
    run_event_loop(&display);
274
275
    bar_mod.destroy_bars(allocator, display.handle);
276
277
    var mon = monitor_mod.monitors;
278
    while (mon) |m| {
279
        const next = m.next;
280
        monitor_mod.destroy(m);
281
        mon = next;
282
    }
283
284
    if (keybind_overlay) |overlay| {
285
        overlay.deinit(allocator);
286
    }
287
288
    lua.deinit();
289
    std.debug.print("oxwm exiting\n", .{});
290
}
291
292
fn setup_atoms(display: *Display) void {
293
    wm_protocols = xlib.XInternAtom(display.handle, "WM_PROTOCOLS", xlib.False);
294
    wm_delete = xlib.XInternAtom(display.handle, "WM_DELETE_WINDOW", xlib.False);
295
    wm_state = xlib.XInternAtom(display.handle, "WM_STATE", xlib.False);
296
    wm_take_focus = xlib.XInternAtom(display.handle, "WM_TAKE_FOCUS", xlib.False);
297
298
    net_active_window = xlib.XInternAtom(display.handle, "_NET_ACTIVE_WINDOW", xlib.False);
299
    net_supported = xlib.XInternAtom(display.handle, "_NET_SUPPORTED", xlib.False);
300
    net_wm_name = xlib.XInternAtom(display.handle, "_NET_WM_NAME", xlib.False);
301
    net_wm_state = xlib.XInternAtom(display.handle, "_NET_WM_STATE", xlib.False);
302
    net_wm_check = xlib.XInternAtom(display.handle, "_NET_SUPPORTING_WM_CHECK", xlib.False);
303
    net_wm_state_fullscreen = xlib.XInternAtom(display.handle, "_NET_WM_STATE_FULLSCREEN", xlib.False);
304
    net_wm_window_type = xlib.XInternAtom(display.handle, "_NET_WM_WINDOW_TYPE", xlib.False);
305
    net_wm_window_type_dialog = xlib.XInternAtom(display.handle, "_NET_WM_WINDOW_TYPE_DIALOG", xlib.False);
306
    net_client_list = xlib.XInternAtom(display.handle, "_NET_CLIENT_LIST", xlib.False);
307
308
    const utf8_string = xlib.XInternAtom(display.handle, "UTF8_STRING", xlib.False);
309
310
    wm_check_window = xlib.XCreateSimpleWindow(display.handle, display.root, 0, 0, 1, 1, 0, 0, 0);
311
    _ = xlib.XChangeProperty(display.handle, wm_check_window, net_wm_check, xlib.XA_WINDOW, 32, xlib.PropModeReplace, @ptrCast(&wm_check_window), 1);
312
    _ = xlib.XChangeProperty(display.handle, wm_check_window, net_wm_name, utf8_string, 8, xlib.PropModeReplace, "oxwm", 6);
313
    _ = xlib.XChangeProperty(display.handle, display.root, net_wm_check, xlib.XA_WINDOW, 32, xlib.PropModeReplace, @ptrCast(&wm_check_window), 1);
314
315
    var net_atoms = [_]xlib.Atom{ net_supported, net_wm_name, net_wm_state, net_wm_check, net_wm_state_fullscreen, net_active_window, net_wm_window_type, net_wm_window_type_dialog, net_client_list };
316
    _ = xlib.XChangeProperty(display.handle, display.root, net_supported, xlib.XA_ATOM, 32, xlib.PropModeReplace, @ptrCast(&net_atoms), net_atoms.len);
317
318
    _ = xlib.XDeleteProperty(display.handle, display.root, net_client_list);
319
320
    std.debug.print("atoms initialized with EWMH support\n", .{});
321
}
322
323
fn setup_cursors(display: *Display) void {
324
    cursor_normal = xlib.XCreateFontCursor(display.handle, xlib.XC_left_ptr);
325
    cursor_resize = xlib.XCreateFontCursor(display.handle, xlib.XC_sizing);
326
    cursor_move = xlib.XCreateFontCursor(display.handle, xlib.XC_fleur);
327
    _ = xlib.XDefineCursor(display.handle, display.root, cursor_normal);
328
}
329
330
fn setup_bars(allocator: std.mem.Allocator, display: *Display) void {
331
    var current_monitor = monitor_mod.monitors;
332
    var last_bar: ?*bar_mod.Bar = null;
333
334
    while (current_monitor) |monitor| {
335
        const bar = bar_mod.Bar.create(allocator, display.handle, display.screen, monitor, config);
336
        if (bar) |created_bar| {
337
            if (tiling.bar_height == 0) {
338
                tiling.set_bar_height(created_bar.height);
339
            }
340
341
            if (config.blocks.items.len > 0) {
342
                for (config.blocks.items) |cfg_block| {
343
                    const block = config_block_to_bar_block(cfg_block);
344
                    created_bar.add_block(block);
345
                }
346
            } else {
347
                created_bar.add_block(blocks_mod.Block.init_ram("", 5, 0x7aa2f7, true));
348
                created_bar.add_block(blocks_mod.Block.init_static(" | ", 0x666666, false));
349
                created_bar.add_block(blocks_mod.Block.init_datetime("", "%H:%M", 1, 0x0db9d7, true));
350
            }
351
352
            if (last_bar) |prev| {
353
                prev.next = created_bar;
354
            } else {
355
                bar_mod.bars = created_bar;
356
            }
357
            last_bar = created_bar;
358
            std.debug.print("bar created for monitor {d}\n", .{monitor.num});
359
        }
360
        current_monitor = monitor.next;
361
    }
362
}
363
364
fn setup_overlay(allocator: std.mem.Allocator, display: *Display) void {
365
    keybind_overlay = overlay_mod.Keybind_Overlay.init(display.handle, display.screen, display.root, config.font, allocator);
366
}
367
368
fn config_block_to_bar_block(cfg: config_mod.Block) blocks_mod.Block {
369
    return switch (cfg.block_type) {
370
        .static => blocks_mod.Block.init_static(cfg.format, cfg.color, cfg.underline),
371
        .datetime => blocks_mod.Block.init_datetime(cfg.format, cfg.datetime_format orelse "%H:%M", cfg.interval, cfg.color, cfg.underline),
372
        .ram => blocks_mod.Block.init_ram(cfg.format, cfg.interval, cfg.color, cfg.underline),
373
        .shell => blocks_mod.Block.init_shell(cfg.format, cfg.command orelse "", cfg.interval, cfg.color, cfg.underline),
374
        .battery => blocks_mod.Block.init_battery(
375
            cfg.format_charging orelse "",
376
            cfg.format_discharging orelse "",
377
            cfg.format_full orelse "",
378
            cfg.battery_name orelse "BAT0",
379
            cfg.interval,
380
            cfg.color,
381
            cfg.underline,
382
        ),
383
        .cpu_temp => blocks_mod.Block.init_cpu_temp(
384
            cfg.format,
385
            cfg.thermal_zone orelse "thermal_zone0",
386
            cfg.interval,
387
            cfg.color,
388
            cfg.underline,
389
        ),
390
    };
391
}
392
393
fn setup_monitors(display: *Display) void {
394
    std.debug.print("checking xinerama...\n", .{});
395
    if (xlib.XineramaIsActive(display.handle) != 0) {
396
        std.debug.print("xinerama is active!\n", .{});
397
        var screen_count: c_int = 0;
398
        const screens = xlib.XineramaQueryScreens(display.handle, &screen_count);
399
400
        if (screen_count > 0 and screens != null) {
401
            var prev_monitor: ?*Monitor = null;
402
            var index: usize = 0;
403
404
            while (index < @as(usize, @intCast(screen_count))) : (index += 1) {
405
                const screen = screens[index];
406
                const mon = monitor_mod.create() orelse continue;
407
408
                mon.num = @intCast(index);
409
                mon.mon_x = screen.x_org;
410
                mon.mon_y = screen.y_org;
411
                mon.mon_w = screen.width;
412
                mon.mon_h = screen.height;
413
                mon.win_x = screen.x_org;
414
                mon.win_y = screen.y_org;
415
                mon.win_w = screen.width;
416
                mon.win_h = screen.height;
417
                mon.lt[0] = &tiling.layout;
418
                mon.lt[1] = &monocle.layout;
419
                mon.lt[2] = &floating.layout;
420
                mon.lt[3] = &scrolling.layout;
421
                mon.lt[4] = &grid.layout;
422
                for (0..10) |i| {
423
                    mon.pertag.ltidxs[i][0] = mon.lt[0];
424
                    mon.pertag.ltidxs[i][1] = mon.lt[1];
425
                    mon.pertag.ltidxs[i][2] = mon.lt[2];
426
                    mon.pertag.ltidxs[i][3] = mon.lt[3];
427
                    mon.pertag.ltidxs[i][4] = mon.lt[4];
428
                }
429
430
                init_monitor_gaps(mon);
431
432
                if (prev_monitor) |prev| {
433
                    prev.next = mon;
434
                } else {
435
                    monitor_mod.monitors = mon;
436
                }
437
                prev_monitor = mon;
438
439
                std.debug.print("monitor {d}: {d}x{d} at ({d},{d})\n", .{ index, mon.mon_w, mon.mon_h, mon.mon_x, mon.mon_y });
440
            }
441
442
            monitor_mod.selected_monitor = monitor_mod.monitors;
443
            _ = xlib.XFree(@ptrCast(screens));
444
            return;
445
        }
446
    } else {
447
        std.debug.print("xinerama not active, using single monitor\n", .{});
448
    }
449
450
    const mon = monitor_mod.create() orelse return;
451
    mon.mon_x = 0;
452
    mon.mon_y = 0;
453
    mon.mon_w = display.screen_width();
454
    mon.mon_h = display.screen_height();
455
    mon.win_x = 0;
456
    mon.win_y = 0;
457
    mon.win_w = display.screen_width();
458
    mon.win_h = display.screen_height();
459
    mon.lt[0] = &tiling.layout;
460
    mon.lt[1] = &monocle.layout;
461
    mon.lt[2] = &floating.layout;
462
    mon.lt[3] = &scrolling.layout;
463
    mon.lt[4] = &grid.layout;
464
    for (0..10) |i| {
465
        mon.pertag.ltidxs[i][0] = mon.lt[0];
466
        mon.pertag.ltidxs[i][1] = mon.lt[1];
467
        mon.pertag.ltidxs[i][2] = mon.lt[2];
468
        mon.pertag.ltidxs[i][3] = mon.lt[3];
469
        mon.pertag.ltidxs[i][4] = mon.lt[4];
470
    }
471
472
    init_monitor_gaps(mon);
473
474
    monitor_mod.monitors = mon;
475
    monitor_mod.selected_monitor = mon;
476
    std.debug.print("monitor created: {d}x{d}\n", .{ mon.mon_w, mon.mon_h });
477
}
478
479
fn apply_config_values() void {
480
    border_color_focused = config.border_focused;
481
    border_color_unfocused = config.border_unfocused;
482
    border_width = config.border_width;
483
    gap_inner_h = config.gap_inner_h;
484
    gap_inner_v = config.gap_inner_v;
485
    gap_outer_h = config.gap_outer_h;
486
    gap_outer_v = config.gap_outer_v;
487
    tags = config.tags;
488
}
489
490
fn init_monitor_gaps(mon: *Monitor) void {
491
    const any_gap_nonzero = config.gap_inner_h != 0 or config.gap_inner_v != 0 or
492
                            config.gap_outer_h != 0 or config.gap_outer_v != 0;
493
494
    if (config.gaps_enabled and any_gap_nonzero) {
495
        mon.gap_inner_h = config.gap_inner_h;
496
        mon.gap_inner_v = config.gap_inner_v;
497
        mon.gap_outer_h = config.gap_outer_h;
498
        mon.gap_outer_v = config.gap_outer_v;
499
    } else {
500
        mon.gap_inner_h = 0;
501
        mon.gap_inner_v = 0;
502
        mon.gap_outer_h = 0;
503
        mon.gap_outer_v = 0;
504
    }
505
}
506
507
fn make_keybind(mod: u32, key: u64, action: config_mod.Action) config_mod.Keybind {
508
    var kb: config_mod.Keybind = .{ .action = action };
509
    kb.keys[0] = .{ .mod_mask = mod, .keysym = key };
510
    kb.key_count = 1;
511
    return kb;
512
}
513
514
fn make_keybind_int(mod: u32, key: u64, action: config_mod.Action, int_arg: i32) config_mod.Keybind {
515
    var kb = make_keybind(mod, key, action);
516
    kb.int_arg = int_arg;
517
    return kb;
518
}
519
520
fn make_keybind_str(mod: u32, key: u64, action: config_mod.Action, str_arg: []const u8) config_mod.Keybind {
521
    var kb = make_keybind(mod, key, action);
522
    kb.str_arg = str_arg;
523
    return kb;
524
}
525
526
fn initialize_default_config() void {
527
    const mod_key: u32 = 1 << 6;
528
    const shift_key: u32 = 1 << 0;
529
    const control_key: u32 = 1 << 2;
530
531
    config.add_keybind(make_keybind(mod_key, 0xff0d, .spawn_terminal)) catch {};
532
    config.add_keybind(make_keybind_str(mod_key, 'd', .spawn, "rofi -show drun")) catch {};
533
    config.add_keybind(make_keybind_str(mod_key, 's', .spawn, "maim -s | xclip -selection clipboard -t image/png")) catch {};
534
    config.add_keybind(make_keybind(mod_key, 'q', .kill_client)) catch {};
535
    config.add_keybind(make_keybind(mod_key | shift_key, 'q', .quit)) catch {};
536
    config.add_keybind(make_keybind(mod_key | shift_key, 'r', .reload_config)) catch {};
537
    config.add_keybind(make_keybind(mod_key, 'j', .focus_next)) catch {};
538
    config.add_keybind(make_keybind(mod_key, 'k', .focus_prev)) catch {};
539
    config.add_keybind(make_keybind(mod_key | shift_key, 'j', .move_next)) catch {};
540
    config.add_keybind(make_keybind(mod_key | shift_key, 'k', .move_prev)) catch {};
541
    config.add_keybind(make_keybind_int(mod_key, 'h', .resize_master, -50)) catch {};
542
    config.add_keybind(make_keybind_int(mod_key, 'l', .resize_master, 50)) catch {};
543
    config.add_keybind(make_keybind(mod_key, 'i', .inc_master)) catch {};
544
    config.add_keybind(make_keybind(mod_key, 'p', .dec_master)) catch {};
545
    config.add_keybind(make_keybind(mod_key, 'a', .toggle_gaps)) catch {};
546
    config.add_keybind(make_keybind(mod_key, 'f', .toggle_fullscreen)) catch {};
547
    config.add_keybind(make_keybind(mod_key, 0x0020, .toggle_floating)) catch {};
548
    config.add_keybind(make_keybind(mod_key, 'n', .cycle_layout)) catch {};
549
    config.add_keybind(make_keybind_int(mod_key, 0x002c, .focus_monitor, -1)) catch {};
550
    config.add_keybind(make_keybind_int(mod_key, 0x002e, .focus_monitor, 1)) catch {};
551
    config.add_keybind(make_keybind_int(mod_key | shift_key, 0x002c, .send_to_monitor, -1)) catch {};
552
    config.add_keybind(make_keybind_int(mod_key | shift_key, 0x002e, .send_to_monitor, 1)) catch {};
553
554
    var tag_index: i32 = 0;
555
    while (tag_index < 9) : (tag_index += 1) {
556
        const keysym: u64 = @as(u64, '1') + @as(u64, @intCast(tag_index));
557
        config.add_keybind(make_keybind_int(mod_key, keysym, .view_tag, tag_index)) catch {};
558
        config.add_keybind(make_keybind_int(mod_key | shift_key, keysym, .move_to_tag, tag_index)) catch {};
559
        config.add_keybind(make_keybind_int(mod_key | control_key, keysym, .toggle_view_tag, tag_index)) catch {};
560
        config.add_keybind(make_keybind_int(mod_key | control_key | shift_key, keysym, .toggle_tag, tag_index)) catch {};
561
    }
562
563
    config.add_button(.{ .click = .client_win, .mod_mask = mod_key, .button = 1, .action = .move_mouse }) catch {};
564
    config.add_button(.{ .click = .client_win, .mod_mask = mod_key, .button = 3, .action = .resize_mouse }) catch {};
565
}
566
567
fn grab_keybinds(display: *Display) void {
568
    update_numlock_mask(display);
569
    const modifiers = [_]c_uint{ 0, xlib.LockMask, numlock_mask, numlock_mask | xlib.LockMask };
570
571
    _ = xlib.XUngrabKey(display.handle, xlib.AnyKey, xlib.AnyModifier, display.root);
572
573
    for (config.keybinds.items) |keybind| {
574
        if (keybind.key_count == 0) continue;
575
        const first_key = keybind.keys[0];
576
        const keycode = xlib.XKeysymToKeycode(display.handle, @intCast(first_key.keysym));
577
        if (keycode != 0) {
578
            for (modifiers) |modifier| {
579
                _ = xlib.XGrabKey(
580
                    display.handle,
581
                    keycode,
582
                    first_key.mod_mask | modifier,
583
                    display.root,
584
                    xlib.True,
585
                    xlib.GrabModeAsync,
586
                    xlib.GrabModeAsync,
587
                );
588
            }
589
        }
590
    }
591
592
    for (config.buttons.items) |button| {
593
        if (button.click == .client_win) {
594
            for (modifiers) |modifier| {
595
                _ = xlib.XGrabButton(
596
                    display.handle,
597
                    @intCast(button.button),
598
                    button.mod_mask | modifier,
599
                    display.root,
600
                    xlib.True,
601
                    xlib.ButtonPressMask | xlib.ButtonReleaseMask | xlib.PointerMotionMask,
602
                    xlib.GrabModeAsync,
603
                    xlib.GrabModeAsync,
604
                    xlib.None,
605
                    xlib.None,
606
                );
607
            }
608
        }
609
    }
610
611
    std.debug.print("grabbed {d} keybinds from config\n", .{config.keybinds.items.len});
612
}
613
614
fn get_state(display: *Display, window: xlib.Window) c_long {
615
    var actual_type: xlib.Atom = 0;
616
    var actual_format: c_int = 0;
617
    var num_items: c_ulong = 0;
618
    var bytes_after: c_ulong = 0;
619
    var prop: [*c]u8 = null;
620
621
    const result = xlib.XGetWindowProperty(
622
        display.handle,
623
        window,
624
        wm_state,
625
        0,
626
        2,
627
        xlib.False,
628
        wm_state,
629
        &actual_type,
630
        &actual_format,
631
        &num_items,
632
        &bytes_after,
633
        &prop,
634
    );
635
636
    if (result != 0 or actual_type != wm_state or num_items < 1) {
637
        if (prop != null) {
638
            _ = xlib.XFree(prop);
639
        }
640
        return WithdrawnState;
641
    }
642
643
    const state: c_long = @as(*c_long, @ptrCast(@alignCast(prop))).*;
644
    _ = xlib.XFree(prop);
645
    return state;
646
}
647
648
fn scan_existing_windows(display: *Display) void {
649
    var root_return: xlib.Window = undefined;
650
    var parent_return: xlib.Window = undefined;
651
    var children: [*c]xlib.Window = undefined;
652
    var num_children: c_uint = undefined;
653
654
    if (xlib.XQueryTree(display.handle, display.root, &root_return, &parent_return, &children, &num_children) == 0) {
655
        return;
656
    }
657
658
    var index: c_uint = 0;
659
    while (index < num_children) : (index += 1) {
660
        var window_attrs: xlib.XWindowAttributes = undefined;
661
        if (xlib.XGetWindowAttributes(display.handle, children[index], &window_attrs) == 0) {
662
            continue;
663
        }
664
        if (window_attrs.override_redirect != 0) {
665
            continue;
666
        }
667
        var trans: xlib.Window = 0;
668
        if (xlib.XGetTransientForHint(display.handle, children[index], &trans) != 0) {
669
            continue;
670
        }
671
        if (window_attrs.map_state == IsViewable or get_state(display, children[index]) == IconicState) {
672
            manage(display, children[index], &window_attrs);
673
        }
674
    }
675
676
    index = 0;
677
    while (index < num_children) : (index += 1) {
678
        var window_attrs: xlib.XWindowAttributes = undefined;
679
        if (xlib.XGetWindowAttributes(display.handle, children[index], &window_attrs) == 0) {
680
            continue;
681
        }
682
        var trans: xlib.Window = 0;
683
        if (xlib.XGetTransientForHint(display.handle, children[index], &trans) != 0) {
684
            if (window_attrs.map_state == IsViewable or get_state(display, children[index]) == IconicState) {
685
                manage(display, children[index], &window_attrs);
686
            }
687
        }
688
    }
689
690
    if (children != null) {
691
        _ = xlib.XFree(@ptrCast(children));
692
    }
693
}
694
695
fn run_event_loop(display: *Display) void {
696
    const x11_fd = xlib.XConnectionNumber(display.handle);
697
    var fds = [_]std.posix.pollfd{
698
        .{ .fd = x11_fd, .events = std.posix.POLL.IN, .revents = 0 },
699
    };
700
701
    _ = xlib.XSync(display.handle, xlib.False);
702
703
    while (running) {
704
        while (xlib.XPending(display.handle) > 0) {
705
            var event = display.next_event();
706
            handle_event(display, &event);
707
        }
708
709
        tick_animations();
710
711
        var current_bar = bar_mod.bars;
712
        while (current_bar) |bar| {
713
            bar.update_blocks();
714
            bar.draw(display.handle, &tags);
715
            current_bar = bar.next;
716
        }
717
718
        const poll_timeout: i32 = if (scroll_animation.is_active()) 16 else 1000;
719
        _ = std.posix.poll(&fds, poll_timeout) catch 0;
720
    }
721
}
722
723
fn handle_event(display: *Display, event: *xlib.XEvent) void {
724
    const event_type = events.get_event_type(event);
725
726
    if (event_type == .button_press) {
727
        std.debug.print("EVENT: button_press received type={d}\n", .{event.type});
728
    }
729
730
    switch (event_type) {
731
        .map_request => handle_map_request(display, &event.xmaprequest),
732
        .configure_request => handle_configure_request(display, &event.xconfigurerequest),
733
        .key_press => handle_key_press(display, &event.xkey),
734
        .destroy_notify => handle_destroy_notify(display, &event.xdestroywindow),
735
        .unmap_notify => handle_unmap_notify(display, &event.xunmap),
736
        .enter_notify => handle_enter_notify(display, &event.xcrossing),
737
        .focus_in => handle_focus_in(display, &event.xfocus),
738
        .motion_notify => handle_motion_notify(display, &event.xmotion),
739
        .client_message => handle_client_message(display, &event.xclient),
740
        .button_press => handle_button_press(display, &event.xbutton),
741
        .expose => handle_expose(display, &event.xexpose),
742
        .property_notify => handle_property_notify(display, &event.xproperty),
743
        else => {},
744
    }
745
}
746
747
fn handle_map_request(display: *Display, event: *xlib.XMapRequestEvent) void {
748
    std.debug.print("map_request: window=0x{x}\n", .{event.window});
749
750
    var window_attributes: xlib.XWindowAttributes = undefined;
751
    if (xlib.XGetWindowAttributes(display.handle, event.window, &window_attributes) == 0) {
752
        return;
753
    }
754
    if (window_attributes.override_redirect != 0) {
755
        return;
756
    }
757
    if (client_mod.window_to_client(event.window) != null) {
758
        return;
759
    }
760
761
    manage(display, event.window, &window_attributes);
762
}
763
764
fn manage(display: *Display, win: xlib.Window, window_attrs: *xlib.XWindowAttributes) void {
765
    const client = client_mod.create(win) orelse return;
766
    var trans: xlib.Window = 0;
767
768
    client.x = window_attrs.x;
769
    client.y = window_attrs.y;
770
    client.width = window_attrs.width;
771
    client.height = window_attrs.height;
772
    client.old_x = window_attrs.x;
773
    client.old_y = window_attrs.y;
774
    client.old_width = window_attrs.width;
775
    client.old_height = window_attrs.height;
776
    client.old_border_width = window_attrs.border_width;
777
    client.border_width = border_width;
778
779
    update_title(display, client);
780
781
    if (xlib.XGetTransientForHint(display.handle, win, &trans) != 0) {
782
        if (client_mod.window_to_client(trans)) |transient_client| {
783
            client.monitor = transient_client.monitor;
784
            client.tags = transient_client.tags;
785
        }
786
    }
787
788
    if (client.monitor == null) {
789
        client.monitor = monitor_mod.selected_monitor;
790
        apply_rules(display, client);
791
    }
792
793
    const monitor = client.monitor orelse return;
794
795
    if (client.x + client.width > monitor.win_x + monitor.win_w) {
796
        client.x = monitor.win_x + monitor.win_w - client.width - 2 * client.border_width;
797
    }
798
    if (client.y + client.height > monitor.win_y + monitor.win_h) {
799
        client.y = monitor.win_y + monitor.win_h - client.height - 2 * client.border_width;
800
    }
801
    client.x = @max(client.x, monitor.win_x);
802
    client.y = @max(client.y, monitor.win_y);
803
804
    _ = xlib.XSetWindowBorderWidth(display.handle, win, @intCast(client.border_width));
805
    _ = xlib.XSetWindowBorder(display.handle, win, border_color_unfocused);
806
    tiling.send_configure(client);
807
808
    update_window_type(display, client);
809
    update_size_hints(display, client);
810
    update_wm_hints(display, client);
811
812
    _ = xlib.XSelectInput(
813
        display.handle,
814
        win,
815
        xlib.EnterWindowMask | xlib.FocusChangeMask | xlib.PropertyChangeMask | xlib.StructureNotifyMask,
816
    );
817
    grabbuttons(display, client, false);
818
819
    if (!client.is_floating) {
820
        client.is_floating = trans != 0 or client.is_fixed;
821
        client.old_state = client.is_floating;
822
    }
823
    if (client.is_floating) {
824
        _ = xlib.XRaiseWindow(display.handle, client.window);
825
    }
826
827
    client_mod.attach_aside(client);
828
    client_mod.attach_stack(client);
829
830
    _ = xlib.XChangeProperty(display.handle, display.root, net_client_list, xlib.XA_WINDOW, 32, xlib.PropModeAppend, @ptrCast(&client.window), 1);
831
    _ = xlib.XMoveResizeWindow(display.handle, client.window, client.x + 2 * display.screen_width(), client.y, @intCast(client.width), @intCast(client.height));
832
    set_client_state(display, client, NormalState);
833
834
    if (client.monitor == monitor_mod.selected_monitor) {
835
        const selmon = monitor_mod.selected_monitor orelse return;
836
        unfocus_client(display, selmon.sel, false);
837
    }
838
    monitor.sel = client;
839
840
    if (is_scrolling_layout(monitor)) {
841
        monitor.scroll_offset = 0;
842
    }
843
844
    arrange(monitor);
845
    _ = xlib.XMapWindow(display.handle, win);
846
    focus(display, null);
847
}
848
849
fn handle_configure_request(display: *Display, event: *xlib.XConfigureRequestEvent) void {
850
    const client = client_mod.window_to_client(event.window);
851
852
    if (client) |managed_client| {
853
        if ((event.value_mask & xlib.c.CWBorderWidth) != 0) {
854
            managed_client.border_width = event.border_width;
855
        } else if (managed_client.is_floating or (managed_client.monitor != null and managed_client.monitor.?.lt[managed_client.monitor.?.sel_lt] == null)) {
856
            const monitor = managed_client.monitor orelse return;
857
            if ((event.value_mask & xlib.c.CWX) != 0) {
858
                managed_client.old_x = managed_client.x;
859
                managed_client.x = monitor.mon_x + event.x;
860
            }
861
            if ((event.value_mask & xlib.c.CWY) != 0) {
862
                managed_client.old_y = managed_client.y;
863
                managed_client.y = monitor.mon_y + event.y;
864
            }
865
            if ((event.value_mask & xlib.c.CWWidth) != 0) {
866
                managed_client.old_width = managed_client.width;
867
                managed_client.width = event.width;
868
            }
869
            if ((event.value_mask & xlib.c.CWHeight) != 0) {
870
                managed_client.old_height = managed_client.height;
871
                managed_client.height = event.height;
872
            }
873
            const client_full_width = managed_client.width + managed_client.border_width * 2;
874
            const client_full_height = managed_client.height + managed_client.border_width * 2;
875
            if ((managed_client.x + managed_client.width) > monitor.mon_x + monitor.mon_w and managed_client.is_floating) {
876
                managed_client.x = monitor.mon_x + @divTrunc(monitor.mon_w, 2) - @divTrunc(client_full_width, 2);
877
            }
878
            if ((managed_client.y + managed_client.height) > monitor.mon_y + monitor.mon_h and managed_client.is_floating) {
879
                managed_client.y = monitor.mon_y + @divTrunc(monitor.mon_h, 2) - @divTrunc(client_full_height, 2);
880
            }
881
            if (((event.value_mask & (xlib.c.CWX | xlib.c.CWY)) != 0) and ((event.value_mask & (xlib.c.CWWidth | xlib.c.CWHeight)) == 0)) {
882
                tiling.send_configure(managed_client);
883
            }
884
            if (client_mod.is_visible(managed_client)) {
885
                _ = xlib.XMoveResizeWindow(display.handle, managed_client.window, managed_client.x, managed_client.y, @intCast(managed_client.width), @intCast(managed_client.height));
886
            }
887
        } else {
888
            tiling.send_configure(managed_client);
889
        }
890
    } else {
891
        var changes: xlib.XWindowChanges = undefined;
892
        changes.x = event.x;
893
        changes.y = event.y;
894
        changes.width = event.width;
895
        changes.height = event.height;
896
        changes.border_width = event.border_width;
897
        changes.sibling = event.above;
898
        changes.stack_mode = event.detail;
899
        _ = xlib.XConfigureWindow(display.handle, event.window, @intCast(event.value_mask), &changes);
900
    }
901
    _ = xlib.XSync(display.handle, xlib.False);
902
}
903
904
fn reset_chord_state() void {
905
    chord_index = 0;
906
    chord_keys = [_]config_mod.Key_Press{.{}} ** 4;
907
    chord_timestamp = 0;
908
    if (keyboard_grabbed) {
909
        if (display_global) |dsp| {
910
            _ = xlib.XUngrabKeyboard(dsp.handle, xlib.CurrentTime);
911
        }
912
        keyboard_grabbed = false;
913
    }
914
}
915
916
fn handle_key_press(display: *Display, event: *xlib.XKeyEvent) void {
917
    const keysym = xlib.XKeycodeToKeysym(display.handle, @intCast(event.keycode), 0);
918
919
    if (keybind_overlay) |overlay| {
920
        if (overlay.handle_key(keysym)) {
921
            return;
922
        }
923
    }
924
925
    const clean_state = event.state & ~@as(c_uint, xlib.LockMask | xlib.Mod2Mask);
926
    const current_time = std.time.milliTimestamp();
927
928
    if (chord_index > 0 and (current_time - chord_timestamp) > chord_timeout_ms) {
929
        reset_chord_state();
930
    }
931
932
    chord_keys[chord_index] = .{ .mod_mask = clean_state, .keysym = keysym };
933
    chord_index += 1;
934
    chord_timestamp = current_time;
935
936
    for (config.keybinds.items) |keybind| {
937
        if (keybind.key_count == 0) continue;
938
939
        if (keybind.key_count == chord_index) {
940
            var matches = true;
941
            var i: u8 = 0;
942
            while (i < keybind.key_count) : (i += 1) {
943
                if (chord_keys[i].keysym != keybind.keys[i].keysym or
944
                    chord_keys[i].mod_mask != keybind.keys[i].mod_mask)
945
                {
946
                    matches = false;
947
                    break;
948
                }
949
            }
950
            if (matches) {
951
                execute_action(display, keybind.action, keybind.int_arg, keybind.str_arg);
952
                reset_chord_state();
953
                return;
954
            }
955
        }
956
    }
957
958
    var has_partial_match = false;
959
    for (config.keybinds.items) |keybind| {
960
        if (keybind.key_count > chord_index) {
961
            var matches = true;
962
            var i: u8 = 0;
963
            while (i < chord_index) : (i += 1) {
964
                if (chord_keys[i].keysym != keybind.keys[i].keysym or
965
                    chord_keys[i].mod_mask != keybind.keys[i].mod_mask)
966
                {
967
                    matches = false;
968
                    break;
969
                }
970
            }
971
            if (matches) {
972
                has_partial_match = true;
973
                break;
974
            }
975
        }
976
    }
977
978
    if (has_partial_match and !keyboard_grabbed) {
979
        const grab_result = xlib.XGrabKeyboard(
980
            display.handle,
981
            display.root,
982
            xlib.True,
983
            xlib.GrabModeAsync,
984
            xlib.GrabModeAsync,
985
            xlib.CurrentTime,
986
        );
987
        if (grab_result == xlib.GrabSuccess) {
988
            keyboard_grabbed = true;
989
        }
990
    } else if (!has_partial_match) {
991
        reset_chord_state();
992
    }
993
}
994
995
fn execute_action(display: *Display, action: config_mod.Action, int_arg: i32, str_arg: ?[]const u8) void {
996
    switch (action) {
997
        .spawn_terminal => spawn_terminal(),
998
        .spawn => {
999
            if (str_arg) |cmd| {
1000
                spawn_command(cmd);
1001
            }
1002
        },
1003
        .kill_client => kill_focused(display),
1004
        .quit => {
1005
            std.debug.print("quit keybind pressed\n", .{});
1006
            running = false;
1007
        },
1008
        .reload_config => reload_config(display),
1009
        .restart => reload_config(display),
1010
        .show_keybinds => {
1011
            if (keybind_overlay) |overlay| {
1012
                const mon = monitor_mod.selected_monitor orelse monitor_mod.monitors;
1013
                if (mon) |m| {
1014
                    overlay.toggle(m.mon_x, m.mon_y, m.mon_w, m.mon_h);
1015
                }
1016
            }
1017
        },
1018
        .focus_next => focusstack(display, 1),
1019
        .focus_prev => focusstack(display, -1),
1020
        .move_next => movestack(display, 1),
1021
        .move_prev => movestack(display, -1),
1022
        .resize_master => setmfact(@as(f32, @floatFromInt(int_arg)) / 1000.0),
1023
        .inc_master => incnmaster(1),
1024
        .dec_master => incnmaster(-1),
1025
        .toggle_floating => toggle_floating(display),
1026
        .toggle_fullscreen => toggle_fullscreen(display),
1027
        .toggle_gaps => toggle_gaps(),
1028
        .cycle_layout => cycle_layout(),
1029
        .set_layout => set_layout(str_arg),
1030
        .set_layout_tiling => set_layout_index(0),
1031
        .set_layout_floating => set_layout_index(2),
1032
        .view_tag => {
1033
            const tag_mask: u32 = @as(u32, 1) << @intCast(int_arg);
1034
            view(display, tag_mask);
1035
        },
1036
        .view_next_tag => view_adjacent_tag(display, 1),
1037
        .view_prev_tag => view_adjacent_tag(display, -1),
1038
        .view_next_nonempty_tag => view_adjacent_nonempty_tag(display, 1),
1039
        .view_prev_nonempty_tag => view_adjacent_nonempty_tag(display, -1),
1040
        .move_to_tag => {
1041
            const tag_mask: u32 = @as(u32, 1) << @intCast(int_arg);
1042
            tag_client(display, tag_mask);
1043
        },
1044
        .toggle_view_tag => {
1045
            const tag_mask: u32 = @as(u32, 1) << @intCast(int_arg);
1046
            toggle_view(display, tag_mask);
1047
        },
1048
        .toggle_tag => {
1049
            const tag_mask: u32 = @as(u32, 1) << @intCast(int_arg);
1050
            toggle_client_tag(display, tag_mask);
1051
        },
1052
        .focus_monitor => focusmon(display, int_arg),
1053
        .send_to_monitor => sendmon(display, int_arg),
1054
        .scroll_left => {
1055
            scroll_layout(-1);
1056
        },
1057
        .scroll_right => {
1058
            scroll_layout(1);
1059
        },
1060
    }
1061
}
1062
1063
fn reload_config(display: *Display) void {
1064
    std.debug.print("reloading config...\n", .{});
1065
1066
    ungrab_keybinds(display);
1067
1068
    config.keybinds.clearRetainingCapacity();
1069
    config.buttons.clearRetainingCapacity();
1070
    config.rules.clearRetainingCapacity();
1071
    config.blocks.clearRetainingCapacity();
1072
1073
    lua.deinit();
1074
    _ = lua.init(&config);
1075
1076
    const loaded = if (config_path_global) |path|
1077
        lua.load_file(path)
1078
    else
1079
        lua.load_config();
1080
1081
    if (loaded) {
1082
        if (config_path_global) |path| {
1083
            std.debug.print("reloaded config from {s}\n", .{path});
1084
        } else {
1085
            std.debug.print("reloaded config from ~/.config/oxwm/config.lua\n", .{});
1086
        }
1087
        apply_config_values();
1088
    } else {
1089
        std.debug.print("reload failed, restoring defaults\n", .{});
1090
        initialize_default_config();
1091
    }
1092
1093
    bar_mod.destroy_bars(gpa.allocator(), display.handle);
1094
    setup_bars(gpa.allocator(), display);
1095
    rebuild_bar_blocks();
1096
1097
    grab_keybinds(display);
1098
}
1099
1100
fn rebuild_bar_blocks() void {
1101
    var current_bar = bar_mod.bars;
1102
    while (current_bar) |bar| {
1103
        bar.clear_blocks();
1104
        if (config.blocks.items.len > 0) {
1105
            for (config.blocks.items) |cfg_block| {
1106
                const block = config_block_to_bar_block(cfg_block);
1107
                bar.add_block(block);
1108
            }
1109
        } else {
1110
            bar.add_block(blocks_mod.Block.init_ram("", 5, 0x7aa2f7, true));
1111
            bar.add_block(blocks_mod.Block.init_static(" | ", 0x666666, false));
1112
            bar.add_block(blocks_mod.Block.init_datetime("", "%H:%M", 1, 0x0db9d7, true));
1113
        }
1114
        current_bar = bar.next;
1115
    }
1116
}
1117
1118
fn ungrab_keybinds(display: *Display) void {
1119
    _ = xlib.XUngrabKey(display.handle, xlib.AnyKey, xlib.AnyModifier, display.root);
1120
}
1121
1122
fn spawn_child_setup() void {
1123
    _ = std.c.setsid();
1124
    if (display_global) |display| {
1125
        const display_fd = xlib.XConnectionNumber(display.handle);
1126
        std.posix.close(@intCast(display_fd));
1127
    }
1128
    const sigchld_handler = std.posix.Sigaction{
1129
        .handler = .{ .handler = std.posix.SIG.DFL },
1130
        .mask = std.mem.zeroes(std.posix.sigset_t),
1131
        .flags = 0,
1132
    };
1133
    std.posix.sigaction(std.posix.SIG.CHLD, &sigchld_handler, null);
1134
}
1135
1136
fn spawn_command(cmd: []const u8) void {
1137
    std.debug.print("running cmd: {s}\n", .{cmd});
1138
    const pid = std.posix.fork() catch return;
1139
    if (pid == 0) {
1140
        spawn_child_setup();
1141
        var cmd_buf: [1024]u8 = undefined;
1142
        if (cmd.len >= cmd_buf.len) {
1143
            std.posix.exit(1);
1144
        }
1145
        @memcpy(cmd_buf[0..cmd.len], cmd);
1146
        cmd_buf[cmd.len] = 0;
1147
        const argv = [_:null]?[*:0]const u8{ "sh", "-c", @ptrCast(&cmd_buf) };
1148
        _ = std.posix.execvpeZ("sh", &argv, std.c.environ) catch {};
1149
        std.posix.exit(1);
1150
    }
1151
}
1152
1153
fn spawn_terminal() void {
1154
    const pid = std.posix.fork() catch return;
1155
    if (pid == 0) {
1156
        spawn_child_setup();
1157
        var term_buf: [256]u8 = undefined;
1158
        const terminal = config.terminal;
1159
        if (terminal.len >= term_buf.len) {
1160
            std.posix.exit(1);
1161
        }
1162
        @memcpy(term_buf[0..terminal.len], terminal);
1163
        term_buf[terminal.len] = 0;
1164
        const term_ptr: [*:0]const u8 = @ptrCast(&term_buf);
1165
        const argv = [_:null]?[*:0]const u8{term_ptr};
1166
        _ = std.posix.execvpeZ(term_ptr, &argv, std.c.environ) catch {};
1167
        std.posix.exit(1);
1168
    }
1169
}
1170
1171
fn movestack(display: *Display, direction: i32) void {
1172
    const monitor = monitor_mod.selected_monitor orelse return;
1173
    const current = monitor.sel orelse return;
1174
1175
    if (current.is_floating) {
1176
        return;
1177
    }
1178
1179
    var target: ?*Client = null;
1180
1181
    if (direction > 0) {
1182
        target = current.next;
1183
        while (target) |client| {
1184
            if (client_mod.is_visible(client) and !client.is_floating) {
1185
                break;
1186
            }
1187
            target = client.next;
1188
        }
1189
        if (target == null) {
1190
            target = monitor.clients;
1191
            while (target) |client| {
1192
                if (client == current) {
1193
                    break;
1194
                }
1195
                if (client_mod.is_visible(client) and !client.is_floating) {
1196
                    break;
1197
                }
1198
                target = client.next;
1199
            }
1200
        }
1201
    } else {
1202
        var prev: ?*Client = null;
1203
        var iter = monitor.clients;
1204
        while (iter) |client| {
1205
            if (client == current) {
1206
                break;
1207
            }
1208
            if (client_mod.is_visible(client) and !client.is_floating) {
1209
                prev = client;
1210
            }
1211
            iter = client.next;
1212
        }
1213
        if (prev == null) {
1214
            iter = current.next;
1215
            while (iter) |client| {
1216
                if (client_mod.is_visible(client) and !client.is_floating) {
1217
                    prev = client;
1218
                }
1219
                iter = client.next;
1220
            }
1221
        }
1222
        target = prev;
1223
    }
1224
1225
    if (target) |swap_client| {
1226
        if (swap_client != current) {
1227
            client_mod.swap_clients(current, swap_client);
1228
            arrange(monitor);
1229
            focus(display, current);
1230
        }
1231
    }
1232
}
1233
1234
fn toggle_view(display: *Display, tag_mask: u32) void {
1235
    const monitor = monitor_mod.selected_monitor orelse return;
1236
    const new_tags = monitor.tagset[monitor.sel_tags] ^ tag_mask;
1237
    if (new_tags != 0) {
1238
        monitor.tagset[monitor.sel_tags] = new_tags;
1239
1240
        if (new_tags == ~@as(u32, 0)) {
1241
            monitor.pertag.prevtag = monitor.pertag.curtag;
1242
            monitor.pertag.curtag = 0;
1243
        }
1244
1245
        if ((new_tags & (@as(u32, 1) << @intCast(monitor.pertag.curtag -| 1))) == 0) {
1246
            monitor.pertag.prevtag = monitor.pertag.curtag;
1247
            var i: u32 = 0;
1248
            while (i < 9) : (i += 1) {
1249
                if ((new_tags & (@as(u32, 1) << @intCast(i))) != 0) break;
1250
            }
1251
            monitor.pertag.curtag = i + 1;
1252
        }
1253
1254
        monitor.nmaster = monitor.pertag.nmasters[monitor.pertag.curtag];
1255
        monitor.mfact = monitor.pertag.mfacts[monitor.pertag.curtag];
1256
        monitor.sel_lt = monitor.pertag.sellts[monitor.pertag.curtag];
1257
1258
        focus_top_client(display, monitor);
1259
        arrange(monitor);
1260
        bar_mod.invalidate_bars();
1261
    }
1262
}
1263
1264
fn toggle_client_tag(display: *Display, tag_mask: u32) void {
1265
    const monitor = monitor_mod.selected_monitor orelse return;
1266
    const client = monitor.sel orelse return;
1267
    const new_tags = client.tags ^ tag_mask;
1268
    if (new_tags != 0) {
1269
        client.tags = new_tags;
1270
        focus_top_client(display, monitor);
1271
        arrange(monitor);
1272
        bar_mod.invalidate_bars();
1273
    }
1274
}
1275
1276
fn toggle_gaps() void {
1277
    const monitor = monitor_mod.selected_monitor orelse return;
1278
    if (monitor.gap_inner_h == 0) {
1279
        monitor.gap_inner_h = gap_inner_v;
1280
        monitor.gap_inner_v = gap_inner_v;
1281
        monitor.gap_outer_h = gap_outer_h;
1282
        monitor.gap_outer_v = gap_outer_v;
1283
    } else {
1284
        monitor.gap_inner_h = 0;
1285
        monitor.gap_inner_v = 0;
1286
        monitor.gap_outer_h = 0;
1287
        monitor.gap_outer_v = 0;
1288
    }
1289
    arrange(monitor);
1290
}
1291
1292
fn kill_focused(display: *Display) void {
1293
    const selected = monitor_mod.selected_monitor orelse return;
1294
    const client = selected.sel orelse return;
1295
    std.debug.print("killing window: 0x{x}\n", .{client.window});
1296
1297
    if (!send_event(display, client, wm_delete)) {
1298
        _ = xlib.XGrabServer(display.handle);
1299
        _ = xlib.XKillClient(display.handle, client.window);
1300
        _ = xlib.XSync(display.handle, xlib.False);
1301
        _ = xlib.XUngrabServer(display.handle);
1302
    }
1303
}
1304
1305
fn toggle_fullscreen(display: *Display) void {
1306
    const selected = monitor_mod.selected_monitor orelse return;
1307
    const client = selected.sel orelse return;
1308
    set_fullscreen(display, client, !client.is_fullscreen);
1309
}
1310
1311
fn set_fullscreen(display: *Display, client: *Client, fullscreen: bool) void {
1312
    const monitor = client.monitor orelse return;
1313
1314
    if (fullscreen and !client.is_fullscreen) {
1315
        var fullscreen_atom = net_wm_state_fullscreen;
1316
        _ = xlib.XChangeProperty(
1317
            display.handle,
1318
            client.window,
1319
            net_wm_state,
1320
            xlib.XA_ATOM,
1321
            32,
1322
            xlib.PropModeReplace,
1323
            @ptrCast(&fullscreen_atom),
1324
            1,
1325
        );
1326
        client.is_fullscreen = true;
1327
        client.old_state = client.is_floating;
1328
        client.old_border_width = client.border_width;
1329
        client.border_width = 0;
1330
        client.is_floating = true;
1331
1332
        _ = xlib.XSetWindowBorderWidth(display.handle, client.window, 0);
1333
        tiling.resize_client(client, monitor.mon_x, monitor.mon_y, monitor.mon_w, monitor.mon_h);
1334
        _ = xlib.XRaiseWindow(display.handle, client.window);
1335
1336
        std.debug.print("fullscreen enabled: window=0x{x}\n", .{client.window});
1337
    } else if (!fullscreen and client.is_fullscreen) {
1338
        var no_atom: xlib.Atom = 0;
1339
        _ = xlib.XChangeProperty(
1340
            display.handle,
1341
            client.window,
1342
            net_wm_state,
1343
            xlib.XA_ATOM,
1344
            32,
1345
            xlib.PropModeReplace,
1346
            @ptrCast(&no_atom),
1347
            0,
1348
        );
1349
        client.is_fullscreen = false;
1350
        client.is_floating = client.old_state;
1351
        client.border_width = client.old_border_width;
1352
1353
        client.x = client.old_x;
1354
        client.y = client.old_y;
1355
        client.width = client.old_width;
1356
        client.height = client.old_height;
1357
1358
        tiling.resize_client(client, client.x, client.y, client.width, client.height);
1359
        arrange(monitor);
1360
1361
        std.debug.print("fullscreen disabled: window=0x{x}\n", .{client.window});
1362
    }
1363
}
1364
1365
fn view(display: *Display, tag_mask: u32) void {
1366
    const monitor = monitor_mod.selected_monitor orelse return;
1367
    if (tag_mask == monitor.tagset[monitor.sel_tags]) {
1368
        return;
1369
    }
1370
    monitor.sel_tags ^= 1;
1371
    if (tag_mask != 0) {
1372
        monitor.tagset[monitor.sel_tags] = tag_mask;
1373
        monitor.pertag.prevtag = monitor.pertag.curtag;
1374
1375
        if (tag_mask == ~@as(u32, 0)) {
1376
            monitor.pertag.curtag = 0;
1377
        } else {
1378
            var i: u32 = 0;
1379
            while (i < 9) : (i += 1) {
1380
                if ((tag_mask & (@as(u32, 1) << @intCast(i))) != 0) break;
1381
            }
1382
            monitor.pertag.curtag = i + 1;
1383
        }
1384
    } else {
1385
        const tmp = monitor.pertag.prevtag;
1386
        monitor.pertag.prevtag = monitor.pertag.curtag;
1387
        monitor.pertag.curtag = tmp;
1388
    }
1389
1390
    monitor.nmaster = monitor.pertag.nmasters[monitor.pertag.curtag];
1391
    monitor.mfact = monitor.pertag.mfacts[monitor.pertag.curtag];
1392
    monitor.sel_lt = monitor.pertag.sellts[monitor.pertag.curtag];
1393
1394
    focus_top_client(display, monitor);
1395
    arrange(monitor);
1396
    bar_mod.invalidate_bars();
1397
    std.debug.print("view: tag_mask={d}\n", .{monitor.tagset[monitor.sel_tags]});
1398
}
1399
1400
fn view_adjacent_tag(display: *Display, direction: i32) void {
1401
    const monitor = monitor_mod.selected_monitor orelse return;
1402
    const current_tag = monitor.pertag.curtag;
1403
    var new_tag: i32 = @intCast(current_tag);
1404
1405
    new_tag += direction;
1406
    if (new_tag < 1) new_tag = 9;
1407
    if (new_tag > 9) new_tag = 1;
1408
1409
    const tag_mask: u32 = @as(u32, 1) << @intCast(new_tag - 1);
1410
    view(display, tag_mask);
1411
}
1412
1413
fn view_adjacent_nonempty_tag(display: *Display, direction: i32) void {
1414
    const monitor = monitor_mod.selected_monitor orelse return;
1415
    const current_tag = monitor.pertag.curtag;
1416
    var new_tag: i32 = @intCast(current_tag);
1417
1418
    var attempts: i32 = 0;
1419
    while (attempts < 9) : (attempts += 1) {
1420
        new_tag += direction;
1421
        if (new_tag < 1) new_tag = 9;
1422
        if (new_tag > 9) new_tag = 1;
1423
1424
        const tag_mask: u32 = @as(u32, 1) << @intCast(new_tag - 1);
1425
        if (has_clients_on_tag(monitor, tag_mask)) {
1426
            view(display, tag_mask);
1427
            return;
1428
        }
1429
    }
1430
}
1431
1432
fn has_clients_on_tag(monitor: *monitor_mod.Monitor, tag_mask: u32) bool {
1433
    var client = monitor.clients;
1434
    while (client) |c| {
1435
        if ((c.tags & tag_mask) != 0) {
1436
            return true;
1437
        }
1438
        client = c.next;
1439
    }
1440
    return false;
1441
}
1442
1443
fn tag_client(display: *Display, tag_mask: u32) void {
1444
    const monitor = monitor_mod.selected_monitor orelse return;
1445
    const client = monitor.sel orelse return;
1446
    if (tag_mask == 0) {
1447
        return;
1448
    }
1449
    client.tags = tag_mask;
1450
    focus_top_client(display, monitor);
1451
    arrange(monitor);
1452
    bar_mod.invalidate_bars();
1453
    std.debug.print("tag_client: window=0x{x} tag_mask={d}\n", .{ client.window, tag_mask });
1454
}
1455
1456
fn focus_top_client(display: *Display, monitor: *Monitor) void {
1457
    var visible_client = monitor.stack;
1458
    while (visible_client) |client| {
1459
        if (client_mod.is_visible(client)) {
1460
            focus(display, client);
1461
            return;
1462
        }
1463
        visible_client = client.stack_next;
1464
    }
1465
    monitor.sel = null;
1466
    _ = xlib.XSetInputFocus(display.handle, display.root, xlib.RevertToPointerRoot, xlib.CurrentTime);
1467
}
1468
1469
fn focusstack(display: *Display, direction: i32) void {
1470
    const monitor = monitor_mod.selected_monitor orelse return;
1471
    const current = monitor.sel orelse return;
1472
1473
    var next_client: ?*Client = null;
1474
1475
    if (direction > 0) {
1476
        next_client = current.next;
1477
        while (next_client) |client| {
1478
            if (client_mod.is_visible(client)) {
1479
                break;
1480
            }
1481
            next_client = client.next;
1482
        }
1483
        if (next_client == null) {
1484
            next_client = monitor.clients;
1485
            while (next_client) |client| {
1486
                if (client_mod.is_visible(client)) {
1487
                    break;
1488
                }
1489
                next_client = client.next;
1490
            }
1491
        }
1492
    } else {
1493
        var prev: ?*Client = null;
1494
        var iter = monitor.clients;
1495
        while (iter) |client| {
1496
            if (client == current) {
1497
                break;
1498
            }
1499
            if (client_mod.is_visible(client)) {
1500
                prev = client;
1501
            }
1502
            iter = client.next;
1503
        }
1504
        if (prev == null) {
1505
            iter = current.next;
1506
            while (iter) |client| {
1507
                if (client_mod.is_visible(client)) {
1508
                    prev = client;
1509
                }
1510
                iter = client.next;
1511
            }
1512
        }
1513
        next_client = prev;
1514
    }
1515
1516
    if (next_client) |client| {
1517
        focus(display, client);
1518
        if (client.monitor) |client_monitor| {
1519
            restack(display, client_monitor);
1520
        }
1521
    }
1522
}
1523
1524
fn toggle_floating(_: *Display) void {
1525
    const monitor = monitor_mod.selected_monitor orelse return;
1526
    const client = monitor.sel orelse return;
1527
1528
    if (client.is_fullscreen) {
1529
        return;
1530
    }
1531
1532
    client.is_floating = !client.is_floating;
1533
1534
    if (client.is_floating) {
1535
        tiling.resize(client, client.x, client.y, client.width, client.height, false);
1536
    }
1537
1538
    arrange(monitor);
1539
    std.debug.print("toggle_floating: window=0x{x} floating={}\n", .{ client.window, client.is_floating });
1540
}
1541
1542
fn incnmaster(delta: i32) void {
1543
    const monitor = monitor_mod.selected_monitor orelse return;
1544
    const new_val = @max(0, monitor.nmaster + delta);
1545
    monitor.nmaster = new_val;
1546
    monitor.pertag.nmasters[monitor.pertag.curtag] = new_val;
1547
    arrange(monitor);
1548
    std.debug.print("incnmaster: nmaster={d}\n", .{monitor.nmaster});
1549
}
1550
1551
fn setmfact(delta: f32) void {
1552
    const monitor = monitor_mod.selected_monitor orelse return;
1553
    const new_mfact = monitor.mfact + delta;
1554
    if (new_mfact < 0.05 or new_mfact > 0.95) {
1555
        return;
1556
    }
1557
    monitor.mfact = new_mfact;
1558
    monitor.pertag.mfacts[monitor.pertag.curtag] = new_mfact;
1559
    arrange(monitor);
1560
    std.debug.print("setmfact: mfact={d:.2}\n", .{monitor.mfact});
1561
}
1562
1563
fn cycle_layout() void {
1564
    const monitor = monitor_mod.selected_monitor orelse return;
1565
    const new_lt = (monitor.sel_lt + 1) % 5;
1566
    monitor.sel_lt = new_lt;
1567
    monitor.pertag.sellts[monitor.pertag.curtag] = new_lt;
1568
    if (new_lt != 3) {
1569
        monitor.scroll_offset = 0;
1570
    }
1571
    arrange(monitor);
1572
    bar_mod.invalidate_bars();
1573
    if (monitor.lt[monitor.sel_lt]) |layout| {
1574
        std.debug.print("cycle_layout: {s}\n", .{layout.symbol});
1575
    }
1576
}
1577
1578
fn set_layout(layout_name: ?[]const u8) void {
1579
    const monitor = monitor_mod.selected_monitor orelse return;
1580
    const name = layout_name orelse return;
1581
1582
    const new_lt: u32 = if (std.mem.eql(u8, name, "tiling") or std.mem.eql(u8, name, "[]="))
1583
        0
1584
    else if (std.mem.eql(u8, name, "monocle") or std.mem.eql(u8, name, "[M]"))
1585
        1
1586
    else if (std.mem.eql(u8, name, "floating") or std.mem.eql(u8, name, "><>"))
1587
        2
1588
    else if (std.mem.eql(u8, name, "scrolling") or std.mem.eql(u8, name, "[S]"))
1589
        3
1590
    else if (std.mem.eql(u8, name, "grid") or std.mem.eql(u8, name, "[#]"))
1591
        4
1592
    else {
1593
        std.debug.print("set_layout: unknown layout '{s}'\n", .{name});
1594
        return;
1595
    };
1596
1597
    monitor.sel_lt = new_lt;
1598
    monitor.pertag.sellts[monitor.pertag.curtag] = new_lt;
1599
    if (new_lt != 3) {
1600
        monitor.scroll_offset = 0;
1601
    }
1602
    arrange(monitor);
1603
    bar_mod.invalidate_bars();
1604
    if (monitor.lt[monitor.sel_lt]) |layout| {
1605
        std.debug.print("set_layout: {s}\n", .{layout.symbol});
1606
    }
1607
}
1608
1609
fn set_layout_index(index: u32) void {
1610
    const monitor = monitor_mod.selected_monitor orelse return;
1611
    monitor.sel_lt = index;
1612
    monitor.pertag.sellts[monitor.pertag.curtag] = index;
1613
    if (index != 3) {
1614
        monitor.scroll_offset = 0;
1615
    }
1616
    arrange(monitor);
1617
    bar_mod.invalidate_bars();
1618
    if (monitor.lt[monitor.sel_lt]) |layout| {
1619
        std.debug.print("set_layout_index: {s}\n", .{layout.symbol});
1620
    }
1621
}
1622
1623
fn focusmon(display: *Display, direction: i32) void {
1624
    const selmon = monitor_mod.selected_monitor orelse return;
1625
    const target = monitor_mod.dir_to_monitor(direction) orelse return;
1626
    if (target == selmon) {
1627
        return;
1628
    }
1629
    unfocus_client(display, selmon.sel, false);
1630
    monitor_mod.selected_monitor = target;
1631
    focus(display, null);
1632
    std.debug.print("focusmon: monitor {d}\n", .{target.num});
1633
}
1634
1635
fn sendmon(display: *Display, direction: i32) void {
1636
    const source_monitor = monitor_mod.selected_monitor orelse return;
1637
    const client = source_monitor.sel orelse return;
1638
    const target = monitor_mod.dir_to_monitor(direction) orelse return;
1639
1640
    if (target == source_monitor) {
1641
        return;
1642
    }
1643
1644
    client_mod.detach(client);
1645
    client_mod.detach_stack(client);
1646
    client.monitor = target;
1647
    client.tags = target.tagset[target.sel_tags];
1648
    client_mod.attach_aside(client);
1649
    client_mod.attach_stack(client);
1650
1651
    focus_top_client(display, source_monitor);
1652
    arrange(source_monitor);
1653
    arrange(target);
1654
1655
    std.debug.print("sendmon: window=0x{x} to monitor {d}\n", .{ client.window, target.num });
1656
}
1657
1658
fn snap_x(client: *Client, new_x: i32, monitor: *Monitor) i32 {
1659
    const client_width = client.width + 2 * client.border_width;
1660
    if (@abs(monitor.win_x - new_x) < snap_distance) {
1661
        return monitor.win_x;
1662
    } else if (@abs((monitor.win_x + monitor.win_w) - (new_x + client_width)) < snap_distance) {
1663
        return monitor.win_x + monitor.win_w - client_width;
1664
    }
1665
    return new_x;
1666
}
1667
1668
fn snap_y(client: *Client, new_y: i32, monitor: *Monitor) i32 {
1669
    const client_height = client.height + 2 * client.border_width;
1670
    if (@abs(monitor.win_y - new_y) < snap_distance) {
1671
        return monitor.win_y;
1672
    } else if (@abs((monitor.win_y + monitor.win_h) - (new_y + client_height)) < snap_distance) {
1673
        return monitor.win_y + monitor.win_h - client_height;
1674
    }
1675
    return new_y;
1676
}
1677
1678
fn movemouse(display: *Display) void {
1679
    const monitor = monitor_mod.selected_monitor orelse return;
1680
    const client = monitor.sel orelse return;
1681
1682
    if (client.is_fullscreen) {
1683
        return;
1684
    }
1685
1686
    restack(display, monitor);
1687
1688
    const was_floating = client.is_floating;
1689
    if (!client.is_floating) {
1690
        client.is_floating = true;
1691
    }
1692
1693
    var root_x: c_int = undefined;
1694
    var root_y: c_int = undefined;
1695
    var dummy_win: xlib.Window = undefined;
1696
    var dummy_int: c_int = undefined;
1697
    var dummy_uint: c_uint = undefined;
1698
1699
    _ = xlib.XQueryPointer(display.handle, display.root, &dummy_win, &dummy_win, &root_x, &root_y, &dummy_int, &dummy_int, &dummy_uint);
1700
1701
    const grab_result = xlib.XGrabPointer(
1702
        display.handle,
1703
        display.root,
1704
        xlib.False,
1705
        xlib.ButtonPressMask | xlib.ButtonReleaseMask | xlib.PointerMotionMask,
1706
        xlib.GrabModeAsync,
1707
        xlib.GrabModeAsync,
1708
        xlib.None,
1709
        cursor_move,
1710
        xlib.CurrentTime,
1711
    );
1712
1713
    if (grab_result != xlib.GrabSuccess) {
1714
        return;
1715
    }
1716
1717
    const start_x = client.x;
1718
    const start_y = client.y;
1719
    const pointer_start_x = root_x;
1720
    const pointer_start_y = root_y;
1721
    var last_time: c_ulong = 0;
1722
1723
    var event: xlib.XEvent = undefined;
1724
    var done = false;
1725
1726
    while (!done) {
1727
        _ = xlib.XNextEvent(display.handle, &event);
1728
1729
        switch (event.type) {
1730
            xlib.MotionNotify => {
1731
                const motion = &event.xmotion;
1732
                if ((motion.time - last_time) < (1000 / 60)) {
1733
                    continue;
1734
                }
1735
                last_time = motion.time;
1736
                const delta_x = motion.x_root - pointer_start_x;
1737
                const delta_y = motion.y_root - pointer_start_y;
1738
                var new_x = start_x + delta_x;
1739
                var new_y = start_y + delta_y;
1740
                if (client.monitor) |client_monitor| {
1741
                    new_x = snap_x(client, new_x, client_monitor);
1742
                    new_y = snap_y(client, new_y, client_monitor);
1743
                }
1744
                tiling.resize(client, new_x, new_y, client.width, client.height, true);
1745
            },
1746
            xlib.ButtonRelease => {
1747
                done = true;
1748
            },
1749
            else => {},
1750
        }
1751
    }
1752
1753
    _ = xlib.XUngrabPointer(display.handle, xlib.CurrentTime);
1754
1755
    const new_mon = monitor_mod.rect_to_monitor(client.x, client.y, client.width, client.height);
1756
    if (new_mon != null and new_mon != monitor) {
1757
        client_mod.detach(client);
1758
        client_mod.detach_stack(client);
1759
        client.monitor = new_mon;
1760
        client.tags = new_mon.?.tagset[new_mon.?.sel_tags];
1761
        client_mod.attach_aside(client);
1762
        client_mod.attach_stack(client);
1763
        monitor_mod.selected_monitor = new_mon;
1764
        focus(display, client);
1765
        arrange(monitor);
1766
        arrange(new_mon.?);
1767
    } else {
1768
        arrange(monitor);
1769
    }
1770
1771
    if (config.auto_tile and !was_floating) {
1772
        const drop_monitor = client.monitor orelse return;
1773
        const center_x = client.x + @divTrunc(client.width, 2);
1774
        const center_y = client.y + @divTrunc(client.height, 2);
1775
1776
        if (client_mod.tiled_window_at(client, drop_monitor, center_x, center_y)) |target| {
1777
            client_mod.insert_before(client, target);
1778
        }
1779
1780
        client.is_floating = false;
1781
        arrange(drop_monitor);
1782
    }
1783
}
1784
1785
fn resizemouse(display: *Display) void {
1786
    const monitor = monitor_mod.selected_monitor orelse return;
1787
    const client = monitor.sel orelse return;
1788
1789
    if (client.is_fullscreen) {
1790
        return;
1791
    }
1792
1793
    restack(display, monitor);
1794
1795
    if (!client.is_floating) {
1796
        client.is_floating = true;
1797
    }
1798
1799
    const grab_result = xlib.XGrabPointer(
1800
        display.handle,
1801
        display.root,
1802
        xlib.False,
1803
        xlib.ButtonPressMask | xlib.ButtonReleaseMask | xlib.PointerMotionMask,
1804
        xlib.GrabModeAsync,
1805
        xlib.GrabModeAsync,
1806
        xlib.None,
1807
        cursor_resize,
1808
        xlib.CurrentTime,
1809
    );
1810
1811
    if (grab_result != xlib.GrabSuccess) {
1812
        return;
1813
    }
1814
1815
    _ = xlib.XWarpPointer(display.handle, xlib.None, client.window, 0, 0, 0, 0, client.width + client.border_width - 1, client.height + client.border_width - 1);
1816
1817
    var event: xlib.XEvent = undefined;
1818
    var done = false;
1819
    var last_time: c_ulong = 0;
1820
1821
    while (!done) {
1822
        _ = xlib.XNextEvent(display.handle, &event);
1823
1824
        switch (event.type) {
1825
            xlib.MotionNotify => {
1826
                const motion = &event.xmotion;
1827
                if ((motion.time - last_time) < (1000 / 60)) {
1828
                    continue;
1829
                }
1830
                last_time = motion.time;
1831
                var new_width = @max(1, motion.x_root - client.x - 2 * client.border_width + 1);
1832
                var new_height = @max(1, motion.y_root - client.y - 2 * client.border_width + 1);
1833
                if (client.monitor) |client_monitor| {
1834
                    const client_right = client.x + new_width + 2 * client.border_width;
1835
                    const client_bottom = client.y + new_height + 2 * client.border_width;
1836
                    const mon_right = client_monitor.win_x + client_monitor.win_w;
1837
                    const mon_bottom = client_monitor.win_y + client_monitor.win_h;
1838
                    if (@abs(mon_right - client_right) < snap_distance) {
1839
                        new_width = @max(1, mon_right - client.x - 2 * client.border_width);
1840
                    }
1841
                    if (@abs(mon_bottom - client_bottom) < snap_distance) {
1842
                        new_height = @max(1, mon_bottom - client.y - 2 * client.border_width);
1843
                    }
1844
                }
1845
                tiling.resize(client, client.x, client.y, new_width, new_height, true);
1846
            },
1847
            xlib.ButtonRelease => {
1848
                done = true;
1849
            },
1850
            else => {},
1851
        }
1852
    }
1853
1854
    _ = xlib.XUngrabPointer(display.handle, xlib.CurrentTime);
1855
    arrange(monitor);
1856
}
1857
1858
fn handle_expose(display: *Display, event: *xlib.XExposeEvent) void {
1859
    if (event.count != 0) return;
1860
1861
    if (bar_mod.window_to_bar(event.window)) |bar| {
1862
        bar.invalidate();
1863
        bar.draw(display.handle, &tags);
1864
    }
1865
}
1866
1867
fn clean_mask(mask: c_uint) c_uint {
1868
    const lock: c_uint = @intCast(xlib.LockMask);
1869
    const shift: c_uint = @intCast(xlib.ShiftMask);
1870
    const ctrl: c_uint = @intCast(xlib.ControlMask);
1871
    const mod1: c_uint = @intCast(xlib.Mod1Mask);
1872
    const mod2: c_uint = @intCast(xlib.Mod2Mask);
1873
    const mod3: c_uint = @intCast(xlib.Mod3Mask);
1874
    const mod4: c_uint = @intCast(xlib.Mod4Mask);
1875
    const mod5: c_uint = @intCast(xlib.Mod5Mask);
1876
    return mask & ~(lock | numlock_mask) & (shift | ctrl | mod1 | mod2 | mod3 | mod4 | mod5);
1877
}
1878
1879
fn handle_button_press(display: *Display, event: *xlib.XButtonEvent) void {
1880
    std.debug.print("button_press: window=0x{x} subwindow=0x{x}\n", .{ event.window, event.subwindow });
1881
1882
    const clicked_monitor = monitor_mod.window_to_monitor(event.window);
1883
    if (clicked_monitor) |monitor| {
1884
        if (monitor != monitor_mod.selected_monitor) {
1885
            if (monitor_mod.selected_monitor) |selmon| {
1886
                unfocus_client(display, selmon.sel, true);
1887
            }
1888
            monitor_mod.selected_monitor = monitor;
1889
            focus(display, null);
1890
        }
1891
    }
1892
1893
    if (bar_mod.window_to_bar(event.window)) |bar| {
1894
        const clicked_tag = bar.handle_click(event.x, &tags);
1895
        if (clicked_tag) |tag_index| {
1896
            const tag_mask: u32 = @as(u32, 1) << @intCast(tag_index);
1897
            view(display, tag_mask);
1898
        }
1899
        return;
1900
    }
1901
1902
    const click_client = client_mod.window_to_client(event.window);
1903
    if (click_client) |found_client| {
1904
        focus(display, found_client);
1905
        if (monitor_mod.selected_monitor) |selmon| {
1906
            restack(display, selmon);
1907
        }
1908
        _ = xlib.XAllowEvents(display.handle, xlib.ReplayPointer, xlib.CurrentTime);
1909
    }
1910
1911
    const clean_state = clean_mask(event.state);
1912
    for (config.buttons.items) |button| {
1913
        if (button.click != .client_win) continue;
1914
        const button_clean_mask = clean_mask(button.mod_mask);
1915
        if (clean_state == button_clean_mask and event.button == button.button) {
1916
            switch (button.action) {
1917
                .move_mouse => movemouse(display),
1918
                .resize_mouse => resizemouse(display),
1919
                .toggle_floating => {
1920
                    if (click_client) |found_client| {
1921
                        found_client.is_floating = !found_client.is_floating;
1922
                        if (monitor_mod.selected_monitor) |monitor| {
1923
                            arrange(monitor);
1924
                        }
1925
                    }
1926
                },
1927
            }
1928
            return;
1929
        }
1930
    }
1931
}
1932
1933
fn handle_client_message(display: *Display, event: *xlib.XClientMessageEvent) void {
1934
    const client = client_mod.window_to_client(event.window) orelse return;
1935
1936
    if (event.message_type == net_wm_state) {
1937
        const action = event.data.l[0];
1938
        const first_property = @as(xlib.Atom, @intCast(event.data.l[1]));
1939
        const second_property = @as(xlib.Atom, @intCast(event.data.l[2]));
1940
1941
        if (first_property == net_wm_state_fullscreen or second_property == net_wm_state_fullscreen) {
1942
            const net_wm_state_remove = 0;
1943
            const net_wm_state_add = 1;
1944
            const net_wm_state_toggle = 2;
1945
1946
            if (action == net_wm_state_add) {
1947
                set_fullscreen(display, client, true);
1948
            } else if (action == net_wm_state_remove) {
1949
                set_fullscreen(display, client, false);
1950
            } else if (action == net_wm_state_toggle) {
1951
                set_fullscreen(display, client, !client.is_fullscreen);
1952
            }
1953
        }
1954
    } else if (event.message_type == net_active_window) {
1955
        const selected = monitor_mod.selected_monitor orelse return;
1956
        if (client != selected.sel and !client.is_urgent) {
1957
            set_urgent(display, client, true);
1958
        }
1959
    }
1960
}
1961
1962
fn handle_destroy_notify(display: *Display, event: *xlib.XDestroyWindowEvent) void {
1963
    const client = client_mod.window_to_client(event.window) orelse return;
1964
    std.debug.print("destroy_notify: window=0x{x}\n", .{event.window});
1965
    unmanage(display, client);
1966
}
1967
1968
fn handle_unmap_notify(display: *Display, event: *xlib.XUnmapEvent) void {
1969
    const client = client_mod.window_to_client(event.window) orelse return;
1970
    std.debug.print("unmap_notify: window=0x{x}\n", .{event.window});
1971
    unmanage(display, client);
1972
}
1973
1974
fn unmanage(display: *Display, client: *Client) void {
1975
    const client_monitor = client.monitor;
1976
1977
    var next_focus: ?*Client = null;
1978
    if (client_monitor) |monitor| {
1979
        if (monitor.sel == client and is_scrolling_layout(monitor)) {
1980
            next_focus = client.next;
1981
            if (next_focus == null) {
1982
                var prev: ?*Client = null;
1983
                var iter = monitor.clients;
1984
                while (iter) |c| {
1985
                    if (c == client) break;
1986
                    prev = c;
1987
                    iter = c.next;
1988
                }
1989
                next_focus = prev;
1990
            }
1991
        }
1992
    }
1993
1994
    client_mod.detach(client);
1995
    client_mod.detach_stack(client);
1996
1997
    if (client_monitor) |monitor| {
1998
        if (monitor.sel == client) {
1999
            monitor.sel = if (next_focus) |nf| nf else monitor.stack;
2000
        }
2001
        if (is_scrolling_layout(monitor)) {
2002
            const target = if (monitor.sel) |sel| scrolling.get_target_scroll_for_window(monitor, sel) else 0;
2003
            if (target == 0) {
2004
                monitor.scroll_offset = scrolling.get_scroll_step(monitor);
2005
            } else {
2006
                monitor.scroll_offset = 0;
2007
            }
2008
        }
2009
        arrange(monitor);
2010
    }
2011
2012
    if (client_monitor) |monitor| {
2013
        if (monitor.sel) |selected| {
2014
            focus(display, selected);
2015
        }
2016
    }
2017
2018
    client_mod.destroy(client);
2019
    update_client_list(display);
2020
    bar_mod.invalidate_bars();
2021
}
2022
2023
fn handle_enter_notify(display: *Display, event: *xlib.XCrossingEvent) void {
2024
    if ((event.mode != xlib.NotifyNormal or event.detail == xlib.NotifyInferior) and event.window != display.root) {
2025
        return;
2026
    }
2027
2028
    const client = client_mod.window_to_client(event.window);
2029
    const target_mon = if (client) |c| c.monitor else monitor_mod.window_to_monitor(event.window);
2030
    const selmon = monitor_mod.selected_monitor;
2031
2032
    if (target_mon != selmon) {
2033
        if (selmon) |sel| {
2034
            unfocus_client(display, sel.sel, true);
2035
        }
2036
        monitor_mod.selected_monitor = target_mon;
2037
    } else if (client == null) {
2038
        return;
2039
    } else if (selmon) |sel| {
2040
        if (client.? == sel.sel) {
2041
            return;
2042
        }
2043
    }
2044
2045
    focus(display, client);
2046
}
2047
2048
fn handle_focus_in(display: *Display, event: *xlib.XFocusChangeEvent) void {
2049
    const selmon = monitor_mod.selected_monitor orelse return;
2050
    const selected = selmon.sel orelse return;
2051
    if (event.window != selected.window) {
2052
        set_focus(display, selected);
2053
    }
2054
}
2055
2056
fn set_focus(display: *Display, client: *Client) void {
2057
    if (!client.never_focus) {
2058
        _ = xlib.XSetInputFocus(display.handle, client.window, xlib.RevertToPointerRoot, xlib.CurrentTime);
2059
        _ = xlib.XChangeProperty(display.handle, display.root, net_active_window, xlib.XA_WINDOW, 32, xlib.PropModeReplace, @ptrCast(&client.window), 1);
2060
    }
2061
    _ = send_event(display, client, wm_take_focus);
2062
}
2063
2064
var last_motion_monitor: ?*Monitor = null;
2065
2066
fn handle_motion_notify(display: *Display, event: *xlib.XMotionEvent) void {
2067
    if (event.window != display.root) {
2068
        return;
2069
    }
2070
2071
    const target_mon = monitor_mod.rect_to_monitor(event.x_root, event.y_root, 1, 1);
2072
    if (target_mon != last_motion_monitor and last_motion_monitor != null) {
2073
        if (monitor_mod.selected_monitor) |selmon| {
2074
            unfocus_client(display, selmon.sel, true);
2075
        }
2076
        monitor_mod.selected_monitor = target_mon;
2077
        focus(display, null);
2078
    }
2079
    last_motion_monitor = target_mon;
2080
}
2081
2082
fn handle_property_notify(display: *Display, event: *xlib.XPropertyEvent) void {
2083
    if (event.state == xlib.PropertyDelete) {
2084
        return;
2085
    }
2086
2087
    const client = client_mod.window_to_client(event.window) orelse return;
2088
2089
    if (event.atom == xlib.XA_WM_TRANSIENT_FOR) {
2090
        var trans: xlib.Window = 0;
2091
        if (!client.is_floating and xlib.XGetTransientForHint(display.handle, client.window, &trans) != 0) {
2092
            client.is_floating = client_mod.window_to_client(trans) != null;
2093
            if (client.is_floating) {
2094
                if (client.monitor) |monitor| {
2095
                    arrange(monitor);
2096
                }
2097
            }
2098
        }
2099
    } else if (event.atom == xlib.XA_WM_NORMAL_HINTS) {
2100
        client.hints_valid = false;
2101
    } else if (event.atom == xlib.XA_WM_HINTS) {
2102
        update_wm_hints(display, client);
2103
        bar_mod.invalidate_bars();
2104
    } else if (event.atom == xlib.XA_WM_NAME or event.atom == net_wm_name) {
2105
        update_title(display, client);
2106
    } else if (event.atom == net_wm_window_type) {
2107
        update_window_type(display, client);
2108
    }
2109
}
2110
2111
fn unfocus_client(display: *Display, client: ?*Client, reset_input_focus: bool) void {
2112
    const unfocus_target = client orelse return;
2113
    grabbuttons(display, unfocus_target, false);
2114
    _ = xlib.XSetWindowBorder(display.handle, unfocus_target.window, border_color_unfocused);
2115
    if (reset_input_focus) {
2116
        _ = xlib.XSetInputFocus(display.handle, display.root, xlib.RevertToPointerRoot, xlib.CurrentTime);
2117
        _ = xlib.XDeleteProperty(display.handle, display.root, net_active_window);
2118
    }
2119
}
2120
2121
fn set_client_state(display: *Display, client: *Client, state: c_long) void {
2122
    var data: [2]c_long = .{ state, xlib.None };
2123
    _ = xlib.c.XChangeProperty(display.handle, client.window, wm_state, wm_state, 32, xlib.PropModeReplace, @ptrCast(&data), 2);
2124
}
2125
2126
fn update_numlock_mask(display: *Display) void {
2127
    numlock_mask = 0;
2128
    const modmap = xlib.XGetModifierMapping(display.handle);
2129
    if (modmap == null) return;
2130
    defer _ = xlib.XFreeModifiermap(modmap);
2131
2132
    const numlock_keycode = xlib.XKeysymToKeycode(display.handle, xlib.XK_Num_Lock);
2133
2134
    var modifier_index: usize = 0;
2135
    while (modifier_index < 8) : (modifier_index += 1) {
2136
        var key_index: usize = 0;
2137
        while (key_index < @as(usize, @intCast(modmap.*.max_keypermod))) : (key_index += 1) {
2138
            const keycode = modmap.*.modifiermap[modifier_index * @as(usize, @intCast(modmap.*.max_keypermod)) + key_index];
2139
            if (keycode == numlock_keycode) {
2140
                numlock_mask = @as(c_uint, 1) << @intCast(modifier_index);
2141
            }
2142
        }
2143
    }
2144
}
2145
2146
fn grabbuttons(display: *Display, client: *Client, focused: bool) void {
2147
    update_numlock_mask(display);
2148
    const modifiers = [_]c_uint{ 0, xlib.LockMask, numlock_mask, numlock_mask | xlib.LockMask };
2149
2150
    _ = xlib.XUngrabButton(display.handle, xlib.AnyButton, xlib.AnyModifier, client.window);
2151
    if (!focused) {
2152
        _ = xlib.XGrabButton(
2153
            display.handle,
2154
            xlib.AnyButton,
2155
            xlib.AnyModifier,
2156
            client.window,
2157
            xlib.False,
2158
            xlib.ButtonPressMask | xlib.ButtonReleaseMask,
2159
            xlib.GrabModeSync,
2160
            xlib.GrabModeSync,
2161
            xlib.None,
2162
            xlib.None,
2163
        );
2164
    }
2165
    for (config.buttons.items) |button| {
2166
        if (button.click == .client_win) {
2167
            for (modifiers) |modifier| {
2168
                _ = xlib.XGrabButton(
2169
                    display.handle,
2170
                    @intCast(button.button),
2171
                    button.mod_mask | modifier,
2172
                    client.window,
2173
                    xlib.False,
2174
                    xlib.ButtonPressMask | xlib.ButtonReleaseMask,
2175
                    xlib.GrabModeAsync,
2176
                    xlib.GrabModeSync,
2177
                    xlib.None,
2178
                    xlib.None,
2179
                );
2180
            }
2181
        }
2182
    }
2183
}
2184
2185
fn focus(display: *Display, target_client: ?*Client) void {
2186
    const selmon = monitor_mod.selected_monitor orelse return;
2187
2188
    var focus_client = target_client;
2189
    if (focus_client == null or !client_mod.is_visible(focus_client.?)) {
2190
        focus_client = selmon.stack;
2191
        while (focus_client) |iter| {
2192
            if (client_mod.is_visible(iter)) break;
2193
            focus_client = iter.stack_next;
2194
        }
2195
    }
2196
2197
    if (selmon.sel != null and selmon.sel != focus_client) {
2198
        unfocus_client(display, selmon.sel, false);
2199
    }
2200
2201
    if (focus_client) |client| {
2202
        if (client.monitor != selmon) {
2203
            monitor_mod.selected_monitor = client.monitor;
2204
        }
2205
        if (client.is_urgent) {
2206
            set_urgent(display, client, false);
2207
        }
2208
        client_mod.detach_stack(client);
2209
        client_mod.attach_stack(client);
2210
        grabbuttons(display, client, true);
2211
        _ = xlib.XSetWindowBorder(display.handle, client.window, border_color_focused);
2212
        if (!client.never_focus) {
2213
            _ = xlib.XSetInputFocus(display.handle, client.window, xlib.RevertToPointerRoot, xlib.CurrentTime);
2214
            _ = xlib.XChangeProperty(display.handle, display.root, net_active_window, xlib.XA_WINDOW, 32, xlib.PropModeReplace, @ptrCast(&client.window), 1);
2215
        }
2216
        _ = send_event(display, client, wm_take_focus);
2217
    } else {
2218
        _ = xlib.XSetInputFocus(display.handle, display.root, xlib.RevertToPointerRoot, xlib.CurrentTime);
2219
        _ = xlib.XDeleteProperty(display.handle, display.root, net_active_window);
2220
    }
2221
2222
    const current_selmon = monitor_mod.selected_monitor orelse return;
2223
    current_selmon.sel = focus_client;
2224
2225
    if (focus_client) |client| {
2226
        if (is_scrolling_layout(current_selmon)) {
2227
            scroll_to_window(client, true);
2228
        }
2229
    }
2230
2231
    bar_mod.invalidate_bars();
2232
}
2233
2234
fn restack(display: *Display, monitor: *Monitor) void {
2235
    bar_mod.invalidate_bars();
2236
    const selected_client = monitor.sel orelse return;
2237
2238
    if (selected_client.is_floating or monitor.lt[monitor.sel_lt] == null) {
2239
        _ = xlib.XRaiseWindow(display.handle, selected_client.window);
2240
    }
2241
2242
    if (monitor.lt[monitor.sel_lt] != null) {
2243
        var window_changes: xlib.c.XWindowChanges = undefined;
2244
        window_changes.stack_mode = xlib.c.Below;
2245
        window_changes.sibling = monitor.bar_win;
2246
2247
        var current = monitor.stack;
2248
        while (current) |client| {
2249
            if (!client.is_floating and client_mod.is_visible(client)) {
2250
                _ = xlib.c.XConfigureWindow(display.handle, client.window, xlib.c.CWSibling | xlib.c.CWStackMode, &window_changes);
2251
                window_changes.sibling = client.window;
2252
            }
2253
            current = client.stack_next;
2254
        }
2255
    }
2256
2257
    _ = xlib.XSync(display.handle, xlib.False);
2258
2259
    var discard_event: xlib.XEvent = undefined;
2260
    while (xlib.c.XCheckMaskEvent(display.handle, xlib.EnterWindowMask, &discard_event) != 0) {}
2261
}
2262
2263
fn arrange(monitor: *Monitor) void {
2264
    if (display_global) |display| {
2265
        showhide(display, monitor);
2266
    }
2267
    if (monitor.lt[monitor.sel_lt]) |layout| {
2268
        if (layout.arrange_fn) |arrange_fn| {
2269
            arrange_fn(monitor);
2270
        }
2271
    }
2272
    if (display_global) |display| {
2273
        restack(display, monitor);
2274
    }
2275
}
2276
2277
fn tick_animations() void {
2278
    if (!scroll_animation.is_active()) return;
2279
2280
    const monitor = monitor_mod.selected_monitor orelse return;
2281
    if (scroll_animation.update()) |new_offset| {
2282
        monitor.scroll_offset = new_offset;
2283
        arrange(monitor);
2284
    }
2285
}
2286
2287
fn is_scrolling_layout(monitor: *Monitor) bool {
2288
    if (monitor.lt[monitor.sel_lt]) |layout| {
2289
        return layout.arrange_fn == scrolling.layout.arrange_fn;
2290
    }
2291
    return false;
2292
}
2293
2294
fn scroll_layout(direction: i32) void {
2295
    const monitor = monitor_mod.selected_monitor orelse return;
2296
    if (!is_scrolling_layout(monitor)) return;
2297
2298
    const scroll_step = scrolling.get_scroll_step(monitor);
2299
    const max_scroll = scrolling.get_max_scroll(monitor);
2300
2301
    const current = if (scroll_animation.is_active())
2302
        scroll_animation.target()
2303
    else
2304
        monitor.scroll_offset;
2305
2306
    var target = current + direction * scroll_step;
2307
    target = @max(0, @min(target, max_scroll));
2308
2309
    scroll_animation.start(monitor.scroll_offset, target, animation_config);
2310
}
2311
2312
fn scroll_to_window(client: *Client, animate: bool) void {
2313
    const monitor = client.monitor orelse return;
2314
    if (!is_scrolling_layout(monitor)) return;
2315
2316
    const target = scrolling.get_target_scroll_for_window(monitor, client);
2317
2318
    if (animate) {
2319
        scroll_animation.start(monitor.scroll_offset, target, animation_config);
2320
    } else {
2321
        monitor.scroll_offset = target;
2322
        arrange(monitor);
2323
    }
2324
}
2325
2326
fn showhide_client(display: *Display, client: ?*Client) void {
2327
    const target = client orelse return;
2328
    if (client_mod.is_visible(target)) {
2329
        _ = xlib.XMoveWindow(display.handle, target.window, target.x, target.y);
2330
        const monitor = target.monitor orelse return;
2331
        if ((monitor.lt[monitor.sel_lt] == null or target.is_floating) and !target.is_fullscreen) {
2332
            tiling.resize(target, target.x, target.y, target.width, target.height, false);
2333
        }
2334
        showhide_client(display, target.stack_next);
2335
    } else {
2336
        showhide_client(display, target.stack_next);
2337
        const client_width = target.width + 2 * target.border_width;
2338
        _ = xlib.XMoveWindow(display.handle, target.window, -2 * client_width, target.y);
2339
    }
2340
}
2341
2342
fn showhide(display: *Display, monitor: *Monitor) void {
2343
    showhide_client(display, monitor.stack);
2344
}
2345
2346
fn update_size_hints(display: *Display, client: *Client) void {
2347
    var size_hints: xlib.XSizeHints = undefined;
2348
    var msize: c_long = 0;
2349
2350
    if (xlib.XGetWMNormalHints(display.handle, client.window, &size_hints, &msize) == 0) {
2351
        size_hints.flags = xlib.PSize;
2352
    }
2353
2354
    if ((size_hints.flags & xlib.PBaseSize) != 0) {
2355
        client.base_width = size_hints.base_width;
2356
        client.base_height = size_hints.base_height;
2357
    } else if ((size_hints.flags & xlib.PMinSize) != 0) {
2358
        client.base_width = size_hints.min_width;
2359
        client.base_height = size_hints.min_height;
2360
    } else {
2361
        client.base_width = 0;
2362
        client.base_height = 0;
2363
    }
2364
2365
    if ((size_hints.flags & xlib.PResizeInc) != 0) {
2366
        client.increment_width = size_hints.width_inc;
2367
        client.increment_height = size_hints.height_inc;
2368
    } else {
2369
        client.increment_width = 0;
2370
        client.increment_height = 0;
2371
    }
2372
2373
    if ((size_hints.flags & xlib.PMaxSize) != 0) {
2374
        client.max_width = size_hints.max_width;
2375
        client.max_height = size_hints.max_height;
2376
    } else {
2377
        client.max_width = 0;
2378
        client.max_height = 0;
2379
    }
2380
2381
    if ((size_hints.flags & xlib.PMinSize) != 0) {
2382
        client.min_width = size_hints.min_width;
2383
        client.min_height = size_hints.min_height;
2384
    } else if ((size_hints.flags & xlib.PBaseSize) != 0) {
2385
        client.min_width = size_hints.base_width;
2386
        client.min_height = size_hints.base_height;
2387
    } else {
2388
        client.min_width = 0;
2389
        client.min_height = 0;
2390
    }
2391
2392
    if ((size_hints.flags & xlib.PAspect) != 0) {
2393
        client.min_aspect = @as(f32, @floatFromInt(size_hints.min_aspect.y)) / @as(f32, @floatFromInt(size_hints.min_aspect.x));
2394
        client.max_aspect = @as(f32, @floatFromInt(size_hints.max_aspect.x)) / @as(f32, @floatFromInt(size_hints.max_aspect.y));
2395
    } else {
2396
        client.min_aspect = 0.0;
2397
        client.max_aspect = 0.0;
2398
    }
2399
2400
    client.is_fixed = (client.max_width != 0 and client.max_height != 0 and client.max_width == client.min_width and client.max_height == client.min_height);
2401
    client.hints_valid = true;
2402
}
2403
2404
fn update_wm_hints(display: *Display, client: *Client) void {
2405
    const wmh = xlib.XGetWMHints(display.handle, client.window);
2406
    if (wmh) |hints| {
2407
        defer _ = xlib.XFree(@ptrCast(hints));
2408
2409
        if (client == (monitor_mod.selected_monitor orelse return).sel and (hints.*.flags & xlib.XUrgencyHint) != 0) {
2410
            hints.*.flags = hints.*.flags & ~@as(c_long, xlib.XUrgencyHint);
2411
            _ = xlib.XSetWMHints(display.handle, client.window, hints);
2412
        } else {
2413
            client.is_urgent = (hints.*.flags & xlib.XUrgencyHint) != 0;
2414
        }
2415
2416
        if ((hints.*.flags & xlib.InputHint) != 0) {
2417
            client.never_focus = hints.*.input == 0;
2418
        } else {
2419
            client.never_focus = false;
2420
        }
2421
    }
2422
}
2423
2424
fn update_window_type(display: *Display, client: *Client) void {
2425
    const state = get_atom_prop(display, client, net_wm_state);
2426
    const window_type = get_atom_prop(display, client, net_wm_window_type);
2427
2428
    if (state == net_wm_state_fullscreen) {
2429
        set_fullscreen(display, client, true);
2430
    }
2431
    if (window_type == net_wm_window_type_dialog) {
2432
        client.is_floating = true;
2433
    }
2434
}
2435
2436
fn update_title(display: *Display, client: *Client) void {
2437
    if (!get_text_prop(display, client.window, net_wm_name, &client.name)) {
2438
        _ = get_text_prop(display, client.window, xlib.XA_WM_NAME, &client.name);
2439
    }
2440
    if (client.name[0] == 0) {
2441
        @memcpy(client.name[0..6], "broken");
2442
    }
2443
}
2444
2445
fn get_atom_prop(display: *Display, client: *Client, prop: xlib.Atom) xlib.Atom {
2446
    var actual_type: xlib.Atom = undefined;
2447
    var actual_format: c_int = undefined;
2448
    var num_items: c_ulong = undefined;
2449
    var bytes_after: c_ulong = undefined;
2450
    var prop_data: [*c]u8 = undefined;
2451
2452
    if (xlib.XGetWindowProperty(display.handle, client.window, prop, 0, @sizeOf(xlib.Atom), xlib.False, xlib.XA_ATOM, &actual_type, &actual_format, &num_items, &bytes_after, &prop_data) == 0 and prop_data != null) {
2453
        const atom: xlib.Atom = @as(*xlib.Atom, @ptrCast(@alignCast(prop_data))).*;
2454
        _ = xlib.XFree(@ptrCast(prop_data));
2455
        return atom;
2456
    }
2457
    return 0;
2458
}
2459
2460
fn get_text_prop(display: *Display, window: xlib.Window, atom: xlib.Atom, text: *[256]u8) bool {
2461
    var name: xlib.XTextProperty = undefined;
2462
    text[0] = 0;
2463
2464
    if (xlib.XGetTextProperty(display.handle, window, &name, atom) == 0 or name.nitems == 0) {
2465
        return false;
2466
    }
2467
2468
    if (name.encoding == xlib.XA_STRING) {
2469
        const len = @min(name.nitems, 255);
2470
        @memcpy(text[0..len], name.value[0..len]);
2471
        text[len] = 0;
2472
    } else {
2473
        var list: [*c][*c]u8 = undefined;
2474
        var count: c_int = undefined;
2475
        if (xlib.XmbTextPropertyToTextList(display.handle, &name, &list, &count) >= xlib.Success and count > 0 and list[0] != null) {
2476
            const str = std.mem.sliceTo(list[0], 0);
2477
            const copy_len = @min(str.len, 255);
2478
            @memcpy(text[0..copy_len], str[0..copy_len]);
2479
            text[copy_len] = 0;
2480
            xlib.XFreeStringList(list);
2481
        }
2482
    }
2483
    text[255] = 0;
2484
    _ = xlib.XFree(@ptrCast(name.value));
2485
    return true;
2486
}
2487
2488
fn apply_rules(display: *Display, client: *Client) void {
2489
    var class_hint: xlib.XClassHint = .{ .res_name = null, .res_class = null };
2490
    _ = xlib.XGetClassHint(display.handle, client.window, &class_hint);
2491
2492
    const class_str: []const u8 = if (class_hint.res_class) |ptr| std.mem.sliceTo(ptr, 0) else "";
2493
    const instance_str: []const u8 = if (class_hint.res_name) |ptr| std.mem.sliceTo(ptr, 0) else "";
2494
2495
    client.is_floating = false;
2496
    client.tags = 0;
2497
    var rule_focus = false;
2498
2499
    for (config.rules.items) |rule| {
2500
        const class_matches = if (rule.class) |rc| std.mem.indexOf(u8, class_str, rc) != null else true;
2501
        const instance_matches = if (rule.instance) |ri| std.mem.indexOf(u8, instance_str, ri) != null else true;
2502
        const title_matches = if (rule.title) |rt| std.mem.indexOf(u8, std.mem.sliceTo(&client.name, 0), rt) != null else true;
2503
2504
        if (class_matches and instance_matches and title_matches) {
2505
            client.is_floating = rule.is_floating;
2506
            client.tags |= rule.tags;
2507
            if (rule.monitor >= 0) {
2508
                var target = monitor_mod.monitors;
2509
                var index: i32 = 0;
2510
                while (target) |mon| {
2511
                    if (index == rule.monitor) {
2512
                        client.monitor = mon;
2513
                        break;
2514
                    }
2515
                    index += 1;
2516
                    target = mon.next;
2517
                }
2518
            }
2519
            if (rule.focus) {
2520
                rule_focus = true;
2521
            }
2522
        }
2523
    }
2524
2525
    if (class_hint.res_class) |ptr| {
2526
        _ = xlib.XFree(@ptrCast(ptr));
2527
    }
2528
    if (class_hint.res_name) |ptr| {
2529
        _ = xlib.XFree(@ptrCast(ptr));
2530
    }
2531
2532
    const monitor = client.monitor orelse return;
2533
    if (client.tags == 0) {
2534
        client.tags = monitor.tagset[monitor.sel_tags];
2535
    }
2536
2537
    if (rule_focus and client.tags != 0) {
2538
        const monitor_tagset = monitor.tagset[monitor.sel_tags];
2539
        const is_tag_focused = (monitor_tagset & client.tags) == client.tags;
2540
        if (!is_tag_focused) {
2541
            view(display, client.tags);
2542
        }
2543
    }
2544
}
2545
2546
fn update_client_list(display: *Display) void {
2547
    _ = xlib.XDeleteProperty(display.handle, display.root, net_client_list);
2548
2549
    var current_monitor = monitor_mod.monitors;
2550
    while (current_monitor) |monitor| {
2551
        var current_client = monitor.clients;
2552
        while (current_client) |client| {
2553
            _ = xlib.XChangeProperty(display.handle, display.root, net_client_list, xlib.XA_WINDOW, 32, xlib.PropModeAppend, @ptrCast(&client.window), 1);
2554
            current_client = client.next;
2555
        }
2556
        current_monitor = monitor.next;
2557
    }
2558
}
2559
2560
fn send_event(display: *Display, client: *Client, protocol: xlib.Atom) bool {
2561
    var protocols: [*c]xlib.Atom = undefined;
2562
    var num_protocols: c_int = 0;
2563
    var exists = false;
2564
2565
    if (xlib.XGetWMProtocols(display.handle, client.window, &protocols, &num_protocols) != 0) {
2566
        var index: usize = 0;
2567
        while (index < @as(usize, @intCast(num_protocols))) : (index += 1) {
2568
            if (protocols[index] == protocol) {
2569
                exists = true;
2570
                break;
2571
            }
2572
        }
2573
        _ = xlib.XFree(@ptrCast(protocols));
2574
    }
2575
2576
    if (exists) {
2577
        var event: xlib.XEvent = undefined;
2578
        event.type = xlib.ClientMessage;
2579
        event.xclient.window = client.window;
2580
        event.xclient.message_type = wm_protocols;
2581
        event.xclient.format = 32;
2582
        event.xclient.data.l[0] = @intCast(protocol);
2583
        event.xclient.data.l[1] = xlib.CurrentTime;
2584
        _ = xlib.XSendEvent(display.handle, client.window, xlib.False, xlib.NoEventMask, &event);
2585
    }
2586
    return exists;
2587
}
2588
2589
fn set_urgent(display: *Display, client: *Client, urgent: bool) void {
2590
    client.is_urgent = urgent;
2591
    const wmh = xlib.XGetWMHints(display.handle, client.window);
2592
    if (wmh) |hints| {
2593
        if (urgent) {
2594
            hints.*.flags = hints.*.flags | xlib.XUrgencyHint;
2595
        } else {
2596
            hints.*.flags = hints.*.flags & ~@as(c_long, xlib.XUrgencyHint);
2597
        }
2598
        _ = xlib.XSetWMHints(display.handle, client.window, hints);
2599
        _ = xlib.XFree(@ptrCast(hints));
2600
    }
2601
}
2602
2603
fn run_autostart_commands(_: std.mem.Allocator, commands: []const []const u8) !void {
2604
    for (commands) |cmd| spawn_command(cmd);
2605
}
2606
2607
test {
2608
    _ = @import("x11/events.zig");
2609
}