oxwm

https://git.tonybtw.com/oxwm.git git://git.tonybtw.com/oxwm.git
93,203 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
45
var cursor_normal: xlib.Cursor = 0;
46
var cursor_resize: xlib.Cursor = 0;
47
var cursor_move: xlib.Cursor = 0;
48
49
const NormalState: c_long = 1;
50
const WithdrawnState: c_long = 0;
51
const IconicState: c_long = 3;
52
const IsViewable: c_int = 2;
53
const snap_distance: i32 = 32;
54
55
var numlock_mask: c_uint = 0;
56
57
var config: config_mod.Config = undefined;
58
var display_global: ?*Display = null;
59
var config_path_global: ?[]const u8 = null;
60
61
var scroll_animation: animations.Scroll_Animation = .{};
62
var animation_config: animations.Animation_Config = .{ .duration_ms = 150, .easing = .ease_out };
63
64
var chord_keys: [4]config_mod.Key_Press = [_]config_mod.Key_Press{.{}} ** 4;
65
var chord_index: u8 = 0;
66
var chord_timestamp: i64 = 0;
67
const chord_timeout_ms: i64 = 1000;
68
var keyboard_grabbed: bool = false;
69
70
var keybind_overlay: ?*overlay_mod.Keybind_Overlay = null;
71
72
fn print_help() void {
73
    std.debug.print(
74
        \\oxwm - A window manager
75
        \\
76
        \\USAGE:
77
        \\    oxwm [OPTIONS]
78
        \\
79
        \\OPTIONS:
80
        \\    --init              Create default config in ~/.config/oxwm/config.lua
81
        \\    --config <PATH>     Use custom config file
82
        \\    --validate          Validate config file without starting window manager
83
        \\    --version           Print version information
84
        \\    --help              Print this help message
85
        \\
86
        \\CONFIG:
87
        \\    Location: ~/.config/oxwm/config.lua
88
        \\    Edit the config file and use Mod+Shift+R to reload
89
        \\    No compilation needed - instant hot-reload!
90
        \\
91
        \\FIRST RUN:
92
        \\    Run 'oxwm --init' to create a config file
93
        \\    Or just start oxwm and it will create one automatically
94
        \\
95
    , .{});
96
}
97
98
fn get_config_path(allocator: std.mem.Allocator) ![]u8 {
99
    const config_home = std.posix.getenv("XDG_CONFIG_HOME") orelse blk: {
100
        const home = std.posix.getenv("HOME") orelse return error.CouldNotGetHomeDir;
101
        break :blk try std.fs.path.join(allocator, &.{ home, ".config" });
102
    };
103
    defer if (std.posix.getenv("XDG_CONFIG_HOME") == null) allocator.free(config_home);
104
105
    const config_path = try std.fs.path.join(allocator, &.{ config_home, "oxwm", "config.lua" });
106
    return config_path;
107
}
108
109
fn init_config(allocator: std.mem.Allocator) void {
110
    const config_path = get_config_path(allocator) catch return;
111
    defer allocator.free(config_path);
112
113
    const template = @embedFile("templates/config.lua");
114
115
    if (std.fs.path.dirname(config_path)) |dir_path| {
116
        var root = std.fs.openDirAbsolute("/", .{}) catch |err| {
117
            std.debug.print("error: could not open root directory: {}\n", .{err});
118
            return;
119
        };
120
        defer root.close();
121
122
        const relative_path = std.mem.trimLeft(u8, dir_path, "/");
123
        root.makePath(relative_path) catch |err| {
124
            std.debug.print("error: could not create config directory: {}\n", .{err});
125
            return;
126
        };
127
    }
128
129
    const file = std.fs.createFileAbsolute(config_path, .{}) catch |err| {
130
        std.debug.print("error: could not create config file: {}\n", .{err});
131
        return;
132
    };
133
    defer file.close();
134
135
    _ = file.writeAll(template) catch |err| {
136
        std.debug.print("error: could not write config file: {}\n", .{err});
137
        return;
138
    };
139
140
    std.debug.print("Config created at {s}\n", .{config_path});
141
    std.debug.print("Edit the file and reload with Mod+Shift+R\n", .{});
142
    std.debug.print("No compilation needed - changes take effect immediately!\n", .{});
143
}
144
145
fn validate_config(allocator: std.mem.Allocator, config_path: []const u8) !void {
146
    config = config_mod.Config.init(allocator);
147
    defer config.deinit();
148
149
    if (!lua.init(&config)) {
150
        std.debug.print("error: failed to initialize lua\n", .{});
151
        std.process.exit(1);
152
    }
153
    defer lua.deinit();
154
155
    _ = std.fs.cwd().statFile(config_path) catch |err| {
156
        std.debug.print("error: config file not found: {s}\n", .{config_path});
157
        std.debug.print("  {}\n", .{err});
158
        std.process.exit(1);
159
    };
160
161
    if (lua.load_file(config_path)) {
162
        std.debug.print("✓ config valid: {s}\n", .{config_path});
163
        std.process.exit(0);
164
    } else {
165
        std.debug.print("✗ config validation failed\n", .{});
166
        std.process.exit(1);
167
    }
168
}
169
170
pub fn main() !void {
171
    const allocator = gpa.allocator();
172
    defer _ = gpa.deinit();
173
174
    const default_config_path = try get_config_path(allocator);
175
    defer allocator.free(default_config_path);
176
177
    var config_path: []const u8 = default_config_path;
178
    var validate_mode: bool = false;
179
    var args = std.process.args();
180
    _ = args.skip();
181
    while (args.next()) |arg| {
182
        if (std.mem.eql(u8, arg, "-c") or std.mem.eql(u8, arg, "--config")) {
183
            if (args.next()) |path| config_path = path;
184
        } else if (std.mem.eql(u8, arg, "-h") or std.mem.eql(u8, arg, "--help")) {
185
            print_help();
186
            return;
187
        } else if (std.mem.eql(u8, arg, "-v") or std.mem.eql(u8, arg, "--version")) {
188
            std.debug.print("{s}\n", .{VERSION});
189
            return;
190
        } else if (std.mem.eql(u8, arg, "--init")) {
191
            init_config(allocator);
192
            return;
193
        } else if (std.mem.eql(u8, arg, "--validate")) {
194
            validate_mode = true;
195
        }
196
    }
197
198
    if (validate_mode) {
199
        try validate_config(allocator, config_path);
200
        return;
201
    }
202
203
    std.debug.print("oxwm starting\n", .{});
204
205
    config = config_mod.Config.init(allocator);
206
    defer config.deinit();
207
    config_mod.set_config(&config);
208
209
    if (lua.init(&config)) {
210
        const loaded = if (std.fs.cwd().statFile(config_path)) |_|
211
            lua.load_file(config_path)
212
        else |_| blk: {
213
            init_config(allocator);
214
            break :blk lua.load_config();
215
        };
216
217
        if (loaded) {
218
            config_path_global = config_path;
219
            std.debug.print("loaded config from {s}\n", .{config_path});
220
        } else {
221
            std.debug.print("no config found, using defaults\n", .{});
222
            initialize_default_config();
223
        }
224
    } else {
225
        std.debug.print("failed to init lua, using defaults\n", .{});
226
        initialize_default_config();
227
    }
228
229
    var display = Display.open() catch |err| {
230
        std.debug.print("failed to open display: {}\n", .{err});
231
        return;
232
    };
233
    defer display.close();
234
235
    display_global = &display;
236
237
    std.debug.print("display opened: screen={d} root=0x{x}\n", .{ display.screen, display.root });
238
    std.debug.print("screen size: {d}x{d}\n", .{ display.screen_width(), display.screen_height() });
239
240
    display.become_window_manager() catch |err| {
241
        std.debug.print("failed to become window manager: {}\n", .{err});
242
        return;
243
    };
244
245
    std.debug.print("successfully became window manager\n", .{});
246
247
    setup_atoms(&display);
248
    setup_cursors(&display);
249
    client_mod.init(allocator);
250
    monitor_mod.init(allocator);
251
    monitor_mod.set_root_window(display.root, display.handle);
252
    tiling.set_display(display.handle);
253
    tiling.set_screen_size(display.screen_width(), display.screen_height());
254
255
    setup_monitors(&display);
256
    setup_bars(allocator, &display);
257
    setup_overlay(allocator, &display);
258
    grab_keybinds(&display);
259
    scan_existing_windows(&display);
260
261
    try run_autostart_commands(allocator, config.autostart.items);
262
    std.debug.print("entering event loop\n", .{});
263
    run_event_loop(&display);
264
265
    bar_mod.destroy_bars(allocator, display.handle);
266
267
    var mon = monitor_mod.monitors;
268
    while (mon) |m| {
269
        const next = m.next;
270
        monitor_mod.destroy(m);
271
        mon = next;
272
    }
273
274
    if (keybind_overlay) |overlay| {
275
        overlay.deinit(allocator);
276
    }
277
278
    lua.deinit();
279
    std.debug.print("oxwm exiting\n", .{});
280
}
281
282
fn setup_atoms(display: *Display) void {
283
    wm_protocols = xlib.XInternAtom(display.handle, "WM_PROTOCOLS", xlib.False);
284
    wm_delete = xlib.XInternAtom(display.handle, "WM_DELETE_WINDOW", xlib.False);
285
    wm_state = xlib.XInternAtom(display.handle, "WM_STATE", xlib.False);
286
    wm_take_focus = xlib.XInternAtom(display.handle, "WM_TAKE_FOCUS", xlib.False);
287
288
    net_active_window = xlib.XInternAtom(display.handle, "_NET_ACTIVE_WINDOW", xlib.False);
289
    net_supported = xlib.XInternAtom(display.handle, "_NET_SUPPORTED", xlib.False);
290
    net_wm_name = xlib.XInternAtom(display.handle, "_NET_WM_NAME", xlib.False);
291
    net_wm_state = xlib.XInternAtom(display.handle, "_NET_WM_STATE", xlib.False);
292
    net_wm_check = xlib.XInternAtom(display.handle, "_NET_SUPPORTING_WM_CHECK", xlib.False);
293
    net_wm_state_fullscreen = xlib.XInternAtom(display.handle, "_NET_WM_STATE_FULLSCREEN", xlib.False);
294
    net_wm_window_type = xlib.XInternAtom(display.handle, "_NET_WM_WINDOW_TYPE", xlib.False);
295
    net_wm_window_type_dialog = xlib.XInternAtom(display.handle, "_NET_WM_WINDOW_TYPE_DIALOG", xlib.False);
296
    net_client_list = xlib.XInternAtom(display.handle, "_NET_CLIENT_LIST", xlib.False);
297
298
    const utf8_string = xlib.XInternAtom(display.handle, "UTF8_STRING", xlib.False);
299
300
    wm_check_window = xlib.XCreateSimpleWindow(display.handle, display.root, 0, 0, 1, 1, 0, 0, 0);
301
    _ = xlib.XChangeProperty(display.handle, wm_check_window, net_wm_check, xlib.XA_WINDOW, 32, xlib.PropModeReplace, @ptrCast(&wm_check_window), 1);
302
    _ = xlib.XChangeProperty(display.handle, wm_check_window, net_wm_name, utf8_string, 8, xlib.PropModeReplace, "oxwm", 6);
303
    _ = xlib.XChangeProperty(display.handle, display.root, net_wm_check, xlib.XA_WINDOW, 32, xlib.PropModeReplace, @ptrCast(&wm_check_window), 1);
304
305
    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 };
