oxwm

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