Merge pull request 'Add window title and wm info to Bar' (#2) from new-bar into main

Reviewed-on: https://codeberg.org/beansprout/beansprout/pulls/2
This commit is contained in:
Ben Buhse 2026-02-27 19:09:05 +01:00
commit f7dba63ed5
13 changed files with 414 additions and 126 deletions

View file

@ -118,46 +118,4 @@ pub fn build(b: *std.Build) !void {
}
const version = manifest.version;
/// Needed until https://github.com/ziglang/zig/issues/22775
/// is addressed.
const manifest: struct {
name: @Type(.enum_literal),
version: []const u8,
fingerprint: u64,
minimum_zig_version: []const u8,
dependencies: struct {
wayland: struct {
url: []const u8,
hash: []const u8,
},
xkbcommon: struct {
url: []const u8,
hash: []const u8,
},
kdl: struct {
url: []const u8,
hash: []const u8,
},
known_folders: struct {
url: []const u8,
hash: []const u8,
},
pixman: struct {
url: []const u8,
hash: []const u8,
},
zigimg: struct {
url: []const u8,
hash: []const u8,
},
fcft: struct {
url: []const u8,
hash: []const u8,
},
zeit: struct {
url: []const u8,
hash: []const u8,
},
},
paths: []const []const u8,
} = @import("build.zig.zon");
const manifest = @import("build.zig.zon");

View file

@ -126,10 +126,10 @@ do not re-trigger rules.
## Bar
The bar is an optional widget that shows the time on your screen. Right now, that's it.
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:
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:
```kdl
bar {
@ -144,6 +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 |
### Margins

View file

@ -2,16 +2,14 @@
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)
- [ ] Add focused window title to bar
- [ ] Support overriding config location
- [ ] Add support for center-primary layout
- [ ] Add bar padding to config
- [ ] 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)
@ -49,3 +47,7 @@ These are in rough order of my priority, though no promises I do them in this or
- [x] Move orphan handling out of .output and .seat events; into manage()
- [x] Add config for single-window width ratio (mostly because my ultrawide makes a single window massive)
- [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?)
- [x] Support configuring bar item positions (left/center/right)

View file

@ -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

View file

@ -114,8 +114,9 @@ initialization do not re-trigger rules.
# BAR
The bar is an optional widget that shows the time. 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:
```
bar {
@ -135,9 +136,30 @@ 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)
*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. (Default: "%Y-%m-%d %H:%M, %A")
The bar also supports *margins* and *anchors* child blocks; see *TAG OVERLAY*
for their format.
An empty block can be used to enable the widget with all defaults.
# TAG OVERLAY
The tag overlay is an optional widget that briefly shows tag state when

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.
@ -43,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
@ -55,7 +59,22 @@ pub const Options = struct {
/// Whether the bar is at the top or bottom of the screen
position: Position = .top,
/// Directional margins top, right, bottom, left, in pixels
margins: struct { top: i32 = 0, right: i32 = 0, bottom: i32 = 0, left: i32 = 0 } = .{},
margins: struct { top: u8 = 0, right: u8 = 0, bottom: u8 = 0, left: u8 = 0 } = .{},
/// Vertical padding between bar edges and content, in pixels
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,
/// 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 {
@ -98,12 +117,14 @@ pub fn init(context: *Context, output: *Output, options: Options) !Bar {
.node = node,
},
.configured = true,
.pending_manage = .{ .output_geometry = true },
};
}
pub fn deinit(bar: *Bar) void {
bar.configured = false;
bar.timezone.deinit();
bar.fcft_fonts.destroy();
if (bar.surfaces) |surfaces| {
surfaces.node.destroy();
surfaces.river_shell_surface.destroy();
@ -112,6 +133,16 @@ pub fn deinit(bar: *Bar) void {
}
}
/// Update bar options in-place without destroying/recreating Wayland surfaces
/// This is used when reloading the config
pub fn reconfigure(bar: *Bar, options: Options) !void {
const new_fonts = try getFcftFonts(options.fonts, bar.font_scale);
bar.fcft_fonts.destroy();
bar.fcft_fonts = new_fonts;
bar.options = options;
bar.pending_manage.output_geometry = true;
}
pub fn manage(bar: *Bar) !void {
if (!bar.configured) return;
defer bar.pending_manage = .{};
@ -131,10 +162,9 @@ pub fn manage(bar: *Bar) !void {
bar.font_scale = scale;
}
const vertical_padding = 5;
const logical_font_height = @divFloor(bar.fcft_fonts.height, @as(i32, bar.font_scale));
const height: u31 = @intCast(logical_font_height + 2 * vertical_padding);
const width: u31 = output.geometry.width;
const height: u31 = @intCast(logical_font_height + 2 * options.vertical_padding);
const width: u31 = output.geometry.width -| @as(u31, @intCast(options.margins.left + options.margins.right));
if (bar.geometry.width != width or bar.geometry.height != height) {
bar.geometry.width = width;
@ -206,46 +236,90 @@ pub fn draw(bar: *Bar) !void {
);
// Set-up text color
const text_color = options.text_color;
const color = pixman.Image.createSolidFill(&text_color) orelse return error.FailedToCreatePixmanImage;
defer _ = color.unref();
const text_color = pixman.Image.createSolidFill(&options.text_color) orelse return error.FailedToCreatePixmanImage;
defer _ = text_color.unref();
// 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);
// Generate date/time info for this instant
const dt = now_local.time();
// Convert time to a string
var buf: [255:0]u8 = undefined;
var fbs = io.fixedBufferStream(&buf);
try dt.strftime(fbs.writer(), "%H:%M");
// Convert ASCII text string to unicode
// XXX: Not sure if this even needs to be converted to unicode
var codepoint_it = (try unicode.Utf8View.init(fbs.getWritten())).iterator();
const codepoint_count = try unicode.utf8CountCodepoints(fbs.getWritten());
// We use u32 for fcft even if zig uses u21
var codepoints: []u32 = try utils.gpa.alloc(u32, codepoint_count);
defer utils.gpa.free(codepoints);
var i: usize = 0;
while (codepoint_it.nextCodepoint()) |cp| : (i += 1) {
codepoints[i] = cp;
}
const text_width = try bar.textWidth(codepoints);
var x: i32 = @divFloor(buffer.width - text_width, 2);
// Y is shared between all components
const y: i32 = @divFloor(buffer.height - bar.fcft_fonts.height, 2);
// Actually render the unicode codepoints
try bar.renderChars(codepoints, buffer, &x, y, color);
// Pre-compute codepoints for each component type
// Finally, attach the buffer to the surface
// Clock
var time_buf: [255:0]u8 = undefined;
var time_writer = Io.Writer.fixed(&time_buf);
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);
// 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);
// WM info
const output = bar.output;
var wm_info_buf: [255:0]u8 = undefined;
var wm_info_writer = Io.Writer.fixed(&wm_info_buf);
const ratio_percent: u32 = @intFromFloat(@round(output.primary_ratio * 100));
try wm_info_writer.print("P:{d}/{d}% W:{d}({d})", .{
output.primary_count,
ratio_percent,
output.countVisible(),
output.windows.length(),
});
const wm_info_codepoints = try utils.utf8ToCodepoints(wm_info_writer.buffered());
defer utils.gpa.free(wm_info_codepoints);
// 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;
// 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);
// 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);
}
// 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
surfaces.river_shell_surface.syncNextCommit();
wl_surface.setBufferScale(scale);
wl_surface.attach(buffer.wl_buffer, 0, 0);
@ -253,7 +327,6 @@ pub fn draw(bar: *Bar) !void {
wl_surface.commit();
}
// TODO: This should be moved to utils once fonts are in config
/// Computes the pixel width of a text string.
fn textWidth(bar: *Bar, text: []const u32) !i32 {
var width: i32 = 0;
@ -270,6 +343,25 @@ fn textWidth(bar: *Bar, text: []const u32) !i32 {
return width;
}
/// Return the longest prefix of `text` that fits within `max_width` pixels.
fn truncateToWidth(bar: *Bar, text: []const u32, max_width: i32) ![]const u32 {
var width: i32 = 0;
for (text, 0..) |cp, i| {
const glyph = try bar.fcft_fonts.rasterizeCharUtf32(cp, .default);
if (i > 0) {
var x_kern: c_long = 0;
if (bar.fcft_fonts.kerning(text[i - 1], cp, &x_kern, null)) {
width += @intCast(x_kern);
}
}
if (width + glyph.advance.x > max_width) {
return text[0..i];
}
width += glyph.advance.x;
}
return text;
}
// Borrowed and modified from https://git.sr.ht/~novakane/zig-fcft-example
fn renderChars(
bar: *Bar,
@ -373,7 +465,7 @@ fn getFcftFonts(fonts: []const u8, scale: u31) !*fcft.Font {
log.debug("Scaling font DPI: base={d} scale={d}", .{ base_dpi, scale });
const scaled = try arena_alloc.dupeZ(
u8,
try std.fmt.allocPrint(arena_alloc, "{s}:dpi={}", .{ font, @as(u32, base_dpi) * scale }),
try fmt.allocPrint(arena_alloc, "{s}:dpi={}", .{ font, @as(u32, base_dpi) * scale }),
);
try list.append(arena_alloc, scaled);
} else {
@ -390,10 +482,10 @@ fn getFcftFonts(fonts: []const u8, scale: u31) !*fcft.Font {
const std = @import("std");
const assert = std.debug.assert;
const io = std.io;
const fmt = std.fmt;
const mem = std.mem;
const process = std.process;
const unicode = std.unicode;
const Io = std.Io;
const wayland = @import("wayland");
const wl = wayland.client.wl;

View file

@ -200,23 +200,33 @@ pub fn manage(context: *Context) void {
}
}
// Recreate or destroy bars on all outputs
// Update, create, or destroy bars on all outputs
const has_bar = new_config.bar_config != null;
if (had_bar or has_bar) {
var out_it = context.wm.outputs.iterator(.forward);
while (out_it.next()) |output| {
// Destroy existing bar
if (new_config.bar_config) |bar_config| {
if (output.bar) |*bar| {
// Existing bar; reconfigure in-place to keep surfaces
bar.reconfigure(bar_config.toBarOptions()) catch |e| {
log.err("Failed to reconfigure bar: {}", .{e});
bar.deinit();
output.bar = null;
}
// Create new bar if configured
if (new_config.bar_config) |bar_config| {
continue;
};
} else {
// No bar; we need to initialize a new one
output.bar = Bar.init(context, output, bar_config.toBarOptions()) catch |e| {
log.err("Failed to create bar: {}", .{e});
continue;
};
output.bar.?.pending_manage.output_geometry = true;
}
} else {
// New config doesn't have a bar; destroy existing one
if (output.bar) |*bar| {
bar.deinit();
output.bar = null;
}
}
}
}

View file

@ -523,10 +523,16 @@ pub fn manage(output: *Output) void {
Config.min_primary_ratio,
Config.max_primary_ratio,
);
if (output.bar) |*bar| {
bar.pending_render.draw = true;
}
}
if (output.pending_manage.primary_count) |primary_count| {
// Don't allow less than 1 primary
output.primary_count = @max(1, primary_count);
if (output.bar) |*bar| {
bar.pending_render.draw = true;
}
}
if (output.pending_manage.single_window_ratio) |single_window_ratio| {
output.single_window_ratio = std.math.clamp(
@ -554,6 +560,33 @@ pub fn manage(output: *Output) void {
output.tags = new_tags;
// If the focused window is no longer visible on the new tags, update focus.
if (output.context.wm.seats.first()) |seat| {
if (seat.focused_output == output) {
// Whether focus has changed, either to a new window or to no focus
const should_update_focus = if (seat.focused_window) |w|
w.tags & new_tags == 0
else
true;
if (should_update_focus) {
var new_focus: ?*Window = null;
var it = output.windows.iterator(.forward);
while (it.next()) |window| {
if (window.tags & new_tags != 0) {
new_focus = window;
break;
}
}
if (new_focus) |w| {
seat.pending_manage.window = .{ .window = w };
seat.pending_manage.should_warp_pointer = true;
} else {
seat.pending_manage.window = .clear_focus;
}
}
}
}
// Show tag overlay and arm the hide timer
if (output.tag_overlay) |*tag_overlay| {
if (tag_overlay.surfaces) |_| {
@ -580,6 +613,10 @@ pub fn manage(output: *Output) void {
};
}
}
if (output.bar) |*bar| {
bar.pending_render.draw = true;
}
}
if (output.bar) |*bar| {
@ -794,6 +831,15 @@ pub fn occupiedTags(output: *Output) u32 {
return occupied_tags;
}
pub fn countVisible(output: *Output) usize {
var visible: usize = 0;
var it = output.windows.iterator(.forward);
while (it.next()) |window| {
if (window.tags & output.tags != 0) visible += 1;
}
return visible;
}
const std = @import("std");
const assert = std.debug.assert;
const mem = std.mem;

View file

@ -137,9 +137,15 @@ pub fn manage(seat: *Seat) void {
switch (pending_window) {
.window => |window| {
if (seat.focused_window) |focused| {
// Tell the previously focused Window that it's no longer focused
if (focused != window) {
// Tell the previously focused Window that it's no longer focused
focused.pending_render.focused = false;
// Update the Bar to have the newly-focused window's title
if (focused.output) |output| {
if (output.bar) |*bar| {
bar.pending_render.draw = true;
}
}
}
}
seat.focused_window = window;

View file

@ -97,6 +97,9 @@ pub fn create(context: *Context, river_window_v1: *river.WindowV1, output: ?*Out
.output = output,
.tags = if (output) |o| o.tags else 0x0001,
.link = undefined, // Handled by the wl.list
// Ensure borders are applied on the first render cycle, even for windows that
// are never explicitly told they are unfocused (e.g. on WM restart).
.pending_render = .{ .focused = false },
};
window.river_window_v1.setListener(*Window, windowListener, window);
@ -145,7 +148,7 @@ fn windowListener(river_window_v1: *river.WindowV1, event: river.WindowV1.Event,
while (it.next()) |seat| {
if (seat.focused_window == window) {
// Find another window to focus and warp pointer there
if (output.prevWindow(window)) |next_focus| {
if (output.nextWindow(window)) |next_focus| {
if (next_focus != window) {
seat.pending_manage.window = .{ .window = next_focus };
seat.pending_manage.should_warp_pointer = true;
@ -188,6 +191,19 @@ fn windowListener(river_window_v1: *river.WindowV1, event: river.WindowV1.Event,
utils.gpa.dupe(u8, std.mem.span(t)) catch @panic("Out of memory")
else
null;
// Need to update the bar if this window is focused
if (window.context.wm.seats.first()) |seat| {
if (seat.focused_window) |focused_window| {
if (focused_window == window) {
if (window.output) |output| {
if (output.bar) |*bar| {
bar.pending_render.draw = true;
}
}
}
}
}
},
.parent => |ev| {
// Nothing to do if ev.parent is null

View file

@ -9,7 +9,13 @@ const NodeName = enum {
text_color,
background_color,
position,
left,
center,
right,
vertical_padding,
horizontal_padding,
margins,
time_format,
};
const MarginsNodeName = enum { top, right, bottom, left };
@ -18,11 +24,34 @@ const MarginsNodeName = enum { top, right, bottom, left };
fonts: ?[]const u8 = null,
text_color: pixman.Color = utils.parseRgbaPixmanComptime("0xcdd6f4"),
background_color: pixman.Color = utils.parseRgbaPixmanComptime("0x1e1e2e"),
/// Whether the bar is at the top or bottom of the screen
position: Bar.Position = .top,
margin_top: i32 = 0,
margin_right: i32 = 0,
margin_bottom: i32 = 0,
margin_left: i32 = 0,
/// 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)
margin_right: u8 = 0,
/// Margin above bottom top of the bar and another element (a window or the top of the output)
margin_bottom: u8 = 0,
/// Margin above left top of the bar and another element (a window or the top of the output)
margin_left: u8 = 0,
/// Vertical padding between bar edges and content, in pixels
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 .{
@ -30,12 +59,18 @@ 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,
.bottom = config.margin_bottom,
.left = config.margin_left,
},
.vertical_padding = config.vertical_padding,
.horizontal_padding = config.horizontal_padding,
.time_format = config.time_format orelse Bar.default_time_format,
};
}
@ -76,6 +111,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,
@ -86,6 +133,25 @@ pub fn load(config: *Config, parser: *kdl.Parser, hostname: ?[]const u8) !void {
};
logDebugSettingNode(name, val_str);
},
inline .vertical_padding,
.horizontal_padding,
=> |tag| {
const padding_str = utils.stripQuotes(node.arg(parser, 0) orelse "");
const padding = fmt.parseInt(u8, padding_str, 10) catch {
logWarnInvalidNodeArg(name, padding_str);
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 {
helpers.logWarnInvalidNode(node.name);
@ -117,7 +183,7 @@ fn loadMarginsBlock(config: *Config, parser: *kdl.Parser, hostname: ?[]const u8)
continue;
}
const val_str = utils.stripQuotes(node.arg(parser, 0) orelse "");
const val = fmt.parseInt(i32, val_str, 10) catch {
const val = fmt.parseInt(u8, val_str, 10) catch {
logWarnInvalidNodeArg(name, val_str);
continue;
};
@ -165,15 +231,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| {

View file

@ -193,6 +193,21 @@ pub fn stripQuotes(s: []const u8) []const u8 {
return s;
}
/// Convert a Utf-8 string into codepoints
/// Caller owns the returned slice and is responsible for freeing it.
pub fn utf8ToCodepoints(utf8: []const u8) ![]u32 {
var codepoint_it = (try unicode.Utf8View.init(utf8)).iterator();
const codepoint_count = try unicode.utf8CountCodepoints(utf8);
// We use u32 for fcft even if zig uses u21
const codepoints: []u32 = try gpa.alloc(u32, codepoint_count);
var i: usize = 0;
while (codepoint_it.nextCodepoint()) |cp| : (i += 1) {
codepoints[i] = cp;
}
return codepoints;
}
/// Report that the given WaylandGlobal wasn't advertised and exit the program
pub fn interfaceNotAdvertised(comptime WaylandGlobal: type) noreturn {
fatal("{s} not advertised. Exiting", .{WaylandGlobal.interface.name});
@ -207,6 +222,7 @@ const std = @import("std");
const fatal = std.process.fatal;
const fmt = std.fmt;
const mem = std.mem;
const unicode = std.unicode;
const wayland = @import("wayland");
const river = wayland.client.river;
@ -447,3 +463,41 @@ test "tokenizeShell quotes mid-token" {
try testing.expectEqual(1, result.len);
try testing.expectEqualStrings("foobar bazqux", result[0]);
}
test "utf8ToCodepoints ASCII" {
const codepoints = try utf8ToCodepoints("hello");
defer gpa.free(codepoints);
try testing.expectEqual(5, codepoints.len);
try testing.expectEqual('h', codepoints[0]);
try testing.expectEqual('e', codepoints[1]);
try testing.expectEqual('l', codepoints[2]);
try testing.expectEqual('l', codepoints[3]);
try testing.expectEqual('o', codepoints[4]);
}
test "utf8ToCodepoints multi-byte" {
const codepoints = try utf8ToCodepoints("grüezi");
defer gpa.free(codepoints);
try testing.expectEqual(6, codepoints.len);
try testing.expectEqual('g', codepoints[0]);
try testing.expectEqual('r', codepoints[1]);
try testing.expectEqual(0x00FC, codepoints[2]); // ü
try testing.expectEqual('e', codepoints[3]);
try testing.expectEqual('z', codepoints[4]);
try testing.expectEqual('i', codepoints[5]);
}
test "utf8ToCodepoints empty" {
const codepoints = try utf8ToCodepoints("");
defer gpa.free(codepoints);
try testing.expectEqual(0, codepoints.len);
}
test "utf8ToCodepoints emoji" {
// 🇨🇦 is two regional indicator symbols: U+1F1E8 U+1F1E6
const codepoints = try utf8ToCodepoints("🇨🇦");
defer gpa.free(codepoints);
try testing.expectEqual(2, codepoints.len);
try testing.expectEqual(0x1F1E8, codepoints[0]);
try testing.expectEqual(0x1F1E6, codepoints[1]);
}