[root]/ src
/ client.zig
19.6KB
const std = @import("std");
const c = @import("c.zig").c;
const config = @import("config.zig");
const atoms_mod = @import("atoms.zig");
const Atoms = atoms_mod.Atoms;
pub const Client = struct {
window: c.Window,
x: i32 = 0,
y: i32 = 0,
w: u32 = 0,
h: u32 = 0,
bw: u32 = config.border_width,
saved: struct {
x: i32 = 0,
y: i32 = 0,
w: u32 = 0,
h: u32 = 0,
} = .{},
min_w: i32 = 0,
min_h: i32 = 0,
max_w: i32 = 0,
max_h: i32 = 0,
base_w: i32 = 0,
base_h: i32 = 0,
inc_w: i32 = 0,
inc_h: i32 = 0,
min_aspect: f64 = 0,
max_aspect: f64 = 0,
has_aspect: bool = false,
flags: packed struct {
floating: bool = false,
fullscreen: bool = false,
minimized: bool = false,
urgent: bool = false,
never_focus: bool = false,
fixed_size: bool = false,
} = .{},
title: []u8 = &.{},
class: []u8 = &.{},
prev: ?*Client = null,
next: ?*Client = null,
unmap_count: u32 = 0,
supports_delete: bool = false,
supports_take_focus: bool = false,
input_hint: bool = true,
transient_for: c.Window = 0,
};
pub const GrabState = struct {
client: *Client,
orig_x: i32,
orig_y: i32,
orig_w: u32,
orig_h: u32,
start_x: i32,
start_y: i32,
resizing: bool,
};
pub const ButtonEntry = struct {
x: i32,
w: u32,
win: c.Window,
};
pub const Bar = struct {
window: c.Window = 0,
pixmap: c.Pixmap = 0,
draw: ?*c.XftDraw = null,
fonts: [config.font_names.len]?*c.XftFont,
colors: struct {
bg: c.XftColor,
fg: c.XftColor,
sel_bg: c.XftColor,
min_fg: c.XftColor,
urg_fg: c.XftColor,
border_focused: c.XftColor,
border_unfocused: c.XftColor,
},
buttons: [128]ButtonEntry,
button_count: usize = 0,
};
pub const WM = struct {
display: ?*c.Display,
screen: c_int,
root: c.Window,
sw: u32,
sh: u32,
atoms: Atoms,
bar: Bar,
clients: ?*Client,
focused: ?*Client,
running: bool,
allocator: std.mem.Allocator,
support_win: c.Window,
grab: ?GrabState,
visual: ?*c.Visual,
colormap: c.Colormap,
};
pub fn listAppend(head: *?*Client, cl: *Client) void {
cl.next = null;
cl.prev = null;
if (head.*) |h| {
var it: *Client = h;
while (it.next) |next| it = next;
it.next = cl;
cl.prev = it;
} else {
head.* = cl;
}
}
pub fn listRemove(head: *?*Client, cl: *Client) void {
if (cl.prev) |p| p.next = cl.next else head.* = cl.next;
if (cl.next) |n| n.prev = cl.prev;
cl.prev = null;
cl.next = null;
}
pub fn listFind(head: ?*Client, win: c.Window) ?*Client {
var it = head;
while (it) |cl| : (it = cl.next) {
if (cl.window == win) return cl;
}
return null;
}
pub fn listCount(head: ?*Client) usize {
var n: usize = 0;
var it = head;
while (it) |cl| : (it = cl.next) n += 1;
return n;
}
pub fn listLast(head: ?*Client) ?*Client {
var it = head orelse return null;
while (it.next) |next| it = next;
return it;
}
fn setWmState(wm: *WM, cl: *Client, state: i32) void {
const data = [2]c_long{ @intCast(state), 0 };
_ = c.XChangeProperty(wm.display, cl.window, wm.atoms.wm_state,
wm.atoms.wm_state, 32, c.PropModeReplace, @ptrCast(&data), 2);
}
fn sendProtocol(wm: *WM, cl: *Client, protocol: c.Atom) void {
var ev: c.XEvent = undefined;
@memset(@as([*]u8, @ptrCast(&ev))[0..@sizeOf(c.XEvent)], 0);
ev.type = c.ClientMessage;
ev.xclient.window = cl.window;
ev.xclient.message_type = wm.atoms.wm_protocols;
ev.xclient.format = 32;
ev.xclient.data.l[0] = @intCast(protocol);
ev.xclient.data.l[1] = @intCast(c.CurrentTime);
_ = c.XSendEvent(wm.display, cl.window, 0, c.NoEventMask, &ev);
}
fn updateFocusPolicy(cl: *Client) void {
cl.flags.never_focus = !cl.input_hint and !cl.supports_take_focus;
}
pub fn closeClient(wm: *WM, cl: *Client) void {
if (cl.supports_delete) {
sendProtocol(wm, cl, wm.atoms.wm_delete_window);
} else {
_ = c.XKillClient(wm.display, cl.window);
}
}
pub fn readTitle(wm: *WM, cl: *Client) void {
if (cl.title.len > 0) {
wm.allocator.free(cl.title);
cl.title = &.{};
}
var text: c.XTextProperty = undefined;
if (c.XGetTextProperty(wm.display, cl.window, &text, wm.atoms.net_wm_name) != 0) {
if (text.value != null) {
defer _ = c.XFree(text.value);
if (text.nitems > 0) {
var len: usize = @intCast(text.nitems);
if (text.value[len - 1] == 0) len -= 1;
cl.title = wm.allocator.dupe(u8, text.value[0..len]) catch &.{};
return;
}
}
}
var name: [*c]u8 = null;
if (c.XFetchName(wm.display, cl.window, &name) != 0) {
if (name != null) {
cl.title = wm.allocator.dupe(u8, std.mem.sliceTo(name.?, 0)) catch &.{};
_ = c.XFree(name);
}
}
}
pub fn readClass(wm: *WM, cl: *Client) void {
if (cl.class.len > 0) {
wm.allocator.free(cl.class);
cl.class = &.{};
}
var hint: c.XClassHint = .{ .res_name = null, .res_class = null };
if (c.XGetClassHint(wm.display, cl.window, &hint) != 0) {
if (hint.res_class != null) {
cl.class = wm.allocator.dupe(u8, std.mem.sliceTo(hint.res_class.?, 0)) catch &.{};
_ = c.XFree(hint.res_class);
}
if (hint.res_name != null) _ = c.XFree(hint.res_name);
}
}
fn readProtocols(wm: *WM, cl: *Client) void {
var protos: [*c]c.Atom = null;
var n: c_int = 0;
if (c.XGetWMProtocols(wm.display, cl.window, &protos, &n) != 0) {
var i: c_int = 0;
while (i < n) : (i += 1) {
if (protos[@intCast(i)] == wm.atoms.wm_delete_window)
cl.supports_delete = true;
if (protos[@intCast(i)] == wm.atoms.wm_take_focus)
cl.supports_take_focus = true;
}
_ = c.XFree(protos);
}
updateFocusPolicy(cl);
}
fn readHints(wm: *WM, cl: *Client) void {
var hints: c.XSizeHints = undefined;
var supplied: c_long = 0;
if (c.XGetWMNormalHints(wm.display, cl.window, &hints, &supplied) != 0) {
if (hints.flags & c.PMinSize != 0) {
cl.min_w = hints.min_width;
cl.min_h = hints.min_height;
}
if (hints.flags & c.PMaxSize != 0) {
cl.max_w = hints.max_width;
cl.max_h = hints.max_height;
}
if (hints.flags & c.PBaseSize != 0) {
cl.base_w = hints.base_width;
cl.base_h = hints.base_height;
}
if (hints.flags & c.PResizeInc != 0) {
cl.inc_w = hints.width_inc;
cl.inc_h = hints.height_inc;
}
if (hints.flags & c.PAspect != 0) {
if (hints.min_aspect.y > 0 and hints.max_aspect.y > 0) {
cl.min_aspect = @as(f64, @floatFromInt(hints.min_aspect.x)) /
@as(f64, @floatFromInt(hints.min_aspect.y));
cl.max_aspect = @as(f64, @floatFromInt(hints.max_aspect.x)) /
@as(f64, @floatFromInt(hints.max_aspect.y));
cl.has_aspect = true;
}
}
if (hints.flags & (c.PMinSize | c.PBaseSize) != 0 and
hints.flags & c.PMaxSize != 0 and
hints.min_width == hints.max_width and
hints.min_height == hints.max_height)
{
cl.flags.fixed_size = true;
cl.flags.floating = true;
}
}
const wm_hints: [*c]c.XWMHints = c.XGetWMHints(wm.display, cl.window);
if (wm_hints != null) {
if (wm_hints.*.flags & c.InputHint != 0)
cl.input_hint = wm_hints.*.input != 0;
if (wm_hints.*.flags & c.XUrgencyHint != 0)
cl.flags.urgent = true;
_ = c.XFree(wm_hints);
}
updateFocusPolicy(cl);
var trans: c.Window = 0;
_ = c.XGetTransientForHint(wm.display, cl.window, &trans);
if (trans != 0) {
cl.transient_for = trans;
cl.flags.floating = true;
}
}
fn checkWindowType(wm: *WM, cl: *Client) void {
var actual_type: c.Atom = 0;
var actual_format: c_int = 0;
var nitems: c_ulong = 0;
var bytes_after: c_ulong = 0;
var prop: [*c]u8 = null;
if (c.XGetWindowProperty(wm.display, cl.window, wm.atoms.net_wm_window_type,
0, 1024, 0, c.XA_ATOM, &actual_type, &actual_format,
&nitems, &bytes_after, &prop) == c.Success and prop != null)
{
const atoms: [*]c.Atom = @ptrCast(@alignCast(prop));
var i: usize = 0;
while (i < nitems) : (i += 1) {
const a = atoms[i];
if (a == wm.atoms.net_wm_window_type_dialog or
a == wm.atoms.net_wm_window_type_utility or
a == wm.atoms.net_wm_window_type_toolbar or
a == wm.atoms.net_wm_window_type_splash or
a == wm.atoms.net_wm_window_type_menu or
a == wm.atoms.net_wm_window_type_dock)
{
cl.flags.floating = true;
}
}
_ = c.XFree(prop);
}
}
fn applyRules(cl: *Client) void {
for (&config.rules) |rule| {
if (rule.class) |rc| {
if (cl.class.len > 0 and std.mem.indexOf(u8, cl.class, rc) != null) {
cl.flags.floating = rule.floating;
}
}
}
}
pub fn applySizeHints(cl: *Client, w: i32, h: i32) struct { w: i32, h: i32 } {
var nw = w;
var nh = h;
if (cl.inc_w > 0 and cl.base_w >= 0)
nw = cl.base_w + cl.inc_w * @as(i32, @intFromFloat(@round(@as(f64, @floatFromInt(nw - cl.base_w)) / @as(f64, @floatFromInt(cl.inc_w)))));
if (cl.inc_h > 0 and cl.base_h >= 0)
nh = cl.base_h + cl.inc_h * @as(i32, @intFromFloat(@round(@as(f64, @floatFromInt(nh - cl.base_h)) / @as(f64, @floatFromInt(cl.inc_h)))));
if (cl.min_w > 0 and nw < cl.min_w) nw = cl.min_w;
if (cl.min_h > 0 and nh < cl.min_h) nh = cl.min_h;
if (cl.max_w > 0 and nw > cl.max_w) nw = cl.max_w;
if (cl.max_h > 0 and nh > cl.max_h) nh = cl.max_h;
if (cl.has_aspect) {
const aspect = @as(f64, @floatFromInt(nw)) / @as(f64, @floatFromInt(@max(nh, 1)));
if (aspect < cl.min_aspect)
nw = @intFromFloat(@as(f64, @floatFromInt(nh)) * cl.min_aspect);
if (aspect > cl.max_aspect)
nh = @intFromFloat(@as(f64, @floatFromInt(nw)) / cl.max_aspect);
}
if (nw < 1) nw = 1;
if (nh < 1) nh = 1;
return .{ .w = nw, .h = nh };
}
pub fn configure(wm: *WM, cl: *Client) void {
var ce: c.XConfigureEvent = undefined;
ce.type = c.ConfigureNotify;
ce.display = wm.display;
ce.event = cl.window;
ce.window = cl.window;
ce.x = cl.x;
ce.y = cl.y;
ce.width = @intCast(cl.w);
ce.height = @intCast(cl.h);
ce.border_width = @intCast(cl.bw);
ce.above = 0;
ce.override_redirect = 0;
_ = c.XSendEvent(wm.display, cl.window, 0, c.StructureNotifyMask, @ptrCast(&ce));
}
pub fn resize(wm: *WM, cl: *Client, x: i32, y: i32, w: u32, h: u32) void {
const s = applySizeHints(cl, @intCast(w), @intCast(h));
const sw: u32 = @intCast(@max(s.w, 1));
const sh: u32 = @intCast(@max(s.h, 1));
cl.x = x;
cl.y = y;
cl.w = sw;
cl.h = sh;
_ = c.XMoveResizeWindow(wm.display, cl.window, x, y, sw, sh);
configure(wm, cl);
updateFrameExtents(wm, cl);
}
pub fn setFullscreen(wm: *WM, cl: *Client, fullscreen: bool) void {
const was_fs = cl.flags.fullscreen;
cl.flags.fullscreen = fullscreen;
if (fullscreen and !was_fs) {
cl.saved = .{ .x = cl.x, .y = cl.y, .w = cl.w, .h = cl.h };
cl.bw = 0;
_ = c.XSetWindowBorderWidth(wm.display, cl.window, 0);
resize(wm, cl, 0, @intCast(config.bar_height), wm.sw, wm.sh - config.bar_height);
_ = c.XRaiseWindow(wm.display, cl.window);
setNetState(wm, cl);
} else if (!fullscreen and was_fs) {
cl.bw = config.border_width;
_ = c.XSetWindowBorderWidth(wm.display, cl.window, @intCast(config.border_width));
resize(wm, cl, cl.saved.x, cl.saved.y, cl.saved.w, cl.saved.h);
setNetState(wm, cl);
}
}
pub fn setMinimized(wm: *WM, cl: *Client, minimized: bool) void {
const was_min = cl.flags.minimized;
cl.flags.minimized = minimized;
if (minimized and !was_min) {
cl.unmap_count += 1;
_ = c.XUnmapWindow(wm.display, cl.window);
setWmState(wm, cl, 3);
setNetState(wm, cl);
} else if (!minimized and was_min) {
_ = c.XMapWindow(wm.display, cl.window);
setWmState(wm, cl, 1);
setNetState(wm, cl);
}
}
pub fn setNetState(wm: *WM, cl: *Client) void {
var atoms: [4]c.Atom = undefined;
var n: usize = 0;
if (cl.flags.fullscreen) {
atoms[n] = wm.atoms.net_wm_state_fullscreen;
n += 1;
}
if (cl.flags.minimized) {
atoms[n] = wm.atoms.net_wm_state_hidden;
n += 1;
}
if (cl.flags.urgent) {
atoms[n] = wm.atoms.net_wm_state_demands_attention;
n += 1;
}
if (n > 0) {
_ = c.XChangeProperty(wm.display, cl.window, wm.atoms.net_wm_state,
c.XA_ATOM, 32, c.PropModeReplace, @ptrCast(&atoms), @intCast(n));
} else {
_ = c.XDeleteProperty(wm.display, cl.window, wm.atoms.net_wm_state);
}
}
pub fn grabClientButtons(wm: *WM, win: c.Window, focused: bool) void {
_ = c.XUngrabButton(wm.display, c.AnyButton, c.AnyModifier, win);
_ = c.XGrabButton(wm.display, c.Button1, c.Mod1Mask, win, 0,
c.ButtonPressMask | c.ButtonReleaseMask | c.PointerMotionMask,
c.GrabModeAsync, c.GrabModeAsync, 0, 0);
_ = c.XGrabButton(wm.display, c.Button3, c.Mod1Mask, win, 0,
c.ButtonPressMask | c.ButtonReleaseMask | c.PointerMotionMask,
c.GrabModeAsync, c.GrabModeAsync, 0, 0);
_ = c.XGrabButton(wm.display, c.Button1, c.Mod1Mask | c.LockMask, win, 0,
c.ButtonPressMask | c.ButtonReleaseMask | c.PointerMotionMask,
c.GrabModeAsync, c.GrabModeAsync, 0, 0);
_ = c.XGrabButton(wm.display, c.Button3, c.Mod1Mask | c.LockMask, win, 0,
c.ButtonPressMask | c.ButtonReleaseMask | c.PointerMotionMask,
c.GrabModeAsync, c.GrabModeAsync, 0, 0);
if (!focused) {
_ = c.XGrabButton(wm.display, c.AnyButton, c.AnyModifier, win, 0,
c.ButtonPressMask, c.GrabModeSync, c.GrabModeSync, 0, 0);
}
}
pub fn focus(wm: *WM, cl: ?*Client) void {
if (wm.focused) |prev| {
setBorder(wm, prev, false);
grabClientButtons(wm, prev.window, false);
}
wm.focused = cl;
if (cl) |client| {
setBorder(wm, client, true);
grabClientButtons(wm, client.window, true);
_ = c.XRaiseWindow(wm.display, client.window);
if (!client.flags.never_focus) {
if (client.input_hint) {
_ = c.XSetInputFocus(wm.display, client.window, c.RevertToPointerRoot, c.CurrentTime);
}
if (client.supports_take_focus) {
sendProtocol(wm, client, wm.atoms.wm_take_focus);
}
}
_ = c.XChangeProperty(wm.display, wm.root, wm.atoms.net_active_window,
c.XA_WINDOW, 32, c.PropModeReplace, @ptrCast(&client.window), 1);
} else {
_ = c.XSetInputFocus(wm.display, wm.root, c.RevertToPointerRoot, c.CurrentTime);
_ = c.XDeleteProperty(wm.display, wm.root, wm.atoms.net_active_window);
}
}
fn setBorder(wm: *WM, cl: *Client, focused: bool) void {
const color = if (focused)
wm.bar.colors.border_focused.pixel
else
wm.bar.colors.border_unfocused.pixel;
_ = c.XSetWindowBorder(wm.display, cl.window, color);
}
pub fn focusNext(wm: *WM) void {
const start = wm.focused orelse wm.clients orelse return;
var it = start;
while (true) {
it = it.next orelse wm.clients orelse return;
if (!it.flags.minimized) {
focus(wm, it);
_ = c.XRaiseWindow(wm.display, it.window);
return;
}
if (it == start) break;
}
}
pub fn focusVisible(wm: *WM) void {
var it = wm.clients;
while (it) |cl| : (it = cl.next) {
if (!cl.flags.minimized) {
focus(wm, cl);
return;
}
}
focus(wm, null);
}
pub fn raise(wm: *WM, cl: *Client) void {
_ = c.XRaiseWindow(wm.display, cl.window);
listRemove(&wm.clients, cl);
listAppend(&wm.clients, cl);
updateClientList(wm);
}
pub fn updateClientList(wm: *WM) void {
const n = listCount(wm.clients);
if (n == 0) {
_ = c.XDeleteProperty(wm.display, wm.root, wm.atoms.net_client_list);
_ = c.XDeleteProperty(wm.display, wm.root, wm.atoms.net_client_list_stacking);
return;
}
var buf: [256]c.Window = undefined;
var i: usize = 0;
var it = wm.clients;
while (it) |cl| : (it = cl.next) {
if (i < buf.len) {
buf[i] = cl.window;
i += 1;
}
}
_ = c.XChangeProperty(wm.display, wm.root, wm.atoms.net_client_list,
c.XA_WINDOW, 32, c.PropModeReplace, @ptrCast(&buf), @intCast(i));
i = 0;
it = wm.clients;
while (it) |cl| : (it = cl.next) {
if (i < buf.len) {
buf[i] = cl.window;
i += 1;
}
}
_ = c.XChangeProperty(wm.display, wm.root, wm.atoms.net_client_list_stacking,
c.XA_WINDOW, 32, c.PropModeReplace, @ptrCast(&buf), @intCast(i));
}
pub fn updateFrameExtents(wm: *WM, cl: *Client) void {
const data = [4]c_long{
@intCast(cl.bw),
@intCast(cl.bw),
@intCast(cl.bw),
@intCast(cl.bw),
};
_ = c.XChangeProperty(wm.display, cl.window, wm.atoms.net_frame_extents,
c.XA_CARDINAL, 32, c.PropModeReplace, @ptrCast(&data), 4);
}
pub fn manage(wm: *WM, window: c.Window) !void {
if (listFind(wm.clients, window) != null) return;
if (window == wm.bar.window) return;
var wa: c.XWindowAttributes = undefined;
if (c.XGetWindowAttributes(wm.display, window, &wa) == 0) return;
if (wa.override_redirect != 0) return;
const cl = try wm.allocator.create(Client);
errdefer wm.allocator.destroy(cl);
cl.* = .{
.window = window,
.x = wa.x,
.y = wa.y,
.w = @intCast(wa.width),
.h = @intCast(wa.height),
.bw = config.border_width,
};
readHints(wm, cl);
readTitle(wm, cl);
readClass(wm, cl);
readProtocols(wm, cl);
checkWindowType(wm, cl);
applyRules(cl);
if (!cl.flags.floating) {
cl.y = @max(cl.y, @as(i32, @intCast(config.bar_height)));
}
listAppend(&wm.clients, cl);
setWmState(wm, cl, 1);
_ = c.XSelectInput(wm.display, window,
c.StructureNotifyMask | c.PropertyChangeMask |
c.EnterWindowMask | c.FocusChangeMask);
_ = c.XSetWindowBorderWidth(wm.display, window, @intCast(config.border_width));
_ = c.XMoveResizeWindow(wm.display, window, cl.x, cl.y, cl.w, cl.h);
_ = c.XMapWindow(wm.display, window);
grabClientButtons(wm, window, false);
updateFrameExtents(wm, cl);
focus(wm, cl);
updateClientList(wm);
}
pub fn unmanage(wm: *WM, cl: *Client) void {
if (wm.focused == cl) {
const next = cl.next orelse cl.prev;
focus(wm, next);
}
listRemove(&wm.clients, cl);
setWmState(wm, cl, 0);
if (cl.title.len > 0) wm.allocator.free(cl.title);
if (cl.class.len > 0) wm.allocator.free(cl.class);
wm.allocator.destroy(cl);
updateClientList(wm);
}
pub fn findClientByBarClick(wm: *WM, x: i32) ?*Client {
var i: usize = 0;
while (i < wm.bar.button_count) : (i += 1) {
const btn = wm.bar.buttons[i];
if (x >= btn.x and x < btn.x + @as(i32, @intCast(btn.w))) {
return listFind(wm.clients, btn.win);
}
}
return null;
}