diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index e68daf1..f7a24ff 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -147,6 +147,7 @@ bar { | `position` | enum | `top` | Bar position (`top` or `bottom`) | | `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) | ### Margins diff --git a/docs/TODO.md b/docs/TODO.md index 0140a53..d5493b8 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -10,7 +10,6 @@ These are in rough order of my priority, though no promises I do them in this or - [ ] Support configuring bar item positions (left/center/right) - [ ] Support overriding config location - [ ] Add support for center-primary layout -- [ ] Support 12-hour clock format (maybe take any time format string?) - [ ] Support per-output bar visibility - [ ] Support more window rule options (e.g. ssd/csd) - [ ] Support solid `background-color` fallback (no wallpaper) @@ -50,3 +49,4 @@ These are in rough order of my priority, though no promises I do them in this or - [x] Support configuring primary vs secondary stack side - [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?) diff --git a/examples/config.kdl b/examples/config.kdl index 8c43c34..2fa5222 100644 --- a/examples/config.kdl +++ b/examples/config.kdl @@ -28,6 +28,7 @@ borders { // Bar widget; shows the time bar { position top + time_format "%H:%M" } // Tag overlay widget; shown briefly when switching tags // Remove this block to disable the overlay entirely diff --git a/man/beansprout.5.scd b/man/beansprout.5.scd index 2c73352..7ac835c 100644 --- a/man/beansprout.5.scd +++ b/man/beansprout.5.scd @@ -143,6 +143,11 @@ bar { *horizontal_padding* _pixels_ Horizontal padding between bar edges and text. (Default: 5) +*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") + The bar also supports *margins* and *anchors* child blocks; see *TAG OVERLAY* for their format. diff --git a/src/Bar.zig b/src/Bar.zig index 98a65e6..682edd2 100644 --- a/src/Bar.zig +++ b/src/Bar.zig @@ -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()); diff --git a/src/config/BarConfig.zig b/src/config/BarConfig.zig index bf20c67..34ebf99 100644 --- a/src/config/BarConfig.zig +++ b/src/config/BarConfig.zig @@ -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"); diff --git a/src/main.zig b/src/main.zig index 4191091..ba9eceb 100644 --- a/src/main.zig +++ b/src/main.zig @@ -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| {