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;
}