306
    _ = xlib.XChangeProperty(display.handle, display.root, net_supported, xlib.XA_ATOM, 32, xlib.PropModeReplace, @ptrCast(&net_atoms), net_atoms.len);
307
308
    _ = xlib.XDeleteProperty(display.handle, display.root, net_client_list);
309
310
    std.debug.print("atoms initialized with EWMH support\n", .{});
311
}
312
313
fn setup_cursors(display: *Display) void {
314
    cursor_normal = xlib.XCreateFontCursor(display.handle, xlib.XC_left_ptr);
315
    cursor_resize = xlib.XCreateFontCursor(display.handle, xlib.XC_sizing);
316
    cursor_move = xlib.XCreateFontCursor(display.handle, xlib.XC_fleur);
317
    _ = xlib.XDefineCursor(display.handle, display.root, cursor_normal);
318
}
319
320
fn setup_bars(allocator: std.mem.Allocator, display: *Display) void {
321
    var current_monitor = monitor_mod.monitors;
322
    var last_bar: ?*bar_mod.Bar = null;
323
324
    while (current_monitor) |monitor| {
325
        const bar = bar_mod.Bar.create(allocator, display.handle, display.screen, monitor, config);
326
        if (bar) |created_bar| {
327
            if (tiling.bar_height == 0) {
328
                tiling.set_bar_height(created_bar.height);
329
            }
330
331
            if (config.blocks.items.len > 0) {
332
                for (config.blocks.items) |cfg_block| {
333
                    const block = config_block_to_bar_block(cfg_block);
334
                    created_bar.add_block(block);
335
                }
336
            } else {
337
                created_bar.add_block(blocks_mod.Block.init_ram("", 5, 0x7aa2f7, true));
338
                created_bar.add_block(blocks_mod.Block.init_static(" | ", 0x666666, false));
339
                created_bar.add_block(blocks_mod.Block.init_datetime("", "%H:%M", 1, 0x0db9d7, true));
340
            }
341
342
            if (last_bar) |prev| {
343
                prev.next = created_bar;
344
            } else {
345
                bar_mod.bars = created_bar;
346
            }
347
            last_bar = created_bar;
348
            std.debug.print("bar created for monitor {d}\n", .{monitor.num});
349
        }
350
        current_monitor = monitor.next;
351
    }
352
}
353
354
fn setup_overlay(allocator: std.mem.Allocator, display: *Display) void {
355
    keybind_overlay = overlay_mod.Keybind_Overlay.init(display.handle, display.screen, display.root, config.font, allocator);
356
}
357
358
fn config_block_to_bar_block(cfg: config_mod.Block) blocks_mod.Block {
359
    return switch (cfg.block_type) {
360
        .static => blocks_mod.Block.init_static(cfg.format, cfg.color, cfg.underline),
361
        .datetime => blocks_mod.Block.init_datetime(cfg.format, cfg.datetime_format orelse "%H:%M", cfg.interval, cfg.color, cfg.underline),
362
        .ram => blocks_mod.Block.init_ram(cfg.format, cfg.interval, cfg.color, cfg.underline),
363
        .shell => blocks_mod.Block.init_shell(cfg.format, cfg.command orelse "", cfg.interval, cfg.color, cfg.underline),
364
        .battery => blocks_mod.Block.init_battery(
365
            cfg.format_charging orelse "",
366
            cfg.format_discharging orelse "",
367
            cfg.format_full orelse "",
368
            cfg.battery_name orelse "BAT0",
369
            cfg.interval,
370
            cfg.color,
371
            cfg.underline,
372
        ),
373
        .cpu_temp => blocks_mod.Block.init_cpu_temp(
374
            cfg.format,
375
            cfg.thermal_zone orelse "thermal_zone0",
376
            cfg.interval,
377
            cfg.color,
378
            cfg.underline,
379
        ),
380
    };
381
}
382
383
fn setup_monitors(display: *Display) void {
384
    std.debug.print("checking xinerama...\n", .{});
385
    if (xlib.XineramaIsActive(display.handle) != 0) {
386
        std.debug.print("xinerama is active!\n", .{});
387
        var screen_count: c_int = 0;
388
        const screens = xlib.XineramaQueryScreens(display.handle, &screen_count);
389
390
        if (screen_count > 0 and screens != null) {
391
            var prev_monitor: ?*Monitor = null;
392
            var index: usize = 0;
393
394
            while (index < @as(usize, @intCast(screen_count))) : (index += 1) {
395
                const screen = screens[index];
396
                const mon = monitor_mod.create() orelse continue;
397
398
                mon.num = @intCast(index);
399
                mon.mon_x = screen.x_org;
400
                mon.mon_y = screen.y_org;
401
                mon.mon_w = screen.width;
402
                mon.mon_h = screen.height;
403
                mon.win_x = screen.x_org;
404
                mon.win_y = screen.y_org;
405
                mon.win_w = screen.width;
406
                mon.win_h = screen.height;
407
                mon.lt[0] = &tiling.layout;
408
                mon.lt[1] = &monocle.layout;
409
                mon.lt[2] = &floating.layout;
410
                mon.lt[3] = &scrolling.layout;
411
                mon.lt[4] = &grid.layout;
412
                for (0..10) |i| {
413
                    mon.pertag.ltidxs[i][0] = mon.lt[0];
414
                    mon.pertag.ltidxs[i][1] = mon.lt[1];
415
                    mon.pertag.ltidxs[i][2] = mon.lt[2];
416
                    mon.pertag.ltidxs[i][3] = mon.lt[3];
417
                    mon.pertag.ltidxs[i][4] = mon.lt[4];
418
                }
419
420
                init_monitor_gaps(mon);
421
422
                if (prev_monitor) |prev| {
423
                    prev.next = mon;
424
                } else {
425
                    monitor_mod.monitors = mon;
426
                }
427
                prev_monitor = mon;
428
429
                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 });
430
            }
431
432
            monitor_mod.selected_monitor = monitor_mod.monitors;
433
            _ = xlib.XFree(@ptrCast(screens));
434
            return;
435
        }
