2zw - X11 Windowmanager
Files | Log | Commits | Refs | README
Author: SM
Date: 2025-05-03
Subject: proper handle floatnig windows
commit fa5d7f64af238ee8665bcbb7b70e67e1cf00b7ad Author: SM <seb.michalk@gmail.com> Date: Sat May 3 13:02:37 2025 +0200 proper handle floatnig windows diff --git a/README.txt b/README.txt index 1fca52f..09df960 100644 --- a/README.txt +++ b/README.txt @@ -1,15 +1,15 @@ -2zw - zig window +2zw =============== 2zw is an extremely fast and lean dynamic window manager for X written in Zig. Features -------- -- Small hackable Codebase (<900 LOC) +- Small hackable Codebase (~1000 LOC) - Tiling in Master/Stack layout - No Workspaces, instead attach/detach Windows -- No configuration files, configured via source +- No configuration file parsing, configured via source - Floating window management -- Window focusing with colored borders +- Colored borders - Minimal approach to UI (no status bar) - Zero dependencies beyond X11 and Zig standard library @@ -45,8 +45,7 @@ Configuration ------------- All configuration is done directly in main.zig and rebuilding. -The keybindings and configuration constants are at the top of the file -for easy modification: +The keybindings and configuration constants: FOCUS_BORDER_COLOR = 0xffd787; // Focused window border color NORMAL_BORDER_COLOR = 0x333333; // Normal window border color @@ -62,4 +61,4 @@ Keybindings (with MOD4/Super key): - MOD+. Focus next window - MOD+Return Launch terminal (default st) - MOD+p Launch application launcher (default dmenu) -- MOD+s Launch slock \ No newline at end of file +- MOD+s Launch slock diff --git a/src/main.zig b/src/main.zig index b3f4bb8..23a13d9 100644 --- a/src/main.zig +++ b/src/main.zig @@ -17,6 +17,7 @@ const Client = struct { ww: c_int, wh: c_int, w: C.Window, + is_floating: bool = false, }; const L = std.DoublyLinkedList(Client); @@ -35,7 +36,7 @@ var master_factor: f32 = MASTER_FACTOR; var n_master: usize = 1; const terminal = "st"; -const launcher = "dmenu_run"; +const launcher = "zmen"; const autostart_path = "/home/smi/.scripts/ewm.sh"; var resize_cursor: C.Cursor = undefined; @@ -99,7 +100,6 @@ fn logError(msg: []const u8) void { stderr.print("Error: {s}\n", .{msg}) catch return; } -// do we need this ? fn logInfo(msg: []const u8) void { const stdInfo = std.io.getStdOut().writer(); stdInfo.print("INFO: {s}\n", .{msg}) catch return; @@ -205,59 +205,140 @@ fn findAndManageExistingWindows(allocator: std.mem.Allocator) void { } } +fn handleFloatingWindows() void { + var node = list.first; + while (node) |n| : (node = n.next) { + if (n.data.is_floating) { + var attrs: C.XWindowAttributes = undefined; + + _ = C.XSetErrorHandler(ignoreError); + const status = C.XGetWindowAttributes(display, n.data.w, &attrs); + _ = C.XSetErrorHandler(handleError); + + if (status == 0) continue; + + const is_small = attrs.width < @as(c_int, @intCast(screen_w / 2)) and + attrs.height < @as(c_int, @intCast(screen_h / 2)); + + const needs_positioning = + is_small and (attrs.x <= 0 or + attrs.y <= 0 or + attrs.x + attrs.width >= @as(c_int, @intCast(screen_w)) or + attrs.y + attrs.height >= @as(c_int, @intCast(screen_h))); + + if (needs_positioning) { + const x = @divTrunc(@as(c_int, @intCast(screen_w)) - attrs.width, 2); + const y = @divTrunc(@as(c_int, @intCast(screen_h)) - attrs.height, 2); + + _ = C.XMoveWindow(display, n.data.w, x, y); + } + + _ = C.XRaiseWindow(display, n.data.w); + } + } +} + fn applyLayout() void { if (list.len == 0) return; + handleFloatingWindows(); + + var tiled_count: usize = 0; + var next = list.first; + while (next) |node| : (next = node.next) { + if (!node.data.is_floating) { + tiled_count += 1; + } + } + + if (tiled_count == 0) return; + const sw: c_int = @intCast(screen_w); const sh: c_int = @intCast(screen_h); - if (list.len == 1) { - if (list.first) |first| { - _ = C.XMoveResizeWindow(display, first.data.w, GAP_SIZE, GAP_SIZE, @as(c_uint, @intCast(@max(0, screen_w - (2 * GAP_SIZE) - (2 * BORDER_WIDTH)))), @as(c_uint, @intCast(@max(0, screen_h - (2 * GAP_SIZE) - (2 * BORDER_WIDTH))))); + if (tiled_count == 1) { + next = list.first; + while (next) |node| : (next = node.next) { + if (!node.data.is_floating) { + _ = C.XMoveResizeWindow(display, node.data.w, GAP_SIZE, GAP_SIZE, @as(c_uint, @intCast(@max(0, screen_w - (2 * GAP_SIZE) - (2 * BORDER_WIDTH)))), @as(c_uint, @intCast(@max(0, screen_h - (2 * GAP_SIZE) - (2 * BORDER_WIDTH))))); + break; + } } return; } const master_width = @as(c_uint, @intCast(@as(c_int, @intFromFloat(master_factor * @as(f32, @floatFromInt(sw)))))); - const actual_masters = @min(n_master, list.len); - const stack_windows = list.len - actual_masters; + var actual_masters: usize = 0; + var masters_found: usize = 0; - var i: usize = 0; - var node = list.first; + next = list.first; + while (next) |node| : (next = node.next) { + if (!node.data.is_floating) { + if (masters_found < n_master) { + if (actual_masters == 0) { + actual_masters = 1; + + var temp = next; + while (temp) |t| : (temp = t.next) { + if (!t.data.is_floating and masters_found < n_master) { + masters_found += 1; + } + } + } - while (i < actual_masters and node != null) : (i += 1) { - const client_node = node.?; + if (masters_found == 1) { + _ = C.XMoveResizeWindow(display, node.data.w, GAP_SIZE, GAP_SIZE, @as(c_uint, @intCast(@max(0, master_width - (2 * GAP_SIZE) - (2 * BORDER_WIDTH)))), @as(c_uint, @intCast(@max(0, sh - (2 * GAP_SIZE) - (2 * BORDER_WIDTH))))); + } else { + const gap_total = @as(c_int, @intCast((masters_found + 1) * GAP_SIZE)); + const master_height = @divTrunc(sh - gap_total, @as(c_int, @intCast(masters_found))); + const master_index = actual_masters - 1; // 0-based index + const y_position = GAP_SIZE + (@as(c_int, @intCast(master_index)) * (master_height + GAP_SIZE)); - if (actual_masters == 1) { - _ = C.XMoveResizeWindow(display, client_node.data.w, GAP_SIZE, GAP_SIZE, @as(c_uint, @intCast(@max(0, master_width - (2 * GAP_SIZE) - (2 * BORDER_WIDTH)))), @as(c_uint, @intCast(@max(0, sh - (2 * GAP_SIZE) - (2 * BORDER_WIDTH))))); - } else { - const gap_total = @as(c_int, @intCast((actual_masters + 1) * GAP_SIZE)); - const master_height = @divTrunc(sh - gap_total, @as(c_int, @intCast(actual_masters))); - const y_position = GAP_SIZE + (@as(c_int, @intCast(i)) * (master_height + GAP_SIZE)); + _ = C.XMoveResizeWindow(display, node.data.w, GAP_SIZE, y_position, @as(c_uint, @intCast(@max(0, master_width - (2 * GAP_SIZE) - (2 * BORDER_WIDTH)))), @as(c_uint, @intCast(@max(0, master_height - (2 * BORDER_WIDTH))))); + } - _ = C.XMoveResizeWindow(display, client_node.data.w, GAP_SIZE, y_position, @as(c_uint, @intCast(@max(0, master_width - (2 * GAP_SIZE) - (2 * BORDER_WIDTH)))), @as(c_uint, @intCast(@max(0, master_height - (2 * BORDER_WIDTH))))); + actual_masters += 1; + } } - - node = client_node.next; } - if (stack_windows > 0 and node != null) { - const stack_width = sw - @as(c_int, @intCast(master_width)) - GAP_SIZE; + const stack_windows = tiled_count - masters_found; - if (stack_windows == 1) { - _ = C.XMoveResizeWindow(display, node.?.data.w, @as(c_int, @intCast(master_width)) + GAP_SIZE, GAP_SIZE, @as(c_uint, @intCast(@max(0, stack_width - GAP_SIZE - (2 * BORDER_WIDTH)))), @as(c_uint, @intCast(@max(0, sh - (2 * GAP_SIZE) - (2 * BORDER_WIDTH))))); - } else { - const gap_total_stack = @as(c_int, @intCast((stack_windows + 1) * GAP_SIZE)); - const stack_height = @divTrunc(sh - gap_total_stack, @as(c_int, @intCast(stack_windows))); - var stack_i: usize = 0; + if (stack_windows > 0) { + const stack_width = sw - @as(c_int, @intCast(master_width)) - GAP_SIZE; + var stack_i: usize = 0; + + next = list.first; + while (next) |node| : (next = node.next) { + if (!node.data.is_floating) { + var is_master = false; + var master_idx: usize = 0; + var check = list.first; + while (check != null and master_idx < masters_found) { + if (check.? == node and !check.?.data.is_floating) { + is_master = true; + break; + } + if (!check.?.data.is_floating) { + master_idx += 1; + } + check = check.?.next; + } - while (node != null) : (node = node.?.next) { - const y_position = GAP_SIZE + (@as(c_int, @intCast(stack_i)) * (stack_height + GAP_SIZE)); + if (!is_master) { + if (stack_windows == 1) { + _ = C.XMoveResizeWindow(display, node.data.w, @as(c_int, @intCast(master_width)) + GAP_SIZE, GAP_SIZE, @as(c_uint, @intCast(@max(0, stack_width - GAP_SIZE - (2 * BORDER_WIDTH)))), @as(c_uint, @intCast(@max(0, sh - (2 * GAP_SIZE) - (2 * BORDER_WIDTH))))); + } else { + const gap_total_stack = @as(c_int, @intCast((stack_windows + 1) * GAP_SIZE)); + const stack_height = @divTrunc(sh - gap_total_stack, @as(c_int, @intCast(stack_windows))); + const y_position = GAP_SIZE + (@as(c_int, @intCast(stack_i)) * (stack_height + GAP_SIZE)); - _ = C.XMoveResizeWindow(display, node.?.data.w, @as(c_int, @intCast(master_width)) + GAP_SIZE, y_position, @as(c_uint, @intCast(@max(0, stack_width - GAP_SIZE - (2 * BORDER_WIDTH)))), @as(c_uint, @intCast(@max(0, stack_height - (2 * BORDER_WIDTH))))); + _ = C.XMoveResizeWindow(display, node.data.w, @as(c_int, @intCast(master_width)) + GAP_SIZE, y_position, @as(c_uint, @intCast(@max(0, stack_width - GAP_SIZE - (2 * BORDER_WIDTH)))), @as(c_uint, @intCast(@max(0, stack_height - (2 * BORDER_WIDTH))))); - stack_i += 1; + stack_i += 1; + } + } } } } @@ -267,6 +348,8 @@ fn addClient(allocator: std.mem.Allocator, window: C.Window) !*L.Node { var attributes: C.XWindowAttributes = undefined; _ = C.XGetWindowAttributes(display, window, &attributes); + const is_dialog = checkWindowType(window); + const client = Client{ .full = false, .wx = attributes.x, @@ -274,6 +357,7 @@ fn addClient(allocator: std.mem.Allocator, window: C.Window) !*L.Node { .ww = attributes.width, .wh = attributes.height, .w = window, + .is_floating = is_dialog, }; var node = try allocator.create(L.Node); @@ -283,6 +367,70 @@ fn addClient(allocator: std.mem.Allocator, window: C.Window) !*L.Node { return node; } +fn checkWindowType(window: C.Window) bool { + var transient_for: C.Window = undefined; + if (C.XGetTransientForHint(display, window, &transient_for) != 0) { + return true; + } + + var class_hint: C.XClassHint = undefined; + if (C.XGetClassHint(display, window, &class_hint) != 0) { + var is_dialog = false; + + if (class_hint.res_class != null) { + const class_name = std.mem.span(@as([*:0]const u8, @ptrCast(class_hint.res_class))); + is_dialog = (std.mem.indexOf(u8, class_name, "Dialog") != null); + _ = C.XFree(class_hint.res_class); + } + + if (class_hint.res_name != null) { + const res_name = std.mem.span(@as([*:0]const u8, @ptrCast(class_hint.res_name))); + is_dialog = is_dialog or (std.mem.indexOf(u8, res_name, "dialog") != null); + _ = C.XFree(class_hint.res_name); + } + + if (is_dialog) { + return true; + } + } + + var actual_type: C.Atom = undefined; + var actual_format: c_int = undefined; + var nitems: c_ulong = undefined; + var bytes_after: c_ulong = undefined; + var prop_return: [*c]u8 = undefined; + + _ = C.XSetErrorHandler(ignoreError); + + const net_wm_window_type = C.XInternAtom(display, "_NET_WM_WINDOW_TYPE", 0); + if (net_wm_window_type != 0) { + const status = C.XGetWindowProperty(display, window, net_wm_window_type, 0, 32, 0, C.XA_ATOM, &actual_type, &actual_format, &nitems, &bytes_after, &prop_return); + + if (status == 0 and prop_return != null and actual_type == C.XA_ATOM and actual_format == 32 and nitems > 0) { + const atoms = @as([*]C.Atom, @alignCast(@ptrCast(prop_return))); + + for (0..nitems) |i| { + const atom = atoms[i]; + const dialog_atom = C.XInternAtom(display, "_NET_WM_WINDOW_TYPE_DIALOG", 0); + const utility_atom = C.XInternAtom(display, "_NET_WM_WINDOW_TYPE_UTILITY", 0); + const popup_atom = C.XInternAtom(display, "_NET_WM_WINDOW_TYPE_POPUP_MENU", 0); + + if (atom == dialog_atom or atom == utility_atom or atom == popup_atom) { + _ = C.XFree(prop_return); + _ = C.XSetErrorHandler(handleError); + return true; + } + } + + _ = C.XFree(prop_return); + } + } + + _ = C.XSetErrorHandler(handleError); + + return false; +} + fn focus(node: ?*L.Node) void { if (list.len == 0) return; if (cursor) |c| _ = C.XSetWindowBorder(display, c.data.w, NORMAL_BORDER_COLOR); @@ -320,26 +468,18 @@ fn unmanage(allocator: std.mem.Allocator, node: *L.Node, destroyed: bool) void { _ = C.XSetErrorHandler(handleError); _ = C.XUngrabServer(display); } - if (node == cursor) cursor = node.prev; + if (node == cursor) cursor = node.prev; if (previously_focused) |pf| { if (node.data.w == pf.data.w) previously_focused = null; } - _ = C.XSetInputFocus( - display, - root, - C.RevertToPointerRoot, - C.CurrentTime, - ); - _ = C.XDeleteProperty(display, root, C.XInternAtom(display, "_NET_ACTIVE_WINDOW", 0)); - list.remove(node); allocator.destroy(node); if (list.len > 0) { applyLayout(); - focus(null); + focus(list.first); } } @@ -691,6 +831,11 @@ fn onMapRequest(allocator: std.mem.Allocator, event: *C.XEvent) !void { focus(node); applyLayout(); + + if (node.data.is_floating) { + _ = C.XRaiseWindow(display, node.data.w); + focus(node); + } } fn onUnmapNotify(allocator: std.mem.Allocator, e: *C.XEvent) void { @@ -723,20 +868,29 @@ fn onKeyPress(e: *C.XEvent) void { } fn onEnterNotify(e: *C.XEvent) void { - // Ignore EnterNotify events that are due to keyboard/other grab or if focused on root + var current_node = list.first; + while (current_node) |n| : (current_node = n.next) { + if (n.data.is_floating) { + return; // Skip focus changes while dialogs are present + } + } + if (e.xcrossing.mode != C.NotifyNormal or e.xcrossing.detail == C.NotifyInferior) { return; } - // Only focus if the window isa managed client - if (winToNode(e.xcrossing.window)) |node| { - // Don't refocus if already focused - if (cursor != null and node.data.w == cursor.?.data.w) { + _ = C.XSetErrorHandler(ignoreError); + if (winToNode(e.xcrossing.window)) |window_node| { + if (cursor != null and window_node.data.w == cursor.?.data.w) { + _ = C.XSetErrorHandler(handleError); return; } - focus(node); + if (!window_node.data.is_floating) { + focus(window_node); + } } + _ = C.XSetErrorHandler(handleError); } fn onButtonPress(e: *C.XEvent) void {