~andreafeletto/levee

a28f39e9f7014e1cea6976522693c0ec740d094f — Andrea Feletto a month ago c0f0e5a main
remove inheritance for modules
M src/Loop.zig => src/Loop.zig +65 -57
@@ 10,24 10,6 @@ const Loop = @This();
state: *State,
sfd: os.fd_t,

pub const Event = struct {
    fd: os.pollfd,
    data: *anyopaque,
    callbackIn: Callback,
    callbackOut: Callback,

    pub const Action = enum { ok, terminate };
    pub const Callback = fn (*anyopaque) Action;

    pub fn terminate(_: *anyopaque) Action {
        return .terminate;
    }

    pub fn noop(_: *anyopaque) Action {
        return .ok;
    }
};

pub fn init(state: *State) !Loop {
    var mask = os.empty_sigset;
    os.linux.sigaddset(&mask, os.linux.SIG.INT);


@@ 41,56 23,82 @@ pub fn init(state: *State) !Loop {
}

pub fn run(self: *Loop) !void {
    const gpa = self.state.gpa;
    const display = self.state.wayland.display;
    const wayland = &self.state.wayland;
    const modules = &self.state.modules;

    var fds = [_]os.pollfd{
        .{
            .fd = self.sfd,
            .events = os.POLL.IN,
            .revents = undefined,
        },
        .{
            .fd = wayland.fd,
            .events = os.POLL.IN,
            .revents = undefined,
        },
        .{
            .fd = if (modules.backlight) |mod| mod.fd else -1,
            .events = os.POLL.IN,
            .revents = undefined,
        },
        .{
            .fd = if (modules.battery) |mod| mod.fd else -1,
            .events = os.POLL.IN,
            .revents = undefined,
        },
        .{
            .fd = if (modules.pulse) |mod| mod.fd else -1,
            .events = os.POLL.IN,
            .revents = undefined,
        },
    };

    var events: std.MultiArrayList(Event) = .{};
    defer events.deinit(gpa);

    try events.append(gpa, .{
        .fd = .{ .fd = self.sfd, .events = os.POLL.IN, .revents = 0 },
        .data = undefined,
        .callbackIn = Event.terminate,
        .callbackOut = Event.noop,
    });
    try events.append(gpa, try self.state.wayland.getEvent());
    for (self.state.modules.modules.items) |*module| {
        try events.append(gpa, try module.getEvent());
    }

    const fds = events.items(.fd);
    while (true) {
        while (true) {
            const ret = display.dispatchPending();
            _ = display.flush();
            const ret = wayland.display.dispatchPending();
            _ = wayland.display.flush();
            if (ret == .SUCCESS) break;
        }

        _ = os.poll(fds, -1) catch |err| {
        _ = os.poll(&fds, -1) catch |err| {
            log.err("poll failed: {s}", .{@errorName(err)});
            return;
        };

        for (fds) |fd, i| {
            if (fd.revents & os.POLL.HUP != 0) return;
            if (fd.revents & os.POLL.ERR != 0) return;

            if (fd.revents & os.POLL.IN != 0) {
                const event = events.get(i);
                const action = event.callbackIn(event.data);
                switch (action) {
                    .ok => {},
                    .terminate => return,
                }
            }
            if (fd.revents & os.POLL.OUT != 0) {
                const event = events.get(i);
                const action = event.callbackOut(event.data);
                switch (action) {
                    .ok => {},
                    .terminate => return,
                }
        for (fds) |fd| {
            if (fd.revents & os.POLL.HUP != 0 or fd.revents & os.POLL.ERR != 0) {
                return;
            }
        }

        // signals
        if (fds[0].revents & os.POLL.IN != 0) {
            return;
        }

        // wayland
        if (fds[1].revents & os.POLL.IN != 0) {
            const errno = wayland.display.dispatch();
            if (errno != .SUCCESS) return;
        }
        if (fds[1].revents & os.POLL.OUT != 0) {
            const errno = wayland.display.flush();
            if (errno != .SUCCESS) return;
        }

        // modules
        if (modules.backlight) |*mod| if (fds[2].revents & os.POLL.IN != 0) {
            log.info("backlight", .{});
            mod.refresh() catch return;
        };
        if (modules.battery) |*mod| if (fds[3].revents & os.POLL.IN != 0) {
            log.info("battery", .{});
            mod.refresh() catch return;
        };
        if (modules.pulse) |*mod| if (fds[4].revents & os.POLL.IN != 0) {
            log.info("pulse", .{});
            mod.refresh() catch return;
        };
    }
}

M src/Modules.zig => src/Modules.zig +29 -36
@@ 1,55 1,48 @@
const std = @import("std");
const mem = std.mem;
const ArrayList = std.ArrayList;
const log = std.log;

const Event = @import("Loop.zig").Event;
const State = @import("main.zig").State;
const Modules = @This();

state: *State,
modules: ArrayList(Module),

pub const Backlight = @import("modules/Backlight.zig");
pub const Battery = @import("modules/Battery.zig");
pub const Pulse = @import("modules/Pulse.zig");

pub const Module = struct {
    impl: *anyopaque,
    funcs: struct {
        getEvent: fn (*anyopaque) anyerror!Event,
        print: fn (*anyopaque, StringWriter) anyerror!void,
        destroy: fn (*anyopaque) void,
    },

    pub const StringWriter = std.ArrayList(u8).Writer;

    pub fn getEvent(self: *Module) !Event {
        return self.funcs.getEvent(self.impl);
    }
const Backlight = @import("modules/Backlight.zig");
const Battery = @import("modules/Battery.zig");
const Pulse = @import("modules/Pulse.zig");

    pub fn print(self: *Module, writer: StringWriter) !void {
        return self.funcs.print(self.impl, writer);
    }
const Tag = enum { backlight, battery, pulse };

    pub fn destroyInstance(self: *Module) void {
        return self.funcs.destroy(self.impl);
    }
};
state: *State,
backlight: ?Backlight = null,
battery: ?Battery = null,
pulse: ?Pulse = null,
order: ArrayList(Tag),

pub fn init(state: *State) Modules {
    return Modules{
        .state = state,
        .modules = ArrayList(Module).init(state.gpa),
        .order = ArrayList(Tag).init(state.gpa),
    };
}

pub fn deinit(self: *Modules) void {
    for (self.modules.items) |*module| module.destroyInstance();
    self.modules.deinit();
    if (self.backlight) |*mod| mod.deinit();
    if (self.battery) |*mod| mod.deinit();
    if (self.pulse) |*mod| mod.deinit();
    self.order.deinit();
}

pub fn register(self: *Modules, comptime ModuleType: type) !void {
    const instance = try ModuleType.create(self.state);
    try self.modules.append(try instance.module());
    log.info("registered module: {s}", .{@typeName(ModuleType)});
pub fn register(self: *Modules, name: []const u8) !void {
    if (mem.eql(u8, name, "backlight")) {
        self.backlight = try Backlight.init(self.state);
        try self.order.append(.backlight);
    } else if (mem.eql(u8, name, "battery")) {
        self.battery = try Battery.init(self.state);
        try self.order.append(.battery);
    } else if (mem.eql(u8, name, "pulse")) {
        self.pulse = try Pulse.init(self.state);
        try self.pulse.?.start();
        try self.order.append(.pulse);
    } else {
        return error.UnknownModule;
    }
}

M src/Wayland.zig => src/Wayland.zig +36 -61
@@ 21,6 21,7 @@ const Wayland = @This();

state: *State,
display: *wl.Display,
fd: os.fd_t,

monitors: ArrayList(*Monitor),
inputs: ArrayList(*Input),


@@ 39,13 40,16 @@ const Globals = struct {
const GlobalsMask = utils.Mask(Globals);

pub fn init(state: *State) !Wayland {
    const display = wl.Display.connect(null) catch |err| {
        utils.fatal("failed to connect to a wayland compositor: {s}", .{@errorName(err)});
    const display = try wl.Display.connect(null);
    const wfd = wfd: {
        const fd = display.getFd();
        break :wfd @intCast(os.fd_t, fd);
    };

    return Wayland{
        .state = state,
        .display = display,
        .fd = wfd,
        .monitors = ArrayList(*Monitor).init(state.gpa),
        .inputs = ArrayList(*Input).init(state.gpa),
        .globals = undefined,


@@ 69,53 73,33 @@ pub fn deinit(self: *Wayland) void {
}

pub fn registerGlobals(self: *Wayland) !void {
    const registry = self.display.getRegistry() catch |err| {
        utils.fatal("out of memory during initialization: {s}", .{@errorName(err)});
    };
    const registry = try self.display.getRegistry();
    defer registry.destroy();

    registry.setListener(*State, registryListener, self.state);
    const errno = self.display.roundtrip();
    if (errno != .SUCCESS) {
        utils.fatal("initial roundtrip failed", .{});
    }

    if (errno != .SUCCESS) return error.RoundtripFailed;
    for (self.globalsMask) |is_registered| if (!is_registered) {
        utils.fatal("global not advertised", .{});
    };
}

pub fn getEvent(self: *Wayland) !Event {
    const fd = self.display.getFd();

    return Event{
        .fd = .{
            .fd = @intCast(os.fd_t, fd),
            .events = os.POLL.IN,
            .revents = undefined,
        },
        .data = @ptrCast(*anyopaque, self),
        .callbackIn = dispatch,
        .callbackOut = flush,
        return error.GlobalNotAdvertized;
    };
}

fn dispatch(self_opaque: *anyopaque) Event.Action {
    const self = utils.cast(Wayland)(self_opaque);
    const errno = self.display.dispatch();
    switch (errno) {
        .SUCCESS => return .ok,
        else => return .terminate,
pub fn findBar(self: *Wayland, wlSurface: ?*wl.Surface) ?*Bar {
    if (wlSurface == null) {
        return null;
    }
}

fn flush(self_opaque: *anyopaque) Event.Action {
    const self = utils.cast(Wayland)(self_opaque);
    const errno = self.display.flush();
    switch (errno) {
        .SUCCESS => return .ok,
        else => return .terminate,
    for (self.monitors.items) |monitor| {
        if (monitor.bar) |bar| {
            if (bar.background.surface == wlSurface or
                bar.tags.surface == wlSurface or
                bar.clock.surface == wlSurface or
                bar.modules.surface == wlSurface)
            {
                return bar;
            }
        }
    }
    return null;
}

fn registryListener(registry: *wl.Registry, event: wl.Registry.Event, state: *State) void {


@@ 146,7 130,10 @@ fn registryListener(registry: *wl.Registry, event: wl.Registry.Event, state: *St

fn bindGlobal(self: *Wayland, registry: *wl.Registry, name: u32, iface: [*:0]const u8, version: u32) !void {
    if (strcmp(iface, wl.Compositor.getInterface().name) == 0) {
        if (version < 4) utils.fatal("wl_compositor version 4 is required", .{});
        if (version < 4) {
            log.err("wl_compositor version 4 is required", .{});
            return;
        }
        const global = try registry.bind(name, wl.Compositor, 4);
        self.setGlobal(global);
    } else if (strcmp(iface, wl.Subcompositor.getInterface().name) == 0) {


@@ 168,17 155,23 @@ fn bindGlobal(self: *Wayland, registry: *wl.Registry, name: u32, iface: [*:0]con
        const global = try registry.bind(name, zriver.ControlV1, 1);
        self.setGlobal(global);
    } else if (strcmp(iface, wl.Output.getInterface().name) == 0) {
        if (version < 3) utils.fatal("wl_output version 3 is required", .{});
        if (version < 3) {
            log.err("wl_output version 3 is required", .{});
            return;
        }
        const monitor = try Monitor.create(self.state, registry, name);
        try self.monitors.append(monitor);
    } else if (strcmp(iface, wl.Seat.getInterface().name) == 0) {
        if (version < 5) utils.fatal("wl_seat version 5 is required", .{});
        if (version < 5) {
            log.err("wl_seat version 5 is required", .{});
            return;
        }
        const input = try Input.create(self.state, registry, name);
        try self.inputs.append(input);
    }
}

pub fn setGlobal(self: *Wayland, global: anytype) void {
fn setGlobal(self: *Wayland, global: anytype) void {
    inline for (meta.fields(Globals)) |field, i| {
        if (field.field_type == @TypeOf(global)) {
            @field(self.globals, field.name) = global;


@@ 187,21 180,3 @@ pub fn setGlobal(self: *Wayland, global: anytype) void {
        }
    }
}

pub fn findBar(self: *Wayland, wlSurface: ?*wl.Surface) ?*Bar {
    if (wlSurface == null) {
        return null;
    }
    for (self.monitors.items) |monitor| {
        if (monitor.bar) |bar| {
            if (bar.background.surface == wlSurface or
                bar.tags.surface == wlSurface or
                bar.clock.surface == wlSurface or
                bar.modules.surface == wlSurface)
            {
                return bar;
            }
        }
    }
    return null;
}

M src/main.zig => src/main.zig +20 -16
@@ 42,21 42,24 @@ pub fn main() anyerror!void {
    const program_name = args.nextPosix() orelse unreachable;

    while (args.nextPosix()) |arg| {
        if (mem.eql(u8, arg, "backlight")) {
            try state.modules.register(Modules.Backlight);
        } else if (mem.eql(u8, arg, "battery")) {
            try state.modules.register(Modules.Battery);
        } else if (mem.eql(u8, arg, "pulse")) {
            try state.modules.register(Modules.Pulse);
        } else {
            try help(program_name);
        if (mem.eql(u8, arg, "-h") or mem.eql(u8, arg, "--help")) {
            help(program_name);
            return;
        }
    }

    if (state.modules.modules.items.len == 0) {
        try help(program_name);
        return;
        state.modules.register(arg) catch |err| {
            switch (err) {
                error.UnknownModule => {
                    log.err("unknown module: {s}", .{arg});
                },
                else => {
                    log.err(
                        "initialization error for module {s}: {s}",
                        .{ arg, @errorName(err) },
                    );
                },
            }
            return;
        };
    }

    // event loop


@@ 64,8 67,8 @@ pub fn main() anyerror!void {
    try state.loop.run();
}

fn help(program_name: []const u8) !void {
    const help_text =
fn help(program_name: []const u8) void {
    const text =
        \\Usage: {s} [module]...
        \\
        \\Available modules:


@@ 74,5 77,6 @@ fn help(program_name: []const u8) !void {
        \\    pulse       speaker volume with pulseaudio
        \\
    ;
    try io.getStdErr().writer().print(help_text, .{program_name});
    const w = io.getStdErr().writer();
    w.print(text, .{program_name}) catch unreachable;
}

M src/modules/Backlight.zig => src/modules/Backlight.zig +24 -59
@@ 6,8 6,6 @@ const os = std.os;

const udev = @import("udev");

const Module = @import("../Modules.zig").Module;
const Event = @import("../Loop.zig").Event;
const render = @import("../render.zig");
const State = @import("../main.zig").State;
const utils = @import("../utils.zig");


@@ 16,6 14,7 @@ const Backlight = @This();
state: *State,
context: *udev.Udev,
monitor: *udev.Monitor,
fd: os.fd_t,
devices: DeviceList,

const Device = struct {


@@ 26,56 25,36 @@ const Device = struct {

const DeviceList = std.ArrayList(Device);

pub fn create(state: *State) !*Backlight {
    const self = try state.gpa.create(Backlight);
    self.state = state;
    self.context = try udev.Udev.new();
pub fn init(state: *State) !Backlight {
    const context = try udev.Udev.new();

    self.monitor = try udev.Monitor.newFromNetlink(self.context, "udev");
    try self.monitor.filterAddMatchSubsystemDevType("backlight", null);
    try self.monitor.filterAddMatchSubsystemDevType("power_supply", null);
    try self.monitor.enableReceiving();
    const monitor = try udev.Monitor.newFromNetlink(context, "udev");
    try monitor.filterAddMatchSubsystemDevType("backlight", null);
    try monitor.filterAddMatchSubsystemDevType("power_supply", null);
    try monitor.enableReceiving();

    self.devices = DeviceList.init(state.gpa);
    try updateDevices(state.gpa, self.context, &self.devices);
    if (self.devices.items.len == 0) return error.NoDevicesFound;
    var devices = DeviceList.init(state.gpa);
    try updateDevices(state.gpa, context, &devices);

    return self;
}

pub fn module(self: *Backlight) !Module {
    return Module{
        .impl = @ptrCast(*anyopaque, self),
        .funcs = .{
            .getEvent = getEvent,
            .print = print,
            .destroy = destroy,
        },
    return Backlight{
        .state = state,
        .context = context,
        .monitor = monitor,
        .fd = try monitor.getFd(),
        .devices = devices,
    };
}

fn getEvent(self_opaque: *anyopaque) !Event {
    const self = utils.cast(Backlight)(self_opaque);

    return Event{
        .fd = .{
            .fd = try self.monitor.getFd(),
            .events = os.POLL.IN,
            .revents = undefined,
        },
        .data = self_opaque,
        .callbackIn = callbackIn,
        .callbackOut = Event.noop,
    };
pub fn deinit(self: *Backlight) void {
    _ = self.context.unref();
    for (self.devices.items) |*device| {
        self.state.gpa.free(device.name);
    }
    self.devices.deinit();
}

fn callbackIn(self_opaque: *anyopaque) Event.Action {
    const self = utils.cast(Backlight)(self_opaque);

    _ = self.monitor.receiveDevice() catch |err| {
        log.err("failed to receive udev device: {s}", .{@errorName(err)});
        return .terminate;
    };
pub fn refresh(self: *Backlight) !void {
    _ = try self.monitor.receiveDevice();

    for (self.state.wayland.monitors.items) |monitor| {
        if (monitor.bar) |bar| {


@@ 86,12 65,9 @@ fn callbackIn(self_opaque: *anyopaque) Event.Action {
            }
        }
    }
    return .ok;
}

fn print(self_opaque: *anyopaque, writer: Module.StringWriter) !void {
    const self = utils.cast(Backlight)(self_opaque);

pub fn print(self: *Backlight, writer: anytype) !void {
    try updateDevices(self.state.gpa, self.context, &self.devices);
    const device = self.devices.items[0];
    var percent = @intToFloat(f64, device.value) * 100.0;


@@ 142,14 118,3 @@ fn updateOrAppend(
    device.value = try fmt.parseInt(u64, value, 10);
    device.max = try fmt.parseInt(u64, max, 10);
}

fn destroy(self_opaque: *anyopaque) void {
    const self = utils.cast(Backlight)(self_opaque);

    _ = self.context.unref();
    for (self.devices.items) |*device| {
        self.state.gpa.free(device.name);
    }
    self.devices.deinit();
    self.state.gpa.destroy(self);
}

M src/modules/Battery.zig => src/modules/Battery.zig +21 -57
@@ 15,7 15,7 @@ const Battery = @This();

state: *State,
context: *udev.Udev,
timerFd: os.fd_t,
fd: os.fd_t,
devices: DeviceList,

const Device = struct {


@@ 26,11 26,8 @@ const Device = struct {

const DeviceList = std.ArrayList(Device);

pub fn create(state: *State) !*Battery {
    const self = try state.gpa.create(Battery);
    self.state = state;

    self.timerFd = tfd: {
pub fn init(state: *State) !Battery {
    const tfd = tfd: {
        const fd = os.linux.timerfd_create(
            os.CLOCK.MONOTONIC,
            os.linux.TFD.CLOEXEC,


@@ 43,44 40,29 @@ pub fn create(state: *State) !*Battery {
        break :tfd @intCast(os.fd_t, fd);
    };

    self.context = try udev.Udev.new();

    self.devices = DeviceList.init(state.gpa);
    try updateDevices(state.gpa, self.context, &self.devices);
    if (self.devices.items.len == 0) return error.NoDevicesFound;
    const context = try udev.Udev.new();

    return self;
}
    var devices = DeviceList.init(state.gpa);
    try updateDevices(state.gpa, context, &devices);

pub fn module(self: *Battery) !Module {
    return Module{
        .impl = @ptrCast(*anyopaque, self),
        .funcs = .{
            .getEvent = getEvent,
            .print = print,
            .destroy = destroy,
        },
    return Battery{
        .state = state,
        .context = context,
        .fd = tfd,
        .devices = devices,
    };
}

pub fn getEvent(self_opaque: *anyopaque) !Event {
    const self = utils.cast(Battery)(self_opaque);

    return Event{
        .fd = .{
            .fd = self.timerFd,
            .events = os.POLL.IN,
            .revents = undefined,
        },
        .data = self_opaque,
        .callbackIn = callbackIn,
        .callbackOut = Event.noop,
    };
pub fn deinit(self: *Battery) void {
    _ = self.context.unref();
    for (self.devices.items) |*device| {
        self.state.gpa.free(device.name);
        self.state.gpa.free(device.status);
    }
    self.devices.deinit();
}

pub fn print(self_opaque: *anyopaque, writer: Module.StringWriter) !void {
    const self = utils.cast(Battery)(self_opaque);

pub fn print(self: *Battery, writer: anytype) !void {
    try updateDevices(self.state.gpa, self.context, &self.devices);
    const device = self.devices.items[0];



@@ 96,26 78,9 @@ pub fn print(self_opaque: *anyopaque, writer: Module.StringWriter) !void {
    try fmt.format(writer, "{s}   {d}%", .{ icon, device.capacity });
}

pub fn destroy(self_opaque: *anyopaque) void {
    const self = utils.cast(Battery)(self_opaque);

    _ = self.context.unref();
    for (self.devices.items) |*device| {
        self.state.gpa.free(device.name);
        self.state.gpa.free(device.status);
    }
    self.devices.deinit();
    self.state.gpa.destroy(self);
}

fn callbackIn(self_opaque: *anyopaque) Event.Action {
    const self = utils.cast(Battery)(self_opaque);

pub fn refresh(self: *Battery) !void {
    var expirations = mem.zeroes([8]u8);
    _ = os.read(self.timerFd, &expirations) catch |err| {
        log.err("failed to read timer: {s}", .{@errorName(err)});
        return .terminate;
    };
    _ = try os.read(self.fd, &expirations);

    for (self.state.wayland.monitors.items) |monitor| {
        if (monitor.bar) |bar| {


@@ 128,7 93,6 @@ fn callbackIn(self_opaque: *anyopaque) Event.Action {
            }
        }
    }
    return .ok;
}

fn updateDevices(

M src/modules/Pulse.zig => src/modules/Pulse.zig +49 -82
@@ 13,69 13,66 @@ const utils = @import("../utils.zig");
const Pulse = @This();

state: *State,
fd: os.fd_t,
mainloop: *pulse.pa_threaded_mainloop,
api: *pulse.pa_mainloop_api,
context: *pulse.pa_context,
fd: os.fd_t,
// owned by pulse api
sink_name: []const u8,
sink_is_running: bool,
volume: u8,
muted: bool,

pub fn create(state: *State) !*Pulse {
    const self = try state.gpa.create(Pulse);
    self.state = state;
    self.volume = 0;
    self.muted = false;
    try self.initPulse();

pub fn init(state: *State) !Pulse {
    // create descriptor for poll in Loop
    const fd = try os.eventfd(0, os.linux.EFD.NONBLOCK);
    self.fd = @intCast(os.fd_t, fd);

    return self;
}

pub fn module(self: *Pulse) !Module {
    return Module{
        .impl = @ptrCast(*anyopaque, self),
        .funcs = .{
            .getEvent = getEvent,
            .print = print,
            .destroy = destroy,
        },
    const efd = efd: {
        const fd = try os.eventfd(0, os.linux.EFD.NONBLOCK);
        break :efd @intCast(os.fd_t, fd);
    };
}

fn getEvent(self_opaque: *anyopaque) !Event {
    const self = utils.cast(Pulse)(self_opaque);
    // setup pulseaudio api
    const mainloop = pulse.pa_threaded_mainloop_new() orelse {
        return error.InitFailed;
    };
    const api = pulse.pa_threaded_mainloop_get_api(mainloop);
    const context = pulse.pa_context_new(api, "levee") orelse {
        return error.InitFailed;
    };
    const connected = pulse.pa_context_connect(context, null, pulse.PA_CONTEXT_NOFAIL, null);
    if (connected < 0) return error.InitFailed;

    return Event{
        .fd = .{ .fd = self.fd, .events = os.POLL.IN, .revents = undefined },
        .data = self_opaque,
        .callbackIn = callbackIn,
        .callbackOut = Event.noop,
    return Pulse{
        .state = state,
        .fd = efd,
        .mainloop = mainloop,
        .api = api,
        .context = context,
        .sink_name = "",
        .sink_is_running = false,
        .volume = 0,
        .muted = false,
    };
}

fn print(self_opaque: *anyopaque, writer: Module.StringWriter) !void {
    const self = utils.cast(Pulse)(self_opaque);

    if (self.muted) {
        try writer.print("   🔇   ", .{});
    } else {
        try writer.print("🔊   {d}%", .{self.volume});
    }
pub fn deinit(self: *Pulse) void {
    if (self.api.quit) |quit| quit(self.api, 0);
    pulse.pa_threaded_mainloop_stop(self.mainloop);
    pulse.pa_threaded_mainloop_free(self.mainloop);
}

fn callbackIn(self_opaque: *anyopaque) Event.Action {
    const self = utils.cast(Pulse)(self_opaque);
pub fn start(self: *Pulse) !void {
    pulse.pa_context_set_state_callback(
        self.context,
        contextStateCallback,
        @ptrCast(*anyopaque, self),
    );
    const started = pulse.pa_threaded_mainloop_start(self.mainloop);
    if (started < 0) return error.StartFailed;
}

pub fn refresh(self: *Pulse) !void {
    var data = mem.zeroes([8]u8);
    _ = os.read(self.fd, &data) catch |err| {
        log.err("pulse: failed to read: {s}", .{@errorName(err)});
        return .terminate;
    };
    _ = try os.read(self.fd, &data);

    for (self.state.wayland.monitors.items) |monitor| {
        if (monitor.bar) |bar| {


@@ 88,44 85,14 @@ fn callbackIn(self_opaque: *anyopaque) Event.Action {
            }
        }
    }
    return .ok;
}

fn destroy(self_opaque: *anyopaque) void {
    const self = utils.cast(Pulse)(self_opaque);

    self.deinitPulse();
    self.state.gpa.destroy(self);
}

fn initPulse(self: *Pulse) !void {
    self.mainloop = pulse.pa_threaded_mainloop_new() orelse {
        return error.InitFailed;
    };
    self.api = pulse.pa_threaded_mainloop_get_api(self.mainloop);
    self.context = pulse.pa_context_new(self.api, "levee") orelse {
        return error.InitFailed;
    };
    const connected = pulse.pa_context_connect(
        self.context,
        null,
        pulse.PA_CONTEXT_NOFAIL,
        null,
    );
    if (connected < 0) return error.InitFailed;
    pulse.pa_context_set_state_callback(
        self.context,
        contextStateCallback,
        @ptrCast(*anyopaque, self),
    );
    const started = pulse.pa_threaded_mainloop_start(self.mainloop);
    if (started < 0) return error.InitFailed;
}

fn deinitPulse(self: *Pulse) void {
    if (self.api.quit) |quit| quit(self.api, 0);
    pulse.pa_threaded_mainloop_stop(self.mainloop);
    pulse.pa_threaded_mainloop_free(self.mainloop);
pub fn print(self: *Pulse, writer: anytype) !void {
    if (self.muted) {
        try writer.print("   🔇   ", .{});
    } else {
        try writer.print("🔊   {d}%", .{self.volume});
    }
}

export fn contextStateCallback(


@@ 156,8 123,8 @@ export fn contextStateCallback(
        },
        pulse.PA_CONTEXT_TERMINATED, pulse.PA_CONTEXT_FAILED => {
            log.info("pulse: restarting", .{});
            self.deinitPulse();
            self.initPulse() catch return;
            self.deinit();
            self.* = Pulse.init(self.state) catch return;
            log.info("pulse: restarted", .{});
        },
        else => {},

M src/render.zig => src/render.zig +11 -3
@@ 11,6 11,10 @@ const Bar = @import("Bar.zig");
const Tag = @import("Tags.zig").Tag;
const utils = @import("utils.zig");

const Backlight = @import("modules/Backlight.zig");
const Battery = @import("modules/Battery.zig");
const Pulse = @import("modules/Pulse.zig");

pub const RenderFn = fn (*Bar) anyerror!void;

pub fn renderTags(bar: *Bar) !void {


@@ 102,9 106,13 @@ pub fn renderModules(bar: *Bar) !void {
    defer string.deinit();

    const writer = string.writer();
    for (state.modules.modules.items) |*module| {
        try std.fmt.format(writer, " | ", .{});
        try module.print(writer);
    for (state.modules.order.items) |tag| {
        try writer.print(" | ", .{});
        switch (tag) {
            .backlight => try state.modules.backlight.?.print(writer),
            .battery => try state.modules.battery.?.print(writer),
            .pulse => try state.modules.pulse.?.print(writer),
        }
    }

    // ut8 encoding

M src/utils.zig => src/utils.zig +0 -5
@@ 5,11 5,6 @@ const meta = std.meta;
const os = std.os;
const unicode = std.unicode;

pub fn fatal(comptime format: []const u8, args: anytype) noreturn {
    log.err(format, args);
    os.exit(1);
}

pub fn cast(comptime to: type) fn (*anyopaque) *to {
    return (struct {
        pub fn cast(module: *anyopaque) *to {