| 1 |
const std = @import("std");
|
| 2 |
const xlib = @import("x11/xlib.zig");
|
| 3 |
const config_mod = @import("config/config.zig");
|
| 4 |
|
| 5 |
const padding: i32 = 32;
|
| 6 |
const line_spacing: i32 = 12;
|
| 7 |
const key_action_spacing: i32 = 32;
|
| 8 |
const border_width: i32 = 4;
|
| 9 |
const border_color: c_ulong = 0x7fccff;
|
| 10 |
const bg_color: c_ulong = 0x1a1a1a;
|
| 11 |
const fg_color: c_ulong = 0xffffff;
|
| 12 |
const key_bg_color: c_ulong = 0x2a2a2a;
|
| 13 |
|
| 14 |
const max_lines: usize = 12;
|
| 15 |
|
| 16 |
pub const Keybind_Overlay = struct {
|
| 17 |
window: xlib.Window = 0,
|
| 18 |
pixmap: xlib.Pixmap = 0,
|
| 19 |
gc: xlib.GC = null,
|
| 20 |
xft_draw: ?*xlib.XftDraw = null,
|
| 21 |
font: ?*xlib.XftFont = null,
|
| 22 |
font_height: i32 = 0,
|
| 23 |
width: i32 = 0,
|
| 24 |
height: i32 = 0,
|
| 25 |
visible: bool = false,
|
| 26 |
display: ?*xlib.Display = null,
|
| 27 |
root: xlib.Window = 0,
|
| 28 |
screen: c_int = 0,
|
| 29 |
|
| 30 |
key_bufs: [max_lines][64]u8 = undefined,
|
| 31 |
key_lens: [max_lines]usize = undefined,
|
| 32 |
descs: [max_lines][]const u8 = undefined,
|
| 33 |
line_count: usize = 0,
|
| 34 |
|
| 35 |
pub fn init(display: *xlib.Display, screen: c_int, root: xlib.Window, font_name: []const u8, allocator: std.mem.Allocator) ?*Keybind_Overlay {
|
| 36 |
const overlay = allocator.create(Keybind_Overlay) catch return null;
|
| 37 |
|
| 38 |
const font_name_z = allocator.dupeZ(u8, font_name) catch {
|
| 39 |
allocator.destroy(overlay);
|
| 40 |
return null;
|
| 41 |
};
|
| 42 |
defer allocator.free(font_name_z);
|
| 43 |
|
| 44 |
const font = xlib.XftFontOpenName(display, screen, font_name_z);
|
| 45 |
if (font == null) {
|
| 46 |
allocator.destroy(overlay);
|
| 47 |
return null;
|
| 48 |
}
|
| 49 |
|
| 50 |
const font_height = font.*.ascent + font.*.descent;
|
| 51 |
|
| 52 |
overlay.* = .{
|
| 53 |
.display = display,
|
| 54 |
.root = root,
|
| 55 |
.font = font,
|
| 56 |
.font_height = font_height,
|
| 57 |
.screen = screen,
|
| 58 |
};
|
| 59 |
|
| 60 |
return overlay;
|
| 61 |
}
|
| 62 |
|
| 63 |
pub fn deinit(self: *Keybind_Overlay, allocator: std.mem.Allocator) void {
|
| 64 |
if (self.display) |display| {
|
| 65 |
self.destroy_window(display);
|
| 66 |
if (self.font) |font| {
|
| 67 |
xlib.XftFontClose(display, font);
|
| 68 |
}
|
| 69 |
}
|
| 70 |
allocator.destroy(self);
|
| 71 |
}
|
| 72 |
|
| 73 |
fn destroy_window(self: *Keybind_Overlay, display: *xlib.Display) void {
|
| 74 |
if (self.xft_draw) |xft_draw| {
|
| 75 |
xlib.XftDrawDestroy(xft_draw);
|
| 76 |
self.xft_draw = null;
|
| 77 |
}
|
| 78 |
if (self.gc) |gc| {
|
| 79 |
_ = xlib.XFreeGC(display, gc);
|
| 80 |
self.gc = null;
|
| 81 |
}
|
| 82 |
if (self.pixmap != 0) {
|
| 83 |
_ = xlib.XFreePixmap(display, self.pixmap);
|
| 84 |
self.pixmap = 0;
|
| 85 |
}
|
| 86 |
if (self.window != 0) {
|
| 87 |
_ = xlib.c.XDestroyWindow(display, self.window);
|
| 88 |
self.window = 0;
|
| 89 |
}
|
| 90 |
}
|
| 91 |
|
| 92 |
pub fn toggle(self: *Keybind_Overlay, mon_x: i32, mon_y: i32, mon_w: i32, mon_h: i32) void {
|
| 93 |
if (self.visible) {
|
| 94 |
self.hide();
|
| 95 |
} else {
|
| 96 |
self.show(mon_x, mon_y, mon_w, mon_h);
|
| 97 |
}
|
| 98 |
}
|
| 99 |
|
| 100 |
pub fn show(self: *Keybind_Overlay, mon_x: i32, mon_y: i32, mon_w: i32, mon_h: i32) void {
|
| 101 |
const display = self.display orelse return;
|
| 102 |
const cfg = config_mod.get_config() orelse return;
|
| 103 |
|
| 104 |
self.collect_keybinds(cfg);
|
| 105 |
if (self.line_count == 0) return;
|
| 106 |
|
| 107 |
var max_key_width: i32 = 0;
|
| 108 |
var max_desc_width: i32 = 0;
|
| 109 |
|
| 110 |
for (0..self.line_count) |i| {
|
| 111 |
const key_slice = self.key_bufs[i][0..self.key_lens[i]];
|
| 112 |
const key_w = self.text_width(display, key_slice);
|
| 113 |
const desc_w = self.text_width(display, self.descs[i]);
|
| 114 |
if (key_w > max_key_width) max_key_width = key_w;
|
| 115 |
if (desc_w > max_desc_width) max_desc_width = desc_w;
|
| 116 |
}
|
| 117 |
|
| 118 |
const title = "Keybindings";
|
| 119 |
const title_width = self.text_width(display, title);
|
| 120 |
|
| 121 |
const content_width = max_key_width + key_action_spacing + max_desc_width;
|
| 122 |
const min_width = @max(title_width, content_width);
|
| 123 |
|
| 124 |
self.width = min_width + padding * 2;
|
| 125 |
const line_height = self.font_height + line_spacing;
|
| 126 |
const title_height = self.font_height + 20;
|
| 127 |
self.height = title_height + @as(i32, @intCast(self.line_count)) * line_height + padding * 2;
|
| 128 |
|
| 129 |
self.destroy_window(display);
|
| 130 |
|
| 131 |
const x: i32 = mon_x + @divTrunc(mon_w - self.width, 2);
|
| 132 |
const y: i32 = mon_y + @divTrunc(mon_h - self.height, 2);
|
| 133 |
|
| 134 |
const visual = xlib.XDefaultVisual(display, self.screen);
|
| 135 |
const colormap = xlib.XDefaultColormap(display, self.screen);
|
| 136 |
const depth = xlib.XDefaultDepth(display, self.screen);
|
| 137 |
|
| 138 |
self.window = xlib.c.XCreateSimpleWindow(
|
| 139 |
display,
|
| 140 |
self.root,
|
| 141 |
x,
|
| 142 |
y,
|
| 143 |
@intCast(self.width),
|
| 144 |
@intCast(self.height),
|
| 145 |
@intCast(border_width),
|
| 146 |
border_color,
|
| 147 |
bg_color,
|
| 148 |
);
|
| 149 |
|
| 150 |
var attrs: xlib.c.XSetWindowAttributes = undefined;
|
| 151 |
attrs.override_redirect = xlib.True;
|
| 152 |
attrs.event_mask = xlib.c.ExposureMask | xlib.c.KeyPressMask | xlib.c.ButtonPressMask;
|
| 153 |
_ = xlib.c.XChangeWindowAttributes(display, self.window, xlib.c.CWOverrideRedirect | xlib.c.CWEventMask, &attrs);
|
| 154 |
|
| 155 |
self.pixmap = xlib.XCreatePixmap(display, self.window, @intCast(self.width), @intCast(self.height), @intCast(depth));
|
| 156 |
self.gc = xlib.XCreateGC(display, self.pixmap, 0, null);
|
| 157 |
self.xft_draw = xlib.XftDrawCreate(display, self.pixmap, visual, colormap);
|
| 158 |
|
| 159 |
_ = xlib.XMapWindow(display, self.window);
|
| 160 |
_ = xlib.XRaiseWindow(display, self.window);
|
| 161 |
|
| 162 |
self.draw(display, max_key_width, title);
|
| 163 |
|
| 164 |
_ = xlib.XGrabKeyboard(display, self.window, xlib.True, xlib.GrabModeAsync, xlib.GrabModeAsync, xlib.CurrentTime);
|
| 165 |
|
| 166 |
_ = xlib.XSync(display, xlib.False);
|
| 167 |
|
| 168 |
self.visible = true;
|
| 169 |
}
|
| 170 |
|
| 171 |
pub fn hide(self: *Keybind_Overlay) void {
|
| 172 |
if (!self.visible) return;
|
| 173 |
if (self.display) |display| {
|
| 174 |
_ = xlib.XUngrabKeyboard(display, xlib.CurrentTime);
|
| 175 |
if (self.window != 0) {
|
| 176 |
_ = xlib.c.XUnmapWindow(display, self.window);
|
| 177 |
}
|
| 178 |
}
|
| 179 |
self.visible = false;
|
| 180 |
}
|
| 181 |
|
| 182 |
pub fn handle_key(self: *Keybind_Overlay, keysym: u64) bool {
|
| 183 |
if (!self.visible) return false;
|
| 184 |
if (keysym == 0xff1b or keysym == 'q' or keysym == 'Q') {
|
| 185 |
self.hide();
|
| 186 |
return true;
|
| 187 |
}
|
| 188 |
return false;
|
| 189 |
}
|
| 190 |
|
| 191 |
pub fn is_overlay_window(self: *Keybind_Overlay, win: xlib.Window) bool {
|
| 192 |
return self.visible and self.window != 0 and self.window == win;
|
| 193 |
}
|
| 194 |
|
| 195 |
fn draw(self: *Keybind_Overlay, display: *xlib.Display, max_key_width: i32, title: []const u8) void {
|
| 196 |
self.fill_rect(display, 0, 0, self.width, self.height, bg_color);
|
| 197 |
|
| 198 |
const title_x = @divTrunc(self.width - self.text_width(display, title), 2);
|
| 199 |
const title_y = padding + self.font.?.*.ascent;
|
| 200 |
self.draw_text(display, title_x, title_y, title, fg_color);
|
| 201 |
|
| 202 |
const line_height = self.font_height + line_spacing;
|
| 203 |
var y = padding + self.font_height + 20 + self.font.?.*.ascent;
|
| 204 |
|
| 205 |
for (0..self.line_count) |i| {
|
| 206 |
const key_slice = self.key_bufs[i][0..self.key_lens[i]];
|
| 207 |
const key_w = self.text_width(display, key_slice);
|
| 208 |
self.fill_rect(display, padding - 4, y - self.font.?.*.ascent - 2, key_w + 8, self.font_height + 4, key_bg_color);
|
| 209 |
self.draw_text(display, padding, y, key_slice, fg_color);
|
| 210 |
|
| 211 |
const desc_x = padding + max_key_width + key_action_spacing;
|
| 212 |
self.draw_text(display, desc_x, y, self.descs[i], fg_color);
|
| 213 |
|
| 214 |
y += line_height;
|
| 215 |
}
|
| 216 |
|
| 217 |
_ = xlib.c.XCopyArea(display, self.pixmap, self.window, self.gc, 0, 0, @intCast(self.width), @intCast(self.height), 0, 0);
|
| 218 |
_ = xlib.c.XFlush(display);
|
| 219 |
}
|
| 220 |
|
| 221 |
fn fill_rect(self: *Keybind_Overlay, display: *xlib.Display, x: i32, y: i32, w: i32, h: i32, color: c_ulong) void {
|
| 222 |
_ = xlib.XSetForeground(display, self.gc, color);
|
| 223 |
_ = xlib.XFillRectangle(display, self.pixmap, self.gc, x, y, @intCast(w), @intCast(h));
|
| 224 |
}
|
| 225 |
|
| 226 |
fn draw_text(self: *Keybind_Overlay, display: *xlib.Display, x: i32, y: i32, text: []const u8, color: c_ulong) void {
|
| 227 |
if (self.xft_draw == null or self.font == null) return;
|
| 228 |
if (text.len == 0) return;
|
| 229 |
|
| 230 |
var xft_color: xlib.XftColor = undefined;
|
| 231 |
var render_color: xlib.XRenderColor = undefined;
|
| 232 |
render_color.red = @intCast((color >> 16 & 0xff) * 257);
|
| 233 |
render_color.green = @intCast((color >> 8 & 0xff) * 257);
|
| 234 |
render_color.blue = @intCast((color & 0xff) * 257);
|
| 235 |
render_color.alpha = 0xffff;
|
| 236 |
|
| 237 |
const visual = xlib.XDefaultVisual(display, self.screen);
|
| 238 |
const colormap = xlib.XDefaultColormap(display, self.screen);
|
| 239 |
|
| 240 |
_ = xlib.XftColorAllocValue(display, visual, colormap, &render_color, &xft_color);
|
| 241 |
xlib.XftDrawStringUtf8(self.xft_draw, &xft_color, self.font, x, y, text.ptr, @intCast(text.len));
|
| 242 |
xlib.XftColorFree(display, visual, colormap, &xft_color);
|
| 243 |
}
|
| 244 |
|
| 245 |
fn text_width(self: *Keybind_Overlay, display: *xlib.Display, text: []const u8) i32 {
|
| 246 |
if (self.font == null or text.len == 0) return 0;
|
| 247 |
var extents: xlib.XGlyphInfo = undefined;
|
| 248 |
xlib.XftTextExtentsUtf8(display, self.font, text.ptr, @intCast(text.len), &extents);
|
| 249 |
return extents.xOff;
|
| 250 |
}
|
| 251 |
|
| 252 |
fn collect_keybinds(self: *Keybind_Overlay, cfg: *config_mod.Config) void {
|
| 253 |
const priority_actions = [_]config_mod.Action{
|
| 254 |
.show_keybinds,
|
| 255 |
.quit,
|
| 256 |
.reload_config,
|
| 257 |
.kill_client,
|
| 258 |
.spawn_terminal,
|
| 259 |
.toggle_fullscreen,
|
| 260 |
.toggle_floating,
|
| 261 |
.cycle_layout,
|
| 262 |
.focus_next,
|
| 263 |
.focus_prev,
|
| 264 |
.view_tag,
|
| 265 |
.move_to_tag,
|
| 266 |
};
|
| 267 |
|
| 268 |
self.line_count = 0;
|
| 269 |
|
| 270 |
for (priority_actions) |action| {
|
| 271 |
if (self.line_count >= max_lines) break;
|
| 272 |
|
| 273 |
for (cfg.keybinds.items) |kb| {
|
| 274 |
if (kb.action == action and kb.key_count > 0) {
|
| 275 |
self.format_key_to_buf(self.line_count, &kb.keys[0]);
|
| 276 |
self.descs[self.line_count] = action_desc(action);
|
| 277 |
self.line_count += 1;
|
| 278 |
break;
|
| 279 |
}
|
| 280 |
}
|
| 281 |
}
|
| 282 |
}
|
| 283 |
|
| 284 |
fn format_key_to_buf(self: *Keybind_Overlay, idx: usize, key: *const config_mod.Key_Press) void {
|
| 285 |
var len: usize = 0;
|
| 286 |
var buf = &self.key_bufs[idx];
|
| 287 |
|
| 288 |
if (key.mod_mask & (1 << 6) != 0) {
|
| 289 |
const s = "Mod + ";
|
| 290 |
@memcpy(buf[len .. len + s.len], s);
|
| 291 |
len += s.len;
|
| 292 |
}
|
| 293 |
if (key.mod_mask & (1 << 0) != 0) {
|
| 294 |
const s = "Shift + ";
|
| 295 |
@memcpy(buf[len .. len + s.len], s);
|
| 296 |
len += s.len;
|
| 297 |
}
|
| 298 |
if (key.mod_mask & (1 << 2) != 0) {
|
| 299 |
const s = "Ctrl + ";
|
| 300 |
@memcpy(buf[len .. len + s.len], s);
|
| 301 |
len += s.len;
|
| 302 |
}
|
| 303 |
|
| 304 |
const key_name = keysym_to_name(key.keysym);
|
| 305 |
if (len + key_name.len < buf.len) {
|
| 306 |
@memcpy(buf[len .. len + key_name.len], key_name);
|
| 307 |
len += key_name.len;
|
| 308 |
}
|
| 309 |
|
| 310 |
self.key_lens[idx] = len;
|
| 311 |
}
|
| 312 |
|
| 313 |
fn keysym_to_name(keysym: u64) []const u8 {
|
| 314 |
const upper_letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
| 315 |
const digits = "0123456789";
|
| 316 |
|
| 317 |
return switch (keysym) {
|
| 318 |
0xff0d => "Return",
|
| 319 |
0x0020 => "Space",
|
| 320 |
0xff1b => "Escape",
|
| 321 |
0xff08 => "BackSpace",
|
| 322 |
0xff09 => "Tab",
|
| 323 |
0xffbe => "F1",
|
| 324 |
0xffbf => "F2",
|
| 325 |
0xffc0 => "F3",
|
| 326 |
0xffc1 => "F4",
|
| 327 |
0xffc2 => "F5",
|
| 328 |
0xffc3 => "F6",
|
| 329 |
0xffc4 => "F7",
|
| 330 |
0xffc5 => "F8",
|
| 331 |
0xffc6 => "F9",
|
| 332 |
0xffc7 => "F10",
|
| 333 |
0xffc8 => "F11",
|
| 334 |
0xffc9 => "F12",
|
| 335 |
0xff51 => "Left",
|
| 336 |
0xff52 => "Up",
|
| 337 |
0xff53 => "Right",
|
| 338 |
0xff54 => "Down",
|
| 339 |
0x002c => ",",
|
| 340 |
0x002e => ".",
|
| 341 |
0x002f => "/",
|
| 342 |
'a'...'z' => |c| upper_letters[c - 'a' ..][0..1],
|
| 343 |
'A'...'Z' => |c| upper_letters[c - 'A' ..][0..1],
|
| 344 |
'0'...'9' => |c| digits[c - '0' ..][0..1],
|
| 345 |
else => "?",
|
| 346 |
};
|
| 347 |
}
|
| 348 |
|
| 349 |
fn action_desc(action: config_mod.Action) []const u8 {
|
| 350 |
return switch (action) {
|
| 351 |
.show_keybinds => "Show Keybinds",
|
| 352 |
.quit => "Quit WM",
|
| 353 |
.reload_config => "Reload Config",
|
| 354 |
.restart => "Restart WM",
|
| 355 |
.kill_client => "Close Window",
|
| 356 |
.spawn_terminal => "Open Terminal",
|
| 357 |
.spawn => "Launch Program",
|
| 358 |
.toggle_fullscreen => "Toggle Fullscreen",
|
| 359 |
.toggle_floating => "Toggle Floating",
|
| 360 |
.toggle_gaps => "Toggle Gaps",
|
| 361 |
.cycle_layout => "Cycle Layout",
|
| 362 |
.set_layout => "Set Layout",
|
| 363 |
.focus_next => "Focus Next",
|
| 364 |
.focus_prev => "Focus Previous",
|
| 365 |
.move_next => "Move Next",
|
| 366 |
.move_prev => "Move Previous",
|
| 367 |
.view_tag => "View Tag",
|
| 368 |
.move_to_tag => "Move to Tag",
|
| 369 |
.focus_monitor => "Focus Monitor",
|
| 370 |
.send_to_monitor => "Send to Monitor",
|
| 371 |
else => "Action",
|
| 372 |
};
|
| 373 |
}
|
| 374 |
};
|