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

@ -126,9 +126,8 @@ do not re-trigger rules.
## Bar
The bar is an optional widget that shows the focused window title on the left,
the date/time in the center, and layout info (primary count/ratio and visible
window count) on the right. It is only created when a `bar` block is present in
The bar is an optional widget that displays configurable components in three
slots: left, center, and right. It is only created when a `bar` block is present in
the config. All settings have defaults, with the color based on the Catppuccin
Mocha theme. An empty block can be used to enable the widget with all defaults:
@ -145,9 +144,12 @@ bar {
| `text_color` | color | `0xcdd6f4` | Text color |
| `background_color` | color | `0x1e1e2e` | Background color |
| `position` | enum | `top` | Bar position (`top` or `bottom`) |
| `left` | enum | `title` | Component in the left slot (`title`, `clock`, `wm_info`, `none`) |
| `center` | enum | `clock` | Component in the center slot (`title`, `clock`, `wm_info`, `none`) |
| `right` | enum | `wm_info` | Component in the right slot (`title`, `clock`, `wm_info`, `none`) |
| `vertical_padding` | u8 | `5` | Vertical padding above and below text |
| `horizontal_padding` | u8 | `5` | Horizontal padding between bar edges and text |
| `time_format` | string | `%Y-%m-%d %H:%M, %A` | strftime format string for the clock display (an empty string hides the clock) |
| `time_format` | string | `%Y-%m-%d %H:%M, %A` | strftime format string for the clock display |
### Margins

View file

@ -2,12 +2,12 @@
These are in rough order of my priority, though no promises I do them in this order.
- [ ] Support window tag/order caching between WM restarts (within a river session)
- [ ] Add gap support
- [ ] Add build-time options for including the wallpaper (and maybe bar)
- [ ] Check pointer position and only warp if not on focused window already
- [ ] Change focus direction when closing window
- [ ] Use set_xcursor_theme request
- [ ] Support configuring bar item positions (left/center/right)
- [ ] Support overriding config location
- [ ] Add support for center-primary layout
- [ ] Support per-output bar visibility
@ -50,3 +50,4 @@ These are in rough order of my priority, though no promises I do them in this or
- [x] Add focused window title to bar
- [x] Add bar padding to config
- [x] Support 12-hour clock format (maybe take any time format string?)
- [x] Support configuring bar item positions (left/center/right)

View file

@ -114,9 +114,8 @@ initialization do not re-trigger rules.
# BAR
The bar is an optional widget that shows the focused window title on the left,
the date/time in the center, and layout info (primary count/ratio and visible
window count) on the right. It is only created when a *bar* block is present
The bar is an optional widget that displays configurable components in three
slots: left, center, and right. It is only created when a *bar* block is present
in the config:
```
@ -137,6 +136,15 @@ bar {
*position* *top*|*bottom*
Bar position. (Default: *top*)
*left* *title*|*clock*|*wm_info*|*none*
Component shown in the left slot. (Default: *title*)
*center* *title*|*clock*|*wm_info*|*none*
Component shown in the center slot. (Default: *clock*)
*right* *title*|*clock*|*wm_info*|*none*
Component shown in the right slot. (Default: *wm_info*)
*vertical_padding* _pixels_
Vertical padding above and below text. (Default: 5)
@ -145,8 +153,7 @@ bar {
*time_format* _format_
strftime format string for the clock display. Invalid format strings
are ignored and the default is used instead. Set to an empty string
to hide the clock. (Default: "%Y-%m-%d %H:%M, %A")
are ignored and the default is used instead. (Default: "%Y-%m-%d %H:%M, %A")
The bar also supports *margins* and *anchors* child blocks; see *TAG OVERLAY*
for their format.

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);
// 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);
// 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);
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 {