diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index f7a24ff..177da68 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -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 diff --git a/docs/TODO.md b/docs/TODO.md index d5493b8..44cb883 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -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) diff --git a/man/beansprout.5.scd b/man/beansprout.5.scd index 7ac835c..020cafe 100644 --- a/man/beansprout.5.scd +++ b/man/beansprout.5.scd @@ -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. diff --git a/src/Bar.zig b/src/Bar.zig index 682edd2..b19bd3a 100644 --- a/src/Bar.zig +++ b/src/Bar.zig @@ -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, ¢er_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, ¢er_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 diff --git a/src/config/BarConfig.zig b/src/config/BarConfig.zig index 34ebf99..ea7179c 100644 --- a/src/config/BarConfig.zig +++ b/src/config/BarConfig.zig @@ -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 {