creek

A malleable and minimalist status bar for the River compositor

git clone https://git.8pit.net/creek.git

  1const std = @import("std");
  2const log = std.log;
  3const Mutex = std.Thread.Mutex;
  4
  5const wl = @import("wayland").client.wl;
  6
  7const Bar = @import("Bar.zig");
  8const Monitor = @import("Monitor.zig");
  9const render = @import("render.zig");
 10const zriver = @import("wayland").client.zriver;
 11const state = &@import("root").state;
 12
 13pub const Seat = @This();
 14
 15seat_status: *zriver.SeatStatusV1,
 16current_output: ?*wl.Output,
 17window_title: ?[:0]u8,
 18status_buffer: [4096]u8 = undefined,
 19status_text: std.io.FixedBufferStream([]u8),
 20mtx: Mutex,
 21
 22pub fn create() !*Seat {
 23    const self = try state.gpa.create(Seat);
 24    const manager = state.wayland.status_manager.?;
 25    const seat = state.wayland.seat.?;
 26
 27    self.mtx = Mutex{};
 28    self.current_output = null;
 29    self.window_title = null;
 30    self.seat_status = try manager.getRiverSeatStatus(seat);
 31    self.seat_status.setListener(*Seat, seatListener, self);
 32
 33    self.status_text = std.io.fixedBufferStream(&self.status_buffer);
 34    return self;
 35}
 36
 37pub fn destroy(self: *Seat) void {
 38    self.mtx.lock();
 39    if (self.window_title) |w| {
 40        state.gpa.free(w);
 41    }
 42    self.mtx.unlock();
 43
 44    self.seat_status.destroy();
 45    state.gpa.destroy(self);
 46}
 47
 48pub fn focusedMonitor(self: *Seat) ?*Monitor {
 49    // If there is no current monitor, e.g. on startup use the first one.
 50    //
 51    // TODO: Find a better way to do this.
 52    if (self.current_output == null) {
 53        const items = state.wayland.monitors.items;
 54        if (items.len > 0) {
 55            return items[0];
 56        }
 57    }
 58
 59    for (state.wayland.monitors.items) |monitor| {
 60        if (monitor.output == self.current_output) {
 61            return monitor;
 62        }
 63    }
 64
 65    return null;
 66}
 67
 68pub fn focusedBar(self: *Seat) ?*Bar {
 69    if (self.focusedMonitor()) |m| {
 70        return m.confBar();
 71    }
 72
 73    return null;
 74}
 75
 76fn updateTitle(self: *Seat, data: [*:0]const u8) void {
 77    const title = std.mem.sliceTo(data, 0);
 78
 79    self.mtx.lock();
 80    defer self.mtx.unlock();
 81
 82    if (self.window_title) |t| {
 83        state.gpa.free(t);
 84    }
 85    if (title.len == 0) {
 86        self.window_title = null;
 87    } else {
 88        const vz = state.gpa.allocSentinel(u8, title.len, 0) catch |err| {
 89            log.err("allocSentinel failed for window title: {s}\n", .{@errorName(err)});
 90            return;
 91        };
 92        @memcpy(vz[0..vz.len], title);
 93        self.window_title = vz;
 94    }
 95}
 96
 97fn focusedOutput(self: *Seat, output: *wl.Output) void {
 98    var monitor: ?*Monitor = null;
 99    for (state.wayland.monitors.items) |m| {
100        if (m.output == output) {
101            monitor = m;
102            break;
103        }
104    }
105
106    if (monitor) |m| {
107        if (m.confBar()) |bar| {
108            self.current_output = m.output;
109            render.renderText(bar, self.status_text.getWritten()) catch |err| {
110                log.err("renderText failed on focus for monitor {}: {s}",
111                    .{m.globalName, @errorName(err)});
112                return;
113            };
114
115            bar.text.surface.commit();
116            bar.background.surface.commit();
117        }
118    } else {
119        log.err("seatListener: couldn't find focused output", .{});
120    }
121}
122
123fn unfocusedOutput(self: *Seat, output: *wl.Output) void {
124    var monitor: ?*Monitor = null;
125    for (state.wayland.monitors.items) |m| {
126        if (m.output == output) {
127            monitor = m;
128            break;
129        }
130    }
131
132    if (monitor) |m| {
133        if (m.confBar()) |bar| {
134            render.resetText(bar) catch |err| {
135                log.err("resetText failed for monitor {}: {s}",
136                    .{bar.monitor.globalName, @errorName(err)});
137            };
138            bar.text.surface.commit();
139
140            render.renderTitle(bar, null) catch |err| {
141                log.err("renderTitle failed on unfocus for monitor {}: {s}",
142                    .{bar.monitor.globalName, @errorName(err)});
143                return;
144            };
145
146            bar.title.surface.commit();
147            bar.background.surface.commit();
148        }
149    } else {
150        log.err("seatListener: couldn't find unfocused output", .{});
151    }
152
153    self.current_output = null;
154}
155
156fn focusedView(self: *Seat, title: [*:0]const u8) void {
157    self.updateTitle(title);
158    if (self.focusedBar()) |bar| {
159        render.renderTitle(bar, self.window_title) catch |err| {
160            log.err("renderTitle failed on focused view for monitor {}: {s}",
161                .{bar.monitor.globalName, @errorName(err)});
162            return;
163        };
164
165        bar.title.surface.commit();
166        bar.background.surface.commit();
167    }
168}
169
170fn seatListener(
171    _: *zriver.SeatStatusV1,
172    event: zriver.SeatStatusV1.Event,
173    seat: *Seat,
174) void {
175    switch (event) {
176        .focused_output => |data| seat.focusedOutput(data.output.?),
177        .unfocused_output => |data| seat.unfocusedOutput(data.output.?),
178        .focused_view => |data| seat.focusedView(data.title),
179    }
180}