2zw
Owner: IIIlllIIIllI URL: git@git.0x00nyx.xyz:seb/2zw.git
src/bar.zig
// MIT License
// Copyright (c) 2025 Sebastian <sebastian.michalk@pm.me>
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
const std = @import("std");
const C = @import("c.zig");
const drawlib = @import("draw.zig");
pub const BarPos = enum {
top,
bottom,
};
pub const ModAlign = enum {
left,
center,
right,
};
const ModFn = *const fn (*Mod, std.mem.Allocator, i64) anyerror!void;
const Mod = struct {
name: []const u8,
text: std.ArrayList(u8),
interval_ms: u64,
next_update_ms: i64,
updater: ModFn,
side: ModAlign,
pub fn init(
alloc: std.mem.Allocator,
name: []const u8,
interval_ms: u64,
updater: ModFn,
side: ModAlign,
) Mod {
return Mod{
.name = name,
.text = std.ArrayList(u8).init(alloc),
.interval_ms = interval_ms,
.next_update_ms = 0,
.updater = updater,
.side = side,
};
}
pub fn deinit(self: *Mod) void {
self.text.deinit();
}
pub fn due(self: *Mod, now_ms: i64) bool {
return now_ms >= self.next_update_ms;
}
pub fn run(self: *Mod, alloc: std.mem.Allocator, now_ms: i64) !void {
try self.updater(self, alloc, now_ms);
const interval: i64 = @intCast(self.interval_ms);
self.next_update_ms = now_ms + interval;
}
pub fn set(self: *Mod, text: []const u8) !void {
self.text.clearRetainingCapacity();
try self.text.appendSlice(text);
}
};
pub const Bar = struct {
alloc: std.mem.Allocator,
dpy: *C.Display,
root: C.Window,
scr: c_int,
win: C.Window,
pos: BarPos,
w: c_uint,
h: c_uint,
base: c_int,
drw: drawlib.Drw,
xft: *C.XftDraw,
bg: C.ulong,
fg: C.XftColor,
mods: std.ArrayList(Mod),
dirty: bool,
pad: c_int,
delim: []const u8,
mon_x: c_int,
mon_y: c_int,
mon_h: c_uint,
font: [*:0]const u8,
sel_win: ?C.Window,
pub fn init(
alloc: std.mem.Allocator,
dpy: *C.Display,
root: C.Window,
scr: c_int,
w: c_uint,
mon_x: c_int,
mon_y: c_int,
mon_h: c_uint,
pos: BarPos,
font: [*:0]const u8,
delim: []const u8,
) !Bar {
var drw = try drawlib.Drw.init(alloc, dpy, scr, font);
errdefer drw.deinit();
const h: c_uint = @intCast(drw.lh + 4);
const bg = try drw.alloccol(0x1c1c1c);
const fg = try drw.allocxftcol(0xdddddd);
const win = C.XCreateSimpleWindow(
dpy,
root,
mon_x,
mon_y,
w,
h,
0,
bg,
bg,
);
if (win == 0) return error.CreateWindowFailed;
var attrs: C.XSetWindowAttributes = undefined;
attrs.override_redirect = 1;
attrs.event_mask = C.ExposureMask | C.StructureNotifyMask;
attrs.background_pixel = bg;
attrs.border_pixel = bg;
_ = C.XChangeWindowAttributes(
dpy,
win,
C.CWOverrideRedirect | C.CWEventMask |
C.CWBackPixel | C.CWBorderPixel,
&attrs,
);
const xft_draw = C.XftDrawCreate(dpy, win, drw.vis, drw.cmap);
if (xft_draw == null) return error.XftDrawCreateFailed;
_ = C.XSelectInput(dpy, win, C.ExposureMask | C.StructureNotifyMask);
_ = C.XMapRaised(dpy, win);
const base = 2 + drw.ascent;
return Bar{
.alloc = alloc,
.dpy = dpy,
.root = root,
.scr = scr,
.win = win,
.pos = pos,
.w = w,
.h = h,
.base = base,
.drw = drw,
.xft = xft_draw.?,
.bg = bg,
.fg = fg,
.mods = std.ArrayList(Mod).init(alloc),
.dirty = true,
.pad = 8,
.delim = delim,
.mon_x = mon_x,
.mon_y = mon_y,
.mon_h = mon_h,
.font = font,
.sel_win = null,
};
}
pub fn deinit(self: *Bar) void {
for (self.mods.items) |*mod| mod.deinit();
self.mods.deinit();
C.XftColorFree(self.dpy, self.drw.vis, self.drw.cmap, &self.fg);
C.XftDrawDestroy(self.xft);
if (self.win != 0) {
_ = C.XDestroyWindow(self.dpy, self.win);
self.win = 0;
}
self.drw.deinit();
}
pub fn addMod(
self: *Bar,
name: []const u8,
interval_ms: u64,
updater: ModFn,
side: ModAlign,
) !void {
var mod = Mod.init(self.alloc, name, interval_ms, updater, side);
errdefer mod.deinit();
try self.mods.append(mod);
self.dirty = true;
}
pub fn addClock(self: *Bar, interval_ms: u64) !void {
try self.addMod("clock", interval_ms, clkUpd, .right);
}
pub fn addBat(self: *Bar, interval_ms: u64) !void {
try self.addMod("battery", interval_ms, batUpd, .right);
}
pub fn addWin(self: *Bar, interval_ms: u64) !void {
try self.addMod("window", interval_ms, winUpd, .center);
}
pub fn setSel(self: *Bar, win: ?C.Window) void {
if (self.sel_win != win) {
self.sel_win = win;
self.dirty = true;
}
}
pub fn mark(self: *Bar) void {
self.dirty = true;
}
pub fn resize(
self: *Bar,
w: c_uint,
mon_x: c_int,
mon_y: c_int,
mon_h: c_uint,
) void {
self.w = w;
self.mon_x = mon_x;
self.mon_y = mon_y;
self.mon_h = mon_h;
const mon_h_i: c_int = @intCast(mon_h);
const bar_h_i: c_int = @intCast(self.h);
const y = switch (self.pos) {
.top => mon_y,
.bottom => mon_y + mon_h_i - bar_h_i,
};
_ = C.XMoveResizeWindow(self.dpy, self.win, mon_x, y, w, self.h);
self.dirty = true;
}
pub fn barH(self: *Bar) c_int {
return @intCast(self.h);
}
pub fn tick(self: *Bar, now_ms: i64) !bool {
var changed = false;
for (self.mods.items) |*mod| {
if (!mod.due(now_ms)) continue;
try mod.run(self.alloc, now_ms);
changed = true;
}
if (changed) self.dirty = true;
return changed;
}
pub fn nextDelay(self: *Bar, now_ms: i64) u64 {
if (self.mods.items.len == 0) return 1000;
var best: i64 = std.math.maxInt(i64);
for (self.mods.items) |mod| {
const delta = mod.next_update_ms - now_ms;
const clamped = if (delta <= 0) 0 else delta;
if (clamped < best) best = clamped;
}
if (best == std.math.maxInt(i64)) return 1000;
return @intCast(best);
}
pub fn draw(self: *Bar) void {
if (!self.dirty) return;
self.drw.rect(self.win, self.bg, 0, 0, self.w, self.h);
const baseline = self.base;
const delim_txt = self.delim;
const delim_w = self.drw.textw(delim_txt);
var x_right: c_int = @intCast(self.w);
x_right -= self.pad;
var first_right = true;
var i: usize = self.mods.items.len;
while (i > 0) {
i -= 1;
const mod = &self.mods.items[i];
if (mod.side != .right) continue;
const txt = mod.text.items;
if (txt.len == 0) continue;
if (!first_right and delim_txt.len != 0) {
x_right -= delim_w;
var fg_delim = self.fg;
self.drw.text(
self.win,
self.xft,
&fg_delim,
x_right,
baseline,
delim_txt,
);
x_right -= self.pad;
}
const txt_w = self.drw.textw(txt);
x_right -= txt_w;
var fg_copy = self.fg;
self.drw.text(
self.win,
self.xft,
&fg_copy,
x_right,
baseline,
txt,
);
x_right -= self.pad;
first_right = false;
}
for (self.mods.items) |*mod| {
if (mod.side != .center) continue;
const txt = mod.text.items;
if (txt.len == 0) continue;
const txt_w = self.drw.textw(txt);
const bar_w: c_int = @intCast(self.w);
const x_center = @divTrunc(bar_w - txt_w, 2);
var fg_copy = self.fg;
self.drw.text(
self.win,
self.xft,
&fg_copy,
x_center,
baseline,
txt,
);
}
var x_left: c_int = self.pad;
var first_left = true;
for (self.mods.items) |*mod| {
if (mod.side != .left) continue;
const txt = mod.text.items;
if (txt.len == 0) continue;
if (!first_left and delim_txt.len != 0) {
var fg_delim = self.fg;
self.drw.text(
self.win,
self.xft,
&fg_delim,
x_left,
baseline,
delim_txt,
);
x_left += delim_w;
x_left += self.pad;
}
var fg_copy = self.fg;
self.drw.text(
self.win,
self.xft,
&fg_copy,
x_left,
baseline,
txt,
);
const txt_w = self.drw.textw(txt);
x_left += txt_w + self.pad;
first_left = false;
}
_ = C.XSync(self.dpy, 0);
self.dirty = false;
}
};
fn clkUpd(mod: *Mod, alloc: std.mem.Allocator, now_ms: i64) !void {
_ = alloc;
_ = now_ms;
const timestamp = std.time.timestamp();
if (timestamp < 0) {
try mod.set("time err");
return;
}
const wall_seconds: u64 = @intCast(timestamp);
const epoch_seconds = std.time.epoch.EpochSeconds{ .secs = wall_seconds };
const epoch_day = epoch_seconds.getEpochDay();
const day_seconds = epoch_seconds.getDaySeconds();
const year_day = epoch_day.calculateYearDay();
const month_day = year_day.calculateMonthDay();
const hours = day_seconds.getHoursIntoDay();
const minutes = day_seconds.getMinutesIntoHour();
const weekday_names = [_][]const u8{
"Sun",
"Mon",
"Tue",
"Wed",
"Thu",
"Fri",
"Sat",
};
const month_names = [_][]const u8{
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
};
const day_index: usize = @intCast(epoch_day.day);
const weekday_index = (day_index + 4) % weekday_names.len;
const weekday_name = weekday_names[weekday_index];
const month_index: usize = @intCast(month_day.month.numeric() - 1);
const month_name = month_names[month_index];
const day_of_month: u8 = @intCast(month_day.day_index + 1);
var buf: [64]u8 = undefined;
const text = try std.fmt.bufPrint(&buf, "{s}, {s} {d}, {d:0>2}:{d:0>2}", .{
weekday_name,
month_name,
day_of_month,
hours,
minutes,
});
try mod.set(text);
}
fn batUpd(mod: *Mod, alloc: std.mem.Allocator, _: i64) !void {
const capacity_path = "/sys/class/power_supply/BAT0/capacity";
const file = std.fs.openFileAbsolute(capacity_path, .{}) catch |err| {
if (err == error.FileNotFound) {
try mod.set("N/A");
return;
}
return err;
};
defer file.close();
var buf: [8]u8 = undefined;
const bytes_read = try file.readAll(&buf);
const content = std.mem.trimRight(
u8,
buf[0..bytes_read],
&std.ascii.whitespace,
);
const capacity = try std.fmt.parseInt(u8, content, 10);
var buffer: [16]u8 = undefined;
const text = try std.fmt.bufPrint(&buffer, "BAT {d}%", .{capacity});
const owned = try alloc.dupe(u8, text);
defer alloc.free(owned);
try mod.set(owned);
}
var g_bar: ?*Bar = null;
pub fn setGlobalBar(bar_ptr: ?*Bar) void {
g_bar = bar_ptr;
}
fn winUpd(mod: *Mod, alloc: std.mem.Allocator, _: i64) !void {
const bar_ptr = g_bar orelse {
try mod.set("");
return;
};
const win = bar_ptr.sel_win orelse {
try mod.set("");
return;
};
var class_hint: C.XClassHint = undefined;
_ = C.XSetErrorHandler(ignoreError);
const status = C.XGetClassHint(bar_ptr.dpy, win, &class_hint);
_ = C.XSetErrorHandler(handleError);
if (status == 0) {
try mod.set("");
return;
}
defer {
if (class_hint.res_class != null) _ = C.XFree(class_hint.res_class);
if (class_hint.res_name != null) _ = C.XFree(class_hint.res_name);
}
if (class_hint.res_class != null) {
const class_name = std.mem.span(
@as([*:0]const u8, @ptrCast(class_hint.res_class)),
);
const owned = try alloc.dupe(u8, class_name);
defer alloc.free(owned);
try mod.set(owned);
} else {
try mod.set("");
}
}
fn ignoreError(_: ?*C.Display, _: [*c]C.XErrorEvent) callconv(.C) c_int {
return 0;
}
fn handleError(_: ?*C.Display, _: [*c]C.XErrorEvent) callconv(.C) c_int {
return 0;
}