436
    } else {
437
        std.debug.print("xinerama not active, using single monitor\n", .{});
438
    }
439
440
    const mon = monitor_mod.create() orelse return;
441
    mon.mon_x = 0;
442
    mon.mon_y = 0;
443
    mon.mon_w = display.screen_width();
444
    mon.mon_h = display.screen_height();
445
    mon.win_x = 0;
446
    mon.win_y = 0;
447
    mon.win_w = display.screen_width();
448
    mon.win_h = display.screen_height();
449
    mon.lt[0] = &tiling.layout;
450
    mon.lt[1] = &monocle.layout;
451
    mon.lt[2] = &floating.layout;
452
    mon.lt[3] = &scrolling.layout;
453
    mon.lt[4] = &grid.layout;
454
    for (0..10) |i| {
455
        mon.pertag.ltidxs[i][0] = mon.lt[0];
456
        mon.pertag.ltidxs[i][1] = mon.lt[1];
457
        mon.pertag.ltidxs[i][2] = mon.lt[2];
458
        mon.pertag.ltidxs[i][3] = mon.lt[3];
459
        mon.pertag.ltidxs[i][4] = mon.lt[4];
460
    }
461
462
    init_monitor_gaps(mon);
463
464
    monitor_mod.monitors = mon;
465
    monitor_mod.selected_monitor = mon;
466
    std.debug.print("monitor created: {d}x{d}\n", .{ mon.mon_w, mon.mon_h });
