oxwm

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