stevee

My wayland statusbar
git clone git://gtms.dev/stevee
Log | Files | Refs | Submodules | README | LICENSE

commit 06fedf5965f432f21f107955e5d68a7f28abafef
parent 4ece040cbf210783eceaa3baab84c32d36be1d40
Author: Andrea Feletto <andrea@andreafeletto.com>
Date:   Mon, 24 Jan 2022 22:59:25 +0100

remove stdin support and add battery module

Diffstat:
MREADME.md | 14+-------------
Dcontrib/status | 128-------------------------------------------------------------------------------
Msrc/config.zig | 2++
Msrc/event.zig | 18+++++++++++-------
Msrc/main.zig | 9+++++++++
Asrc/modules.zig | 125+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/render.zig | 32++++++++++++++++++++------------
Msrc/wayland.zig | 29+++++++++++++++--------------
8 files changed, 183 insertions(+), 174 deletions(-)

diff --git a/README.md b/README.md @@ -16,20 +16,8 @@ zig build -Drelease-safe --prefix ~/.local install ## Usage -Levee renders the last line received through `stdin` at the right of the -statusbar. -A sample script is provided in the `contrib` folder which prints brightness, -volume and battery level. - -``` -./contrib/status | levee -``` - -The script refreshes automatically every 10 seconds, but can be refreshed -immediately by running: - ``` -./contrib/status --refresh +levee ``` ## Dependencies diff --git a/contrib/status b/contrib/status @@ -1,128 +0,0 @@ -#!/bin/sh - -PrgName=${0##*/} - -PrgDir="$HOME/.local/share/$PrgName" -mkdir -p "$PrgDir" -PidPath="$PrgDir/$PrgName.pid" - -Usage() { - while read -r Line; do - printf '%b\n' "$Line" - done <<-EOF - \rUsage: $PrgName OPTION - \rStatusbar widgets for levee. - - \rOptions: - \r -h, --help, -? display this help and exit - \r -r, --refresh refresh - \r -s, --stop stop daemon if one exists - EOF -} - -Die() { - printf 'error: %s\n' "$1" 1>&2 - exit 1 -} - -Battery() { - CapacityPath='/sys/class/power_supply/BAT1/capacity' - StatusPath='/sys/class/power_supply/BAT1/status' - if [ -r "$CapacityPath" ] && [ -r "$StatusPath" ]; then - read -r Status <"$StatusPath" - case $Status in - Discharging) - Icon="🔋" - ;; - Charging) - Icon="🔌" - ;; - Full) - Icon="⚡" - ;; - Unknown) - Icon="❓" - ;; - esac - read -r Capacity <"$CapacityPath" - printf '%s %4s%%\n' "$Icon" "$Capacity" - else - Die 'unable to access power supply files' - fi -} - -Volume() { - if ! command -v pulsemixer >/dev/null; then - Die 'missing dependency: pulsemixer' - fi - - if [ "$(pulsemixer --get-mute)" -eq 1 ]; then - printf ' 🔇 \n' - else - Value=$(pulsemixer --get-volume) - printf '🔊 %4s%%\n' "${Value%% *}" - fi -} - -Backlight() { - Brightness=$(brightnessctl get) - MaxBrightness=$(brightnessctl max) - Value=$((100 * Brightness / MaxBrightness)) - printf '💡 %4d%%\n' "${Value%%.*}" -} - -Refresh() { - printf '%s' "$$" >"$PidPath" - printf '%s | %s | %s\n' "$(Backlight)" "$(Volume)" "$(Battery)" -} - -GetDaemonPid() { - if [ -r "$PidPath" ]; then - read -r Pid <"$PidPath" - printf '%s' "$Pid" - fi -} - -SendRefresh() { - Pid=$(GetDaemonPid) - if [ -n "$Pid" ]; then - kill -USR1 "$Pid" - else - Die 'no daemon to refresh' - fi -} - -StopDaemon() { - Pid=$(GetDaemonPid) - if [ -n "$Pid" ] && kill -0 "$Pid" ; then - kill -15 "$Pid" - else - Die 'no daemon to kill' - fi -} - -if [ $# -gt 1 ]; then - Usage - exit 1 -fi - -case $1 in ---help | -h | -\?) - Usage - exit 0 ;; ---refresh | -r) - SendRefresh - exit 0 ;; ---stop | -s) - StopDaemon - exit 0 ;; -esac - -trap ':' USR1 -trap 'rm $PidPath; exit 0' INT TERM - -while true; do - Refresh - sleep 10 & - wait $! -done diff --git a/src/config.zig b/src/config.zig @@ -8,6 +8,7 @@ pub const Config = struct { border: u15, font: *fcft.Font, clockFormat: [*:0]const u8, + batteryDir: []const u8, pub fn init() !Config { var font_names = [_][*:0]const u8{"monospace:size=14"}; @@ -29,6 +30,7 @@ pub const Config = struct { .border = 2, .font = try fcft.Font.fromName(&font_names, null), .clockFormat = "%d %b %Y - %R", + .batteryDir = "BAT1", }; } }; diff --git a/src/event.zig b/src/event.zig @@ -13,6 +13,7 @@ pub const Loop = struct { fds: [3]os.pollfd, pub fn init(state: *State) !Loop { + // Timer const tfd = os.linux.timerfd_create( os.CLOCK.MONOTONIC, os.linux.TFD.CLOEXEC, @@ -23,6 +24,9 @@ pub const Loop = struct { }; _ = os.linux.timerfd_settime(@intCast(i32, tfd), 0, &interval, null); + // inotify + const ifd = os.linux.inotify_init1(os.linux.IN.CLOEXEC); + return Loop{ .state = state, .fds = .{ @@ -37,7 +41,7 @@ pub const Loop = struct { .revents = 0, }, .{ - .fd = os.linux.STDIN_FILENO, + .fd = @intCast(os.fd_t, ifd), .events = os.POLL.IN, .revents = 0, }, @@ -90,17 +94,17 @@ pub const Loop = struct { } } - // stdin + // inotify if (self.fds[2].revents & os.POLL.IN != 0) { - var buffer = [_]u8{0} ** 256; - const len = try os.read(self.fds[2].fd, &buffer); - if (len == 0) continue; + const ifd = self.fds[2].fd; + var event = mem.zeroes(os.linux.inotify_event); + _ = try os.read(ifd, mem.asBytes(&event)); for (self.state.wayland.outputs.items) |output| { if (output.surface) |surface| { if (surface.configured) { - render.renderCustom(surface, buffer[0 .. len - 1]) catch continue; - surface.customSurface.commit(); + render.renderModules(surface) catch continue; + surface.modulesSurface.commit(); surface.backgroundSurface.commit(); } } diff --git a/src/main.zig b/src/main.zig @@ -2,6 +2,7 @@ const std = @import("std"); const Config = @import("config.zig").Config; const Loop = @import("event.zig").Loop; +const modules = @import("modules.zig"); const Wayland = @import("wayland.zig").Wayland; pub const State = struct { @@ -9,6 +10,9 @@ pub const State = struct { config: Config, wayland: Wayland, loop: Loop, + + battery: modules.Battery, + modules: std.ArrayList(modules.Module), }; pub fn main() anyerror!void { @@ -22,6 +26,11 @@ pub fn main() anyerror!void { state.wayland = try Wayland.init(&state); state.loop = try Loop.init(&state); + std.log.info("modules initialization", .{}); + state.modules = std.ArrayList(modules.Module).init(state.allocator); + state.battery = try modules.Battery.init(&state); + try state.modules.append(state.battery.module()); + std.log.info("wayland globals registration", .{}); try state.wayland.registerGlobals(); diff --git a/src/modules.zig b/src/modules.zig @@ -0,0 +1,125 @@ +const std = @import("std"); +const fs = std.fs; +const io = std.io; +const mem = std.mem; +const os = std.os; + +const State = @import("main.zig").State; + +const StringWriter = std.ArrayList(u8).Writer; + +pub const Module = struct { + impl: *anyopaque, + + printFn: fn (*anyopaque, StringWriter) anyerror!void, + + pub fn print(self: *Module, writer: StringWriter) !void { + try self.printFn(self.impl, writer); + } + + pub fn cast(comptime to: type) fn (*anyopaque) *to { + return (struct { + pub fn cast(module: *anyopaque) *to { + return @ptrCast(*to, @alignCast(@alignOf(to), module)); + } + }).cast; + } +}; + +pub const Battery = struct { + state: *State, + path: []const u8, + watch: i32, + + pub const Data = struct { + capacity: u8, + icon: []const u8, + }; + + pub fn init(state: *State) !Battery { + const path = try fs.path.join( + state.allocator, + &.{ "/sys/class/power_supply", state.config.batteryDir }, + ); + + const uevent_path = try fs.path.joinZ( + state.allocator, + &.{ path, "uevent" }, + ); + defer state.allocator.free(uevent_path); + + const watch = os.linux.inotify_add_watch( + state.loop.fds[2].fd, + uevent_path, + os.linux.IN.ACCESS, + ); + + return Battery{ + .state = state, + .path = path, + .watch = @intCast(i32, watch), + }; + } + + pub fn module(self: *Battery) Module { + return .{ .impl = @ptrCast(*anyopaque, self), .printFn = print }; + } + + pub fn print(self_opaque: *anyopaque, writer: StringWriter) !void { + const self = Module.cast(Battery)(self_opaque); + + const data = try self.readData(); + try std.fmt.format(writer, "{s} {d}%", .{ data.icon, data.capacity }); + } + + fn readData(self: *const Battery) !Data { + const voltage = try self.readInt("voltage_now"); + const charge = try self.readInt("charge_now"); + const charge_full = try self.readInt("charge_full"); + + const energy = @as(u64, charge) * @as(u64, voltage) / 1000000; + const energy_full = @as(u64, charge_full) * @as(u64, voltage) / 1000000; + + var capacity = @intToFloat(f64, energy) * 100.0; + capacity /= @intToFloat(f64, energy_full); + + const status = try self.readValue("status"); + + var icon: []const u8 = "❓"; + if (mem.eql(u8, status, "Discharging")) { + icon = "🔋"; + } else if (mem.eql(u8, status, "Charging")) { + icon = "🔌"; + } else if (mem.eql(u8, status, "Full")) { + icon = "⚡"; + } + + return Data{ + .capacity = @floatToInt(u8, @round(capacity)), + .icon = icon, + }; + } + + fn readInt(self: *const Battery, filename: []const u8) !u32 { + const value = try self.readValue(filename); + defer self.state.allocator.free(value); + + return std.fmt.parseInt(u32, value, 10); + } + + fn readValue(self: *const Battery, filename: []const u8) ![]u8 { + const state = self.state; + + const path = try fs.path.join( + state.allocator, + &.{ self.path, filename }, + ); + defer state.allocator.free(path); + + const file = try fs.openFileAbsolute(path, .{}); + defer file.close(); + + var str = try file.readToEndAlloc(state.allocator, 128); + return state.allocator.resize(str, str.len - 1).?; + } +}; diff --git a/src/render.zig b/src/render.zig @@ -121,30 +121,31 @@ pub fn renderClock(surface: *Surface) !void { wlSurface.attach(buffer.buffer, 0, 0); } -pub fn renderCustom(surface: *Surface, str: []const u8) !void { +pub fn renderModules(surface: *Surface) !void { const state = surface.output.state; - const wlSurface = surface.customSurface; + const wlSurface = surface.modulesSurface; const buffer = try Buffer.nextBuffer( - &surface.customBuffers, + &surface.modulesBuffers, surface.output.state.wayland.shm, surface.width, surface.height, ); buffer.busy = true; - // clear the buffer - const bg_area = [_]pixman.Rectangle16{ - .{ .x = 0, .y = 0, .width = surface.width, .height = surface.height }, - }; - const bg_color = mem.zeroes(pixman.Color); - _ = pixman.Image.fillRectangles(.src, buffer.pix.?, &bg_color, 1, &bg_area); + // compose string + var string = std.ArrayList(u8).init(state.allocator); + const writer = string.writer(); + for (state.modules.items) |*module| { + try std.fmt.format(writer, " | ", .{}); + try module.print(writer); + } // ut8 encoding - const utf8 = try std.unicode.Utf8View.init(str); + const utf8 = try std.unicode.Utf8View.init(string.items); var utf8_iter = utf8.iterator(); - var runes = try state.allocator.alloc(c_int, str.len); + var runes = try state.allocator.alloc(c_int, string.items.len); defer state.allocator.free(runes); var i: usize = 0; @@ -153,10 +154,17 @@ pub fn renderCustom(surface: *Surface, str: []const u8) !void { } runes = state.allocator.resize(runes, i).?; + // clear the buffer + const bg_area = [_]pixman.Rectangle16{ + .{ .x = 0, .y = 0, .width = surface.width, .height = surface.height }, + }; + const bg_color = mem.zeroes(pixman.Color); + _ = pixman.Image.fillRectangles(.src, buffer.pix.?, &bg_color, 1, &bg_area); + + // compute offsets const run = try fcft.TextRun.rasterize(state.config.font, runes, .default); defer run.destroy(); - // compute offsets i = 0; var text_width: u32 = 0; while (i < run.count) : (i += 1) { diff --git a/src/wayland.zig b/src/wayland.zig @@ -119,7 +119,7 @@ pub const Wayland = struct { if (surface.backgroundSurface == wlSurface or surface.tagsSurface == wlSurface or surface.clockSurface == wlSurface or - surface.customSurface == wlSurface) + surface.modulesSurface == wlSurface) { return surface; } @@ -281,9 +281,9 @@ pub const Surface = struct { clockSubsurface: *wl.Subsurface, clockBuffers: [2]Buffer, - customSurface: *wl.Surface, - customSubsurface: *wl.Subsurface, - customBuffers: [2]Buffer, + modulesSurface: *wl.Surface, + modulesSubsurface: *wl.Subsurface, + modulesBuffers: [2]Buffer, configured: bool, width: u16, @@ -320,12 +320,12 @@ pub const Surface = struct { ); self.clockBuffers = mem.zeroes([2]Buffer); - self.customSurface = try wayland.compositor.createSurface(); - self.customSubsurface = try wayland.subcompositor.getSubsurface( - self.customSurface, + self.modulesSurface = try wayland.compositor.createSurface(); + self.modulesSubsurface = try wayland.subcompositor.getSubsurface( + self.modulesSurface, self.backgroundSurface, ); - self.customBuffers = mem.zeroes([2]Buffer); + self.modulesBuffers = mem.zeroes([2]Buffer); // setup layer surface self.layerSurface.setSize(0, state.config.height); @@ -339,7 +339,7 @@ pub const Surface = struct { // setup subsurfaces self.tagsSubsurface.setPosition(0, 0); self.clockSubsurface.setPosition(0, 0); - self.customSubsurface.setPosition(0, 0); + self.modulesSubsurface.setPosition(0, 0); self.tagsSurface.commit(); self.clockSurface.commit(); @@ -366,10 +366,10 @@ pub const Surface = struct { self.clockBuffers[0].deinit(); self.clockBuffers[1].deinit(); - self.customSurface.destroy(); - self.customSubsurface.destroy(); - self.customBuffers[0].deinit(); - self.customBuffers[1].deinit(); + self.modulesSurface.destroy(); + self.modulesSubsurface.destroy(); + self.modulesBuffers[0].deinit(); + self.modulesBuffers[1].deinit(); self.output.state.allocator.destroy(self); } @@ -390,10 +390,11 @@ pub const Surface = struct { render.renderBackground(surface) catch return; render.renderTags(surface) catch return; render.renderClock(surface) catch return; + render.renderModules(surface) catch return; surface.tagsSurface.commit(); surface.clockSurface.commit(); - surface.customSurface.commit(); + surface.modulesSurface.commit(); surface.backgroundSurface.commit(); }, .closed => {