467
}
468
469
fn init_monitor_gaps(mon: *Monitor) void {
470
    const any_gap_nonzero = config.gap_inner_h != 0 or config.gap_inner_v != 0 or
471
                            config.gap_outer_h != 0 or config.gap_outer_v != 0;
472
473
    if (config.gaps_enabled and any_gap_nonzero) {
474
        mon.gap_inner_h = config.gap_inner_h;
475
        mon.gap_inner_v = config.gap_inner_v;
476
        mon.gap_outer_h = config.gap_outer_h;
477
        mon.gap_outer_v = config.gap_outer_v;
478
    } else {
479
        mon.gap_inner_h = 0;
480
        mon.gap_inner_v = 0;
481
        mon.gap_outer_h = 0;
482
        mon.gap_outer_v = 0;
483
    }
484
}
485
486
fn make_keybind(mod: u32, key: u64, action: config_mod.Action) config_mod.Keybind {
487
    var kb: config_mod.Keybind = .{ .action = action };
488
    kb.keys[0] = .{ .mod_mask = mod, .keysym = key };
489
    kb.key_count = 1;
490
    return kb;
491
}
492
493
fn make_keybind_int(mod: u32, key: u64, action: config_mod.Action, int_arg: i32) config_mod.Keybind {
494
    var kb = make_keybind(mod, key, action);
495
    kb.int_arg = int_arg;
496
    return kb;
497
}
498
499
fn make_keybind_str(mod: u32, key: u64, action: config_mod.Action, str_arg: []const u8) config_mod.Keybind {
500
    var kb = make_keybind(mod, key, action);
501
    kb.str_arg = str_arg;
502
    return kb;
503
}
504
505
fn initialize_default_config() void {
506
    const mod_key: u32 = 1 << 6;
507
    const shift_key: u32 = 1 << 0;
508
    const control_key: u32 = 1 << 2;
509
510
    config.add_keybind(make_keybind(mod_key, 0xff0d, .spawn_terminal)) catch {};
511
    config.add_keybind(make_keybind_str(mod_key, 'd', .spawn, "rofi -show drun")) catch {};
512
    config.add_keybind(make_keybind_str(mod_key, 's', .spawn, "maim -s | xclip -selection clipboard -t image/png")) catch {};
513
    config.add_keybind(make_keybind(mod_key, 'q', .kill_client)) catch {};
514
    config.add_keybind(make_keybind(mod_key | shift_key, 'q', .quit)) catch {};
515
    config.add_keybind(make_keybind(mod_key | shift_key, 'r', .reload_config)) catch {};
516
    config.add_keybind(make_keybind(mod_key, 'j', .focus_next)) catch {};
517
    config.add_keybind(make_keybind(mod_key, 'k', .focus_prev)) catch {};
518
    config.add_keybind(make_keybind(mod_key | shift_key, 'j', .move_next)) catch {};
519
    config.add_keybind(make_keybind(mod_key | shift_key, 'k', .move_prev)) catch {};
520
    config.add_keybind(make_keybind_int(mod_key, 'h', .resize_master, -50)) catch {};
521
    config.add_keybind(make_keybind_int(mod_key, 'l', .resize_master, 50)) catch {};
522
    config.add_keybind(make_keybind(mod_key, 'i', .inc_master)) catch {};
523
    config.add_keybind(make_keybind(mod_key, 'p', .dec_master)) catch {};
524
    config.add_keybind(make_keybind(mod_key, 'a', .toggle_gaps)) catch {};
525
    config.add_keybind(make_keybind(mod_key, 'f', .toggle_fullscreen)) catch {};
526
    config.add_keybind(make_keybind(mod_key, 0x0020, .toggle_floating)) catch {};
527
    config.add_keybind(make_keybind(mod_key, 'n', .cycle_layout)) catch {};
528
    config.add_keybind(make_keybind_int(mod_key, 0x002c, .focus_monitor, -1)) catch {};
529
    config.add_keybind(make_keybind_int(mod_key, 0x002e, .focus_monitor, 1)) catch {};
530
    config.add_keybind(make_keybind_int(mod_key | shift_key, 0x002c, .send_to_monitor, -1)) catch {};
531
    config.add_keybind(make_keybind_int(mod_key | shift_key, 0x002e, .send_to_monitor, 1)) catch {};
532
533
    var tag_index: i32 = 0;
534
    while (tag_index < 9) : (tag_index += 1) {
535
        const keysym: u64 = @as(u64, '1') + @as(u64, @intCast(tag_index));
536
        config.add_keybind(make_keybind_int(mod_key, keysym, .view_tag, tag_index)) catch {};
537
        config.add_keybind(make_keybind_int(mod_key | shift_key, keysym, .move_to_tag, tag_index)) catch {};
538
        config.add_keybind(make_keybind_int(mod_key | control_key, keysym, .toggle_view_tag, tag_index)) catch {};
539
        config.add_keybind(make_keybind_int(mod_key | control_key | shift_key, keysym, .toggle_tag, tag_index)) catch {};
540
    }
541
542
    config.add_button(.{ .click = .client_win, .mod_mask = mod_key, .button = 1, .action = .move_mouse }) catch {};
543
    config.add_button(.{ .click = .client_win, .mod_mask = mod_key, .button = 3, .action = .resize_mouse }) catch {};
544
}
545
546
fn grab_keybinds(display: *Display) void {
547
    update_numlock_mask(display);
548
    const modifiers = [_]c_uint{ 0, xlib.LockMask, numlock_mask, numlock_mask | xlib.LockMask };
549
550
    _ = xlib.XUngrabKey(display.handle, xlib.AnyKey, xlib.AnyModifier, display.root);
551
552
    for (config.keybinds.items) |keybind| {
553
        if (keybind.key_count == 0) continue;
554
        const first_key = keybind.keys[0];
555
        const keycode = xlib.XKeysymToKeycode(display.handle, @intCast(first_key.keysym));
556
        if (keycode != 0) {
557
            for (modifiers) |modifier| {
558
                _ = xlib.XGrabKey(
559
                    display.handle,
560
                    keycode,
561
                    first_key.mod_mask | modifier,
562
                    display.root,
563
                    xlib.True,
564
                    xlib.GrabModeAsync,
565
                    xlib.GrabModeAsync,
566
                );
567
            }
568
        }
569
    }
570
571
    for (config.buttons.items) |button| {
572
        if (button.click == .client_win) {
573
            for (modifiers) |modifier| {
574
                _ = xlib.XGrabButton(
575
                    display.handle,
576
                    @intCast(button.button),
577
                    button.mod_mask | modifier,
578
                    display.root,
579
                    xlib.True,
580
                    xlib.ButtonPressMask | xlib.ButtonReleaseMask | xlib.PointerMotionMask,
581
                    xlib.GrabModeAsync,
582
                    xlib.GrabModeAsync,
583
                    xlib.None,
584
                    xlib.None,
585
                );
586
            }
587
        }
588
    }
589
590
    std.debug.print("grabbed {d} keybinds from config\n", .{config.keybinds.items.len});
591
}
592
593
fn get_state(display: *Display, window: xlib.Window) c_long {
594
    var actual_type: xlib.Atom = 0;
595
    var actual_format: c_int = 0;
596
    var num_items: c_ulong = 0;
597
    var bytes_after: c_ulong = 0;
598
    var prop: [*c]u8 = null;
599
600
    const result = xlib.XGetWindowProperty(
601
        display.handle,
602
        window,
603
        wm_state,
604
        0,
605
        2,
606
        xlib.False,
607
        wm_state,
608
        &actual_type,
609
        &actual_format,
610
        &num_items,
611
        &bytes_after,
612
        &prop,
613
    );
614
615
    if (result != 0 or actual_type != wm_state or num_items < 1) {
616
        if (prop != null) {
617
            _ = xlib.XFree(prop);
618
        }
619
        return WithdrawnState;
620
    }
621
622
    const state: c_long = @as(*c_long, @ptrCast(@alignCast(prop))).*;
623
    _ = xlib.XFree(prop);
624
    return state;
625
}
626
627
fn scan_existing_windows(display: *Display) void {
628
    var root_return: xlib.Window = undefined;
629
    var parent_return: xlib.Window = undefined;
630
    var children: [*c]xlib.Window = undefined;
631
    var num_children: c_uint = undefined;
632
633
    if (xlib.XQueryTree(display.handle, display.root, &root_return, &parent_return, &children, &num_children) == 0) {
634
        return;
635
    }
636
637
    var index: c_uint = 0;
638
    while (index < num_children) : (index += 1) {
639
        var window_attrs: xlib.XWindowAttributes = undefined;
640
        if (xlib.XGetWindowAttributes(display.handle, children[index], &window_attrs) == 0) {
641
            continue;
642
        }
643
        if (window_attrs.override_redirect != 0) {
644
            continue;
645
        }
646
        var trans: xlib.Window = 0;
647
        if (xlib.XGetTransientForHint(display.handle, children[index], &trans) != 0) {
648
            continue;
649
        }
650
        if (window_attrs.map_state == IsViewable or get_state(display, children[index]) == IconicState) {
651
            manage(display, children[index], &window_attrs);
652
        }
653
    }
654
655
    index = 0;
656
    while (index < num_children) : (index += 1) {
657
        var window_attrs: xlib.XWindowAttributes = undefined;
658
        if (xlib.XGetWindowAttributes(display.handle, children[index], &window_attrs) == 0) {
659
            continue;
660
        }
661
        var trans: xlib.Window = 0;
662
        if (xlib.XGetTransientForHint(display.handle, children[index], &trans) != 0) {
663
            if (window_attrs.map_state == IsViewable or get_state(display, children[index]) == IconicState) {
664
                manage(display, children[index], &window_attrs);
665
            }
666
        }
667
    }
668
669
    if (children != null) {
670
        _ = xlib.XFree(@ptrCast(children));
671
    }
672
}
673
674
fn run_event_loop(display: *Display) void {
675
    const x11_fd = xlib.XConnectionNumber(display.handle);
676
    var fds = [_]std.posix.pollfd{
677
        .{ .fd = x11_fd, .events = std.posix.POLL.IN, .revents = 0 },
678
    };
679
680
    _ = xlib.XSync(display.handle, xlib.False);
681
682
    while (running) {
683
        while (xlib.XPending(display.handle) > 0) {
684
            var event = display.next_event();
685
            handle_event(display, &event);
686
        }
687
688
        tick_animations();
689
690
        var current_bar = bar_mod.bars;
691
        while (current_bar) |bar| {
692
            bar.update_blocks();
693
            bar.draw(display.handle, config.tags[0..config.tag_count]);
694
            current_bar = bar.next;
695
        }
696
697
        const poll_timeout: i32 = if (scroll_animation.is_active()) 16 else 1000;
698
        _ = std.posix.poll(&fds, poll_timeout) catch 0;
699
    }
700
}
701
702
fn handle_event(display: *Display, event: *xlib.XEvent) void {
703
    const event_type = events.get_event_type(event);
704
705
    if (event_type == .button_press) {
706
        std.debug.print("EVENT: button_press received type={d}\n", .{event.type});
707
    }
708
709
    switch (event_type) {
710
        .map_request => handle_map_request(display, &event.xmaprequest),
711
        .configure_request => handle_configure_request(display, &event.xconfigurerequest),
712
        .key_press => handle_key_press(display, &event.xkey),
713
        .destroy_notify => handle_destroy_notify(display, &event.xdestroywindow),
714
        .unmap_notify => handle_unmap_notify(display, &event.xunmap),
715
        .enter_notify => handle_enter_notify(display, &event.xcrossing),
716
        .focus_in => handle_focus_in(display, &event.xfocus),
717
        .motion_notify => handle_motion_notify(display, &event.xmotion),
718
        .client_message => handle_client_message(display, &event.xclient),
719
        .button_press => handle_button_press(display, &event.xbutton),
720
        .expose => handle_expose(display, &event.xexpose),
721
        .property_notify => handle_property_notify(display, &event.xproperty),
722
        else => {},
723
    }
724
}
725
726
fn handle_map_request(display: *Display, event: *xlib.XMapRequestEvent) void {
727
    std.debug.print("map_request: window=0x{x}\n", .{event.window});
728
729
    var window_attributes: xlib.XWindowAttributes = undefined;
730
    if (xlib.XGetWindowAttributes(display.handle, event.window, &window_attributes) == 0) {
731
        return;
732
    }
733
    if (window_attributes.override_redirect != 0) {
734
        return;
735
    }
736
    if (client_mod.window_to_client(event.window) != null) {
737
        return;
738
    }
739
740
    manage(display, event.window, &window_attributes);
741
}
742
743
fn manage(display: *Display, win: xlib.Window, window_attrs: *xlib.XWindowAttributes) void {
744
    const client = client_mod.create(win) orelse return;
745
    var trans: xlib.Window = 0;
746
747
    client.x = window_attrs.x;
748
    client.y = window_attrs.y;
749
    client.width = window_attrs.width;
750
    client.height = window_attrs.height;
751
    client.old_x = window_attrs.x;
752
    client.old_y = window_attrs.y;
753
    client.old_width = window_attrs.width;
754
    client.old_height = window_attrs.height;
755
    client.old_border_width = window_attrs.border_width;
756
    client.border_width = config.border_width;
757
758
    update_title(display, client);
759
760
    if (xlib.XGetTransientForHint(display.handle, win, &trans) != 0) {
761
        if (client_mod.window_to_client(trans)) |transient_client| {
762
            client.monitor = transient_client.monitor;
763
            client.tags = transient_client.tags;
764
        }
765
    }
766
767
    if (client.monitor == null) {
768
        client.monitor = monitor_mod.selected_monitor;
769
        apply_rules(display, client);
770
    }
771
772
    const monitor = client.monitor orelse return;
773
774
    if (client.x + client.width > monitor.win_x + monitor.win_w) {
775
        client.x = monitor.win_x + monitor.win_w - client.width - 2 * client.border_width;
776
    }
777
    if (client.y + client.height > monitor.win_y + monitor.win_h) {
778
        client.y = monitor.win_y + monitor.win_h - client.height - 2 * client.border_width;
779
    }
780
    client.x = @max(client.x, monitor.win_x);
781
    client.y = @max(client.y, monitor.win_y);
782
783
    _ = xlib.XSetWindowBorderWidth(display.handle, win, @intCast(client.border_width));
784
    _ = xlib.XSetWindowBorder(display.handle, win, config.border_unfocused);
785
    tiling.send_configure(client);
786
787
    update_window_type(display, client);
788
    update_size_hints(display, client);
789
    update_wm_hints(display, client);
790
791
    _ = xlib.XSelectInput(
792
        display.handle,
793
        win,
794
        xlib.EnterWindowMask | xlib.FocusChangeMask | xlib.PropertyChangeMask | xlib.StructureNotifyMask,
795
    );
796
    grabbuttons(display, client, false);
797
798
    if (!client.is_floating) {
799
        client.is_floating = trans != 0 or client.is_fixed;
800
        client.old_state = client.is_floating;
801
    }
802
    if (client.is_floating) {
803
        _ = xlib.XRaiseWindow(display.handle, client.window);
804
    }
805
806
    client_mod.attach_aside(client);
807
    client_mod.attach_stack(client);
808
809
    _ = xlib.XChangeProperty(display.handle, display.root, net_client_list, xlib.XA_WINDOW, 32, xlib.PropModeAppend, @ptrCast(&client.window), 1);
810
    _ = xlib.XMoveResizeWindow(display.handle, client.window, client.x + 2 * display.screen_width(), client.y, @intCast(client.width), @intCast(client.height));
811
    set_client_state(display, client, NormalState);
812
813
    if (client.monitor == monitor_mod.selected_monitor) {
814
        const selmon = monitor_mod.selected_monitor orelse return;
815
        unfocus_client(display, selmon.sel, false);
816
    }
817
    monitor.sel = client;
818
819
    if (is_scrolling_layout(monitor)) {
820
        monitor.scroll_offset = 0;
821
    }
822
823
    arrange(monitor);
824
    _ = xlib.XMapWindow(display.handle, win);
825
    focus(display, null);
826
}
827
828
fn handle_configure_request(display: *Display, event: *xlib.XConfigureRequestEvent) void {
829
    const client = client_mod.window_to_client(event.window);
830
831
    if (client) |managed_client| {
832
        if ((event.value_mask & xlib.c.CWBorderWidth) != 0) {
833
            managed_client.border_width = event.border_width;
834
        } else if (managed_client.is_floating or (managed_client.monitor != null and managed_client.monitor.?.lt[managed_client.monitor.?.sel_lt] == null)) {
835
            const monitor = managed_client.monitor orelse return;
836
            if ((event.value_mask & xlib.c.CWX) != 0) {
837
                managed_client.old_x = managed_client.x;
838
                managed_client.x = monitor.mon_x + event.x;
839
            }
840
            if ((event.value_mask & xlib.c.CWY) != 0) {
841
                managed_client.old_y = managed_client.y;
842
                managed_client.y = monitor.mon_y + event.y;
843
            }
844
            if ((event.value_mask & xlib.c.CWWidth) != 0) {
845
                managed_client.old_width = managed_client.width;
846
                managed_client.width = event.width;
847
            }
848
            if ((event.value_mask & xlib.c.CWHeight) != 0) {
849
                managed_client.old_height = managed_client.height;
850
                managed_client.height = event.height;
851
            }
852
            const client_full_width = managed_client.width + managed_client.border_width * 2;
853
            const client_full_height = managed_client.height + managed_client.border_width * 2;
854
            if ((managed_client.x + managed_client.width) > monitor.mon_x + monitor.mon_w and managed_client.is_floating) {
855
                managed_client.x = monitor.mon_x + @divTrunc(monitor.mon_w, 2) - @divTrunc(client_full_width, 2);
856
            }
857
            if ((managed_client.y + managed_client.height) > monitor.mon_y + monitor.mon_h and managed_client.is_floating) {
858
                managed_client.y = monitor.mon_y + @divTrunc(monitor.mon_h, 2) - @divTrunc(client_full_height, 2);
859
            }
860
            if (((event.value_mask & (xlib.c.CWX | xlib.c.CWY)) != 0) and ((event.value_mask & (xlib.c.CWWidth | xlib.c.CWHeight)) == 0)) {
861
                tiling.send_configure(managed_client);
862
            }
863
            if (client_mod.is_visible(managed_client)) {
864
                _ = xlib.XMoveResizeWindow(display.handle, managed_client.window, managed_client.x, managed_client.y, @intCast(managed_client.width), @intCast(managed_client.height));
865
            }
866
        } else {
867
            tiling.send_configure(managed_client);
868
        }
869
    } else {
870
        var changes: xlib.XWindowChanges = undefined;
871
        changes.x = event.x;
872
        changes.y = event.y;
873
        changes.width = event.width;
874
        changes.height = event.height;
875
        changes.border_width = event.border_width;
876
        changes.sibling = event.above;
877
        changes.stack_mode = event.detail;
878
        _ = xlib.XConfigureWindow(display.handle, event.window, @intCast(event.value_mask), &changes);
879
    }
880
    _ = xlib.XSync(display.handle, xlib.False);
881
}
882
883
fn reset_chord_state() void {
884
    chord_index = 0;
885
    chord_keys = [_]config_mod.Key_Press{.{}} ** 4;
886
    chord_timestamp = 0;
887
    if (keyboard_grabbed) {
888
        if (display_global) |dsp| {
889
            _ = xlib.XUngrabKeyboard(dsp.handle, xlib.CurrentTime);
890
        }
891
        keyboard_grabbed = false;
892
    }
893
}
894
895
fn handle_key_press(display: *Display, event: *xlib.XKeyEvent) void {
896
    const keysym = xlib.XKeycodeToKeysym(display.handle, @intCast(event.keycode), 0);
897
898
    if (keybind_overlay) |overlay| {
899
        if (overlay.handle_key(keysym)) {
900
            return;
901
        }
902
    }
903
904
    const clean_state = event.state & ~@as(c_uint, xlib.LockMask | xlib.Mod2Mask);
905
    const current_time = std.time.milliTimestamp();
906
907
    if (chord_index > 0 and (current_time - chord_timestamp) > chord_timeout_ms) {
908
        reset_chord_state();
909
    }
910
911
    chord_keys[chord_index] = .{ .mod_mask = clean_state, .keysym = keysym };
912
    chord_index += 1;
913
    chord_timestamp = current_time;
914
915
    for (config.keybinds.items) |keybind| {
916
        if (keybind.key_count == 0) continue;
917
918
        if (keybind.key_count == chord_index) {
919
            var matches = true;
920
            var i: u8 = 0;
921
            while (i < keybind.key_count) : (i += 1) {
922
                if (chord_keys[i].keysym != keybind.keys[i].keysym or
923
                    chord_keys[i].mod_mask != keybind.keys[i].mod_mask)
924
                {
925
                    matches = false;
926
                    break;
927
                }
928
            }
929
            if (matches) {
930
                execute_action(display, keybind.action, keybind.int_arg, keybind.str_arg);
931
                reset_chord_state();
932
                return;
933
            }
934
        }
935
    }
936
937
    var has_partial_match = false;
938
    for (config.keybinds.items) |keybind| {
939
        if (keybind.key_count > chord_index) {
940
            var matches = true;
941
            var i: u8 = 0;
942
            while (i < chord_index) : (i += 1) {
943
                if (chord_keys[i].keysym != keybind.keys[i].keysym or
944
                    chord_keys[i].mod_mask != keybind.keys[i].mod_mask)
945
                {
946
                    matches = false;
947
                    break;
948
                }
949
            }
950
            if (matches) {
951
                has_partial_match = true;
952
                break;
953
            }
954
        }
955
    }
956
957
    if (has_partial_match and !keyboard_grabbed) {
958
        const grab_result = xlib.XGrabKeyboard(
959
            display.handle,
960
            display.root,
961
            xlib.True,
962
            xlib.GrabModeAsync,
963
            xlib.GrabModeAsync,
964
            xlib.CurrentTime,
965
        );
966
        if (grab_result == xlib.GrabSuccess) {
967
            keyboard_grabbed = true;
968
        }
969
    } else if (!has_partial_match) {
970
        reset_chord_state();
971
    }
972
}
973
974
fn execute_action(display: *Display, action: config_mod.Action, int_arg: i32, str_arg: ?[]const u8) void {
975
    switch (action) {
976
        .spawn_terminal => spawn_terminal(),
977
        .spawn => {
978
            if (str_arg) |cmd| {
979
                spawn_command(cmd);
980
            }
981
        },
982
        .kill_client => kill_focused(display),
983
        .quit => {
984
            std.debug.print("quit keybind pressed\n", .{});
985
            running = false;
986
        },
987
        .reload_config => reload_config(display),
988
        .restart => reload_config(display),
989
        .show_keybinds => {
990
            if (keybind_overlay) |overlay| {
991
                const mon = monitor_mod.selected_monitor orelse monitor_mod.monitors;
992
                if (mon) |m| {
993
                    overlay.toggle(m.mon_x, m.mon_y, m.mon_w, m.mon_h);
994
                }
995
            }
996
        },
997
        .focus_next => focusstack(display, 1),
998
        .focus_prev => focusstack(display, -1),
999
        .move_next => movestack(display, 1),
1000
        .move_prev => movestack(display, -1),
1001
        .resize_master => setmfact(@as(f32, @floatFromInt(int_arg)) / 1000.0),
1002
        .inc_master => incnmaster(1),
1003
        .dec_master => incnmaster(-1),
1004
        .toggle_floating => toggle_floating(display),
1005
        .toggle_fullscreen => toggle_fullscreen(display),
1006
        .toggle_gaps => toggle_gaps(),
1007
        .cycle_layout => cycle_layout(),
1008
        .set_layout => set_layout(str_arg),
1009
        .set_layout_tiling => set_layout_index(0),
1010
        .set_layout_floating => set_layout_index(2),
1011
        .view_tag => {
1012
            const tag_mask: u32 = @as(u32, 1) << @intCast(int_arg);
1013
            view(display, tag_mask);
1014
        },
1015
        .view_next_tag => view_adjacent_tag(display, 1),
1016
        .view_prev_tag => view_adjacent_tag(display, -1),
1017
        .view_next_nonempty_tag => view_adjacent_nonempty_tag(display, 1),
1018
        .view_prev_nonempty_tag => view_adjacent_nonempty_tag(display, -1),
1019
        .move_to_tag => {
1020
            const tag_mask: u32 = @as(u32, 1) << @intCast(int_arg);
1021
            tag_client(display, tag_mask);
1022
        },
1023
        .toggle_view_tag => {
1024
            const tag_mask: u32 = @as(u32, 1) << @intCast(int_arg);
1025
            toggle_view(display, tag_mask);
1026
        },
1027
        .toggle_tag => {
1028
            const tag_mask: u32 = @as(u32, 1) << @intCast(int_arg);
1029
            toggle_client_tag(display, tag_mask);
1030
        },
1031
        .focus_monitor => focusmon(display, int_arg),
1032
        .send_to_monitor => sendmon(display, int_arg),
1033
        .scroll_left => {
1034
            scroll_layout(-1);
1035
        },
1036
        .scroll_right => {
1037
            scroll_layout(1);
1038
        },
1039
    }
1040
}
1041
1042
fn reload_config(display: *Display) void {
1043
    std.debug.print("reloading config...\n", .{});
1044
1045
    ungrab_keybinds(display);
1046
1047
    config.keybinds.clearRetainingCapacity();
1048
    config.buttons.clearRetainingCapacity();
1049
    config.rules.clearRetainingCapacity();
1050
    config.blocks.clearRetainingCapacity();
1051
1052
    lua.deinit();
1053
    _ = lua.init(&config);
1054
1055
    const loaded = if (config_path_global) |path|
1056
        lua.load_file(path)
1057
    else
1058
        lua.load_config();
1059
1060
    if (loaded) {
1061
        if (config_path_global) |path| {
1062
            std.debug.print("reloaded config from {s}\n", .{path});
1063
        } else {
1064
            std.debug.print("reloaded config from ~/.config/oxwm/config.lua\n", .{});
1065
        }
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 = config.gap_inner_h;
1258
        monitor.gap_inner_v = config.gap_inner_v;
1259
        monitor.gap_outer_h = config.gap_outer_h;
1260
        monitor.gap_outer_v = config.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, config.tags[0..config.tag_count]);
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, config.tags[0..config.tag_count]);
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, config.border_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, config.border_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
    var rule_focus = false;
