2zw

2zw - X11 Windowmanager

Files | Log | Commits | Refs | README


fa5d7f6

Author: SM

Date: 2025-05-03

Subject: proper handle floatnig windows

Diff

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 {