Implement configurable component locations in bar

This allows the user to configure which component (title, wm_info, clock)
is rendered to which part of the bar (left, right, center).

You can also use `none` to hide the location.
This commit is contained in:
Ben Buhse 2026-02-27 11:33:50 -06:00
commit 040ccc14f3
No known key found for this signature in database
GPG key ID: 7916ACFCD38FD0B4
5 changed files with 104 additions and 68 deletions

View file

@ -46,6 +46,7 @@ pub const PendingRender = struct {
};
pub const Position = enum { top, bottom };
pub const Component = enum { title, clock, wm_info, none };
pub const Options = struct {
/// Comma separated list of FontConfig formatted font specifications
@ -67,6 +68,13 @@ pub const Options = struct {
/// strftime format string for the clock display
time_format: []const u8 = default_time_format,
/// Which component to show on the left side of the bar
left: Component = .title,
/// Which component to show in the center of the bar
center: Component = .clock,
/// Which component to show on the right side of the bar
right: Component = .wm_info,
};
pub fn init(context: *Context, output: *Output, options: Options) !Bar {
@ -234,55 +242,26 @@ pub fn draw(bar: *Bar) !void {
// Y is shared between all components
const y: i32 = @divFloor(buffer.height - bar.fcft_fonts.height, 2);
// Get the current time in seconds since the epoch,
// then load the local timezone,
// then convert `now` to the `local` timezone
const now = try zeit.instant(.{});
const now_local = now.in(&bar.timezone);
// Pre-compute codepoints for each component type
// Generate date/time info for this instant
const dt = now_local.time();
// Convert time to a string
// Clock
var time_buf: [255:0]u8 = undefined;
var time_writer = Io.Writer.fixed(&time_buf);
try dt.strftime(&time_writer, options.time_format);
const now = try zeit.instant(.{});
const now_local = now.in(&bar.timezone);
try now_local.time().strftime(&time_writer, options.time_format);
const clock_codepoints = try utils.utf8ToCodepoints(time_writer.buffered());
defer utils.gpa.free(clock_codepoints);
// Convert date string to Unicode codepoints
const time_codepoints = try utils.utf8ToCodepoints(time_writer.buffered());
defer utils.gpa.free(time_codepoints);
// Title (empty string if no focused window)
const focused_title: []const u8 = if (context.wm.seats.first()) |seat|
if (seat.focused_window) |window| window.title orelse "" else ""
else
"";
const title_codepoints = try utils.utf8ToCodepoints(focused_title);
defer utils.gpa.free(title_codepoints);
// Get the width of the date string so we can truncate title
const center_width = try bar.textWidth(time_codepoints);
// X changes
var center_x: i32 = @divFloor(buffer.width - center_width, 2);
// Write title of focused window to the left side of the bar
if (context.wm.seats.first()) |seat| {
if (seat.focused_window) |window| {
if (window.title) |title| {
if (title.len > 0) {
const title_codepoints = try utils.utf8ToCodepoints(title);
defer utils.gpa.free(title_codepoints);
const max_left_width = center_x - 2 * options.horizontal_padding;
const truncated_codepoints = try bar.truncateToWidth(title_codepoints, max_left_width);
var left_x: i32 = options.horizontal_padding;
try bar.renderChars(
truncated_codepoints,
buffer,
&left_x,
y,
text_color,
);
}
}
}
}
// Put WM info on the right side of the bar
// WM info
const output = bar.output;
var wm_info_buf: [255:0]u8 = undefined;
var wm_info_writer = Io.Writer.fixed(&wm_info_buf);
@ -296,23 +275,48 @@ pub fn draw(bar: *Bar) !void {
const wm_info_codepoints = try utils.utf8ToCodepoints(wm_info_writer.buffered());
defer utils.gpa.free(wm_info_codepoints);
const max_right_width = buffer.width - (center_x + center_width) - 2 * options.horizontal_padding;
const right_truncated = try bar.truncateToWidth(wm_info_codepoints, max_right_width);
const right_text_width = try bar.textWidth(right_truncated);
// Map a Component to its pre-computed codepoints slice
const componentSlice = struct {
fn f(component: Component, clock: []u32, title: []u32, wm_info: []u32) []u32 {
return switch (component) {
.clock => clock,
.title => title,
.wm_info => wm_info,
.none => &.{},
};
}
}.f;
var right_x: i32 = buffer.width - right_text_width - options.horizontal_padding;
try bar.renderChars(
right_truncated,
buffer,
&right_x,
y,
text_color,
);
// Measure center first needed to constrain left and right widths
const center_codepoints = componentSlice(options.center, clock_codepoints, title_codepoints, wm_info_codepoints);
const center_width = try bar.textWidth(center_codepoints);
var center_x: i32 = @divFloor(buffer.width - center_width, 2);
// Finally, put the time in the center of the bar
try bar.renderChars(time_codepoints, buffer, &center_x, y, text_color);
// Render left slot
const left_codepoints = componentSlice(options.left, clock_codepoints, title_codepoints, wm_info_codepoints);
if (left_codepoints.len > 0) {
const max_width = center_x - 2 * options.horizontal_padding;
const truncated = try bar.truncateToWidth(left_codepoints, max_width);
var x: i32 = options.horizontal_padding;
try bar.renderChars(truncated, buffer, &x, y, text_color);
}
// Really finally, attach the buffer to the surface
// Render right slot
const right_codepoints = componentSlice(options.right, clock_codepoints, title_codepoints, wm_info_codepoints);
if (right_codepoints.len > 0) {
const max_width = buffer.width - (center_x + center_width) - 2 * options.horizontal_padding;
const truncated = try bar.truncateToWidth(right_codepoints, max_width);
const text_width = try bar.textWidth(truncated);
var x: i32 = buffer.width - text_width - options.horizontal_padding;
try bar.renderChars(truncated, buffer, &x, y, text_color);
}
// Render center slot
if (center_codepoints.len > 0) {
try bar.renderChars(center_codepoints, buffer, &center_x, y, text_color);
}
// Attach the buffer to the surface
const surfaces = bar.surfaces orelse return error.NoSurfaces;
const wl_surface = surfaces.wl_surface;
// sync_next_commit ensures frame-perfect application

View file

@ -9,6 +9,9 @@ const NodeName = enum {
text_color,
background_color,
position,
left,
center,
right,
vertical_padding,
horizontal_padding,
margins,
@ -25,6 +28,13 @@ background_color: pixman.Color = utils.parseRgbaPixmanComptime("0x1e1e2e"),
/// Whether the bar is at the top or bottom of the screen
position: Bar.Position = .top,
/// Which component to show on the left side of the bar
left: Bar.Component = .title,
/// Which component to show in the center of the bar
center: Bar.Component = .clock,
/// Which component to show on the right side of the bar
right: Bar.Component = .wm_info,
/// Margin above the top of the bar and another element (a window or the top of the output)
margin_top: u8 = 0,
/// Margin above the right of the bar and another element (a window or the top of the output)
@ -49,6 +59,9 @@ pub fn toBarOptions(config: BarConfig) Bar.Options {
.text_color = config.text_color,
.background_color = config.background_color,
.position = config.position,
.left = config.left,
.center = config.center,
.right = config.right,
.margins = .{
.top = config.margin_top,
.right = config.margin_right,
@ -129,6 +142,15 @@ pub fn load(config: *Config, parser: *kdl.Parser, hostname: ?[]const u8) !void {
continue;
};
@field(config.bar_config.?, @tagName(tag)) = padding;
logDebugSettingNode(name, padding_str);
},
inline .left, .center, .right => |tag| {
if (std.meta.stringToEnum(Bar.Component, val_str)) |component| {
@field(config.bar_config.?, @tagName(tag)) = component;
logDebugSettingNode(name, val_str);
} else {
logWarnInvalidNodeArg(name, val_str);
}
},
}
} else {