2476
2477
    for (config.rules.items) |rule| {
2478
        const class_matches = if (rule.class) |rc| std.mem.indexOf(u8, class_str, rc) != null else true;
2479
        const instance_matches = if (rule.instance) |ri| std.mem.indexOf(u8, instance_str, ri) != null else true;
2480
        const title_matches = if (rule.title) |rt| std.mem.indexOf(u8, std.mem.sliceTo(&client.name, 0), rt) != null else true;
2481
2482
        if (class_matches and instance_matches and title_matches) {
2483
            client.is_floating = rule.is_floating;
2484
            client.tags |= rule.tags;
2485
            if (rule.monitor >= 0) {
2486
                var target = monitor_mod.monitors;
2487
                var index: i32 = 0;
2488
                while (target) |mon| {
2489
                    if (index == rule.monitor) {
2490
                        client.monitor = mon;
2491
                        break;
2492
                    }
2493
                    index += 1;
2494
                    target = mon.next;
2495
                }
2496
            }
2497
            if (rule.focus) {
2498
                rule_focus = true;
2499
            }
2500
        }
2501
    }
2502
2503
    if (class_hint.res_class) |ptr| {
2504
        _ = xlib.XFree(@ptrCast(ptr));
2505
    }
2506
    if (class_hint.res_name) |ptr| {
2507
        _ = xlib.XFree(@ptrCast(ptr));
2508
    }
