oxwm

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