Add time_format config for custom strftime strings

This lets the user change to any time format they want in the bar.
As part of this, we also change the bar to re-draw every second (to
allow using seconds in the time format string).
This commit is contained in:
Ben Buhse 2026-02-27 10:51:42 -06:00
commit 0e7d652d24
No known key found for this signature in database
GPG key ID: 7916ACFCD38FD0B4
7 changed files with 50 additions and 11 deletions

View file

@ -7,6 +7,9 @@ const Bar = @This();
/// Standard base DPI at a scale of 1
const base_dpi = 96;
/// Default strftime string to use for the clock
pub const default_time_format = "%Y-%m-%d %H:%M, %A";
context: *Context,
/// The timezone of the computer.
@ -61,6 +64,9 @@ pub const Options = struct {
vertical_padding: u8 = 5,
/// Horizontal padding between bar edges and content, in pixels
horizontal_padding: u8 = 5,
/// strftime format string for the clock display
time_format: []const u8 = default_time_format,
};
pub fn init(context: *Context, output: *Output, options: Options) !Bar {
@ -240,7 +246,7 @@ pub fn draw(bar: *Bar) !void {
// Convert time to a string
var time_buf: [255:0]u8 = undefined;
var time_writer = Io.Writer.fixed(&time_buf);
try dt.strftime(&time_writer, "%H:%M");
try dt.strftime(&time_writer, options.time_format);
// Convert date string to Unicode codepoints
const time_codepoints = try utils.utf8ToCodepoints(time_writer.buffered());

View file

@ -12,6 +12,7 @@ const NodeName = enum {
vertical_padding,
horizontal_padding,
margins,
time_format,
};
const MarginsNodeName = enum { top, right, bottom, left };
@ -38,6 +39,10 @@ vertical_padding: u8 = 5,
/// Horizontal padding between bar edges and content, in pixels
horizontal_padding: u8 = 5,
/// strftime format string for the clock display.
/// null means use the default.
time_format: ?[]const u8 = null,
pub fn toBarOptions(config: BarConfig) Bar.Options {
return .{
.fonts = config.fonts orelse "monospace:size=14",
@ -52,6 +57,7 @@ pub fn toBarOptions(config: BarConfig) Bar.Options {
},
.vertical_padding = config.vertical_padding,
.horizontal_padding = config.horizontal_padding,
.time_format = config.time_format orelse Bar.default_time_format,
};
}
@ -92,7 +98,18 @@ pub fn load(config: *Config, parser: *kdl.Parser, hostname: ?[]const u8) !void {
logWarnInvalidNodeArg(name, val_str);
}
},
.time_format => {
if (node.argcount() < 1) {
logWarnMissingNodeArg(name, "format string");
continue;
}
if (validateTimeFormat(val_str)) {
config.bar_config.?.time_format = utils.gpa.dupe(u8, val_str) catch @panic("Out of memory");
logDebugSettingNode(name, val_str);
} else {
logWarnInvalidNodeArg(name, val_str);
}
},
.margins => next_child_block = .margins,
inline .background_color,
.text_color,
@ -192,15 +209,26 @@ inline fn logWarnInvalidNodeArg(node_name: anytype, node_value: []const u8) void
}
}
fn validateTimeFormat(format: []const u8) bool {
// Try formatting with a dummy time to validate the format string
var buf: [255]u8 = undefined;
var writer = Io.Writer.fixed(&buf);
const dummy_time = zeit.Time{};
dummy_time.strftime(&writer, format) catch return false;
return true;
}
inline fn logWarnMissingNodeArg(node_name: NodeName, comptime arg: []const u8) void {
log.warn("\"bar.{s}\" missing " ++ arg ++ " argument. Ignoring", .{@tagName(node_name)});
}
const std = @import("std");
const fmt = std.fmt;
const Io = std.Io;
const kdl = @import("kdl");
const pixman = @import("pixman");
const zeit = @import("zeit");
const utils = @import("../utils.zig");
const Bar = @import("../Bar.zig");

View file

@ -129,19 +129,17 @@ fn run(wl_display: *wl.Display, context: *Context) !void {
fatal("wl_display flush failed: E{s}", .{@tagName(errno)});
}
// Get the number of milliseconds to the top of the next minute
const time = std.time.timestamp();
if (time < 0) {
log.err("Got a negative time ({d})", .{time});
return error.InvalidTime;
}
const timeout: i32 = @intCast((@divFloor(time, 60) * 60 + 60 - time) * 1000);
// Get the number of milliseconds to the top of the next second
const time_ns = std.time.nanoTimestamp();
const ns_per_sec = std.time.ns_per_s;
const remainder_ns = @mod(time_ns, ns_per_sec);
const timeout: i32 = @intCast(@divFloor(ns_per_sec - remainder_ns, std.time.ns_per_ms));
const poll_rc = posix.poll(&pollfds, timeout) catch |err| {
fatal("Failed to poll {s}", .{@errorName(err)});
};
if (poll_rc == 0) {
// If poll returns 0, it timed out, meaning we hit the top of the minute
// If poll returns 0, it timed out, meaning we hit the top of the next second
// and need to update the clock.
var it = context.wm.outputs.iterator(.forward);
while (it.next()) |output| {