2509
2510
    const monitor = client.monitor orelse return;
2511
    if (client.tags == 0) {
2512
        client.tags = monitor.tagset[monitor.sel_tags];
2513
    }
2514
2515
    if (rule_focus and client.tags != 0) {
2516
        const monitor_tagset = monitor.tagset[monitor.sel_tags];
2517
        const is_tag_focused = (monitor_tagset & client.tags) == client.tags;
2518
        if (!is_tag_focused) {
2519
            view(display, client.tags);
2520
        }
2521
    }
2522
}
2523
2524
fn update_client_list(display: *Display) void {
2525
    _ = xlib.XDeleteProperty(display.handle, display.root, net_client_list);
2526
2527
    var current_monitor = monitor_mod.monitors;
2528
    while (current_monitor) |monitor| {
2529
        var current_client = monitor.clients;
2530
        while (current_client) |client| {
2531
            _ = xlib.XChangeProperty(display.handle, display.root, net_client_list, xlib.XA_WINDOW, 32, xlib.PropModeAppend, @ptrCast(&client.window), 1);
2532
            current_client = client.next;
2533
        }
2534
        current_monitor = monitor.next;
2535
    }
2536
}
2537
2538
fn send_event(display: *Display, client: *Client, protocol: xlib.Atom) bool {
2539
    var protocols: [*c]xlib.Atom = undefined;
2540
    var num_protocols: c_int = 0;
2541
    var exists = false;
2542
2543
    if (xlib.XGetWMProtocols(display.handle, client.window, &protocols, &num_protocols) != 0) {
2544
        var index: usize = 0;
2545
        while (index < @as(usize, @intCast(num_protocols))) : (index += 1) {
2546
            if (protocols[index] == protocol) {
2547
                exists = true;
2548
                break;
2549
            }
2550
        }
2551
        _ = xlib.XFree(@ptrCast(protocols));
2552
    }
2553
2554
    if (exists) {
2555
        var event: xlib.XEvent = undefined;
2556
        event.type = xlib.ClientMessage;
2557
        event.xclient.window = client.window;
2558
        event.xclient.message_type = wm_protocols;
2559
        event.xclient.format = 32;
2560
        event.xclient.data.l[0] = @intCast(protocol);
2561
        event.xclient.data.l[1] = xlib.CurrentTime;
2562
        _ = xlib.XSendEvent(display.handle, client.window, xlib.False, xlib.NoEventMask, &event);
2563
    }
2564
    return exists;
2565
}
2566
2567
fn set_urgent(display: *Display, client: *Client, urgent: bool) void {
2568
    client.is_urgent = urgent;
2569
    const wmh = xlib.XGetWMHints(display.handle, client.window);
2570
    if (wmh) |hints| {
2571
        if (urgent) {
2572
            hints.*.flags = hints.*.flags | xlib.XUrgencyHint;
2573
        } else {
2574
            hints.*.flags = hints.*.flags & ~@as(c_long, xlib.XUrgencyHint);
2575
        }
2576
        _ = xlib.XSetWMHints(display.handle, client.window, hints);
2577
        _ = xlib.XFree(@ptrCast(hints));
2578
    }
2579
}
2580
2581
fn run_autostart_commands(_: std.mem.Allocator, commands: []const []const u8) !void {
2582
    for (commands) |cmd| spawn_command(cmd);
2583
}
2584
2585
test {
2586
    _ = @import("x11/events.zig");
2587
}