oxwm

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