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 {