From f4f056f991a9147ab3e5036833ce4d8d06fda96f Mon Sep 17 00:00:00 2001 From: Zhongheng Liu Date: Tue, 28 Apr 2026 17:38:57 +0200 Subject: [PATCH 1/4] Custom command parsing from config file and handling --- src/Bar.zig | 18 +++++++++++++----- src/config/BarConfig.zig | 10 ++++++++++ src/utils.zig | 27 +++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 5 deletions(-) diff --git a/src/Bar.zig b/src/Bar.zig index 02bfc48..d5c4b40 100644 --- a/src/Bar.zig +++ b/src/Bar.zig @@ -44,7 +44,7 @@ const PendingRender = struct { }; pub const Position = enum { top, bottom }; -pub const Component = enum { title, clock, wm_info, none }; +pub const Component = enum { title, clock, wm_info, custom, none }; pub const Options = struct { /// Comma separated list of FontConfig formatted font specifications @@ -67,6 +67,8 @@ pub const Options = struct { /// strftime format string for the clock display time_format: []const u8 = default_time_format, + /// Custom command to execute for the custom display + custom_command: []const u8 = "uname -r", /// Which component to show on the left side of the bar left: Component = .title, /// Which component to show in the center of the bar @@ -272,25 +274,31 @@ fn draw(bar: *Bar) !void { const wm_info_codepoints = try utils.utf8ToCodepoints(wm_info_writer.buffered()); defer utils.gpa.free(wm_info_codepoints); + // Processing custom component + const argv = [_][]const u8{ "sh", "-c", options.custom_command}; + const custom_str = try utils.execCommand(utils.gpa, &argv); + const custom_codepoints = try utils.utf8ToCodepoints(custom_str); + defer utils.gpa.free(custom_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 { + fn f(component: Component, clock: []u32, title: []u32, wm_info: []u32, custom: []u32) []u32 { return switch (component) { .clock => clock, .title => title, .wm_info => wm_info, + .custom => custom, .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_codepoints = componentSlice(options.center, clock_codepoints, title_codepoints, wm_info_codepoints, custom_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); + const left_codepoints = componentSlice(options.left, clock_codepoints, title_codepoints, wm_info_codepoints, custom_codepoints); if (left_codepoints.len > 0) { const max_width = center_x - 2 * options.horizontal_padding; const truncated = try bar.truncateToWidth(left_codepoints, max_width); @@ -299,7 +307,7 @@ fn draw(bar: *Bar) !void { } // Render right slot - const right_codepoints = componentSlice(options.right, clock_codepoints, title_codepoints, wm_info_codepoints); + const right_codepoints = componentSlice(options.right, clock_codepoints, title_codepoints, wm_info_codepoints, custom_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); diff --git a/src/config/BarConfig.zig b/src/config/BarConfig.zig index ea7179c..890fd67 100644 --- a/src/config/BarConfig.zig +++ b/src/config/BarConfig.zig @@ -16,6 +16,7 @@ const NodeName = enum { horizontal_padding, margins, time_format, + custom_command, }; const MarginsNodeName = enum { top, right, bottom, left }; @@ -52,6 +53,7 @@ horizontal_padding: u8 = 5, /// strftime format string for the clock display. /// null means use the default. time_format: ?[]const u8 = null, +custom_command: ?[]const u8 = null, pub fn toBarOptions(config: BarConfig) Bar.Options { return .{ @@ -68,6 +70,7 @@ pub fn toBarOptions(config: BarConfig) Bar.Options { .bottom = config.margin_bottom, .left = config.margin_left, }, + .custom_command = config.custom_command orelse "uname -r", .vertical_padding = config.vertical_padding, .horizontal_padding = config.horizontal_padding, .time_format = config.time_format orelse Bar.default_time_format, @@ -123,6 +126,13 @@ pub fn load(config: *Config, parser: *kdl.Parser, hostname: ?[]const u8) !void { logWarnInvalidNodeArg(name, val_str); } }, + .custom_command => { + if (node.argcount() < 1) { + logWarnMissingNodeArg(name, "custom command"); + continue; + } + config.bar_config.?.custom_command = utils.gpa.dupe(u8, val_str) catch @panic("Out of memory"); + }, .margins => next_child_block = .margins, inline .background_color, .text_color, diff --git a/src/utils.zig b/src/utils.zig index 0718ed2..b3611e5 100644 --- a/src/utils.zig +++ b/src/utils.zig @@ -227,6 +227,33 @@ pub fn versionNotSupported(comptime WaylandGlobal: type, have_version: u32, need fatal("The compositor only advertised {s} version {d} but version {d} is required. Exiting", .{ WaylandGlobal.interface.name, have_version, need_version }); } +pub fn execCommand(allocator: std.mem.Allocator, argv: []const []const u8) ![]u8 { + // .run() spawns the process, collects stdout/stderr, and waits for completion. + // We set a max_output_size (e.g., 50KB) to prevent runaway memory usage. + const result = try std.process.Child.run(.{ + .allocator = allocator, + .argv = argv, + .max_output_bytes = 50 * 1024, + }); + + // We don't need stderr for this plain function, so free it immediately. + allocator.free(result.stderr); + + // If the command failed (non-zero exit), you might want to handle that. + switch (result.term) { + .Exited => |code| if (code != 0) { + allocator.free(result.stdout); + return error.CommandFailed; + }, + else => { + allocator.free(result.stdout); + return error.CommandTerminatedAbnormally; + }, + } + + return result.stdout; +} + const std = @import("std"); const fatal = std.process.fatal; const fmt = std.fmt; From 417f85ebd7f08ce37711e6e847f031b78f28ba7e Mon Sep 17 00:00:00 2001 From: Zhongheng Liu Date: Tue, 28 Apr 2026 17:54:43 +0200 Subject: [PATCH 2/4] add fork note --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index a737d1f..315b453 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # beansprout +> Forked version of [beansprout/beansprout](https://codeberg.org/beansprout/beansprout.git). ![Single window with gaps](docs/screenshots/single-window.jpeg) ![Tiled windows](docs/screenshots/tiled-windows.png) From 602406648870fb2da6bac0a63e034f935ca1f8f2 Mon Sep 17 00:00:00 2001 From: Ben Buhse Date: Tue, 28 Apr 2026 10:50:00 -0500 Subject: [PATCH 3/4] Support layer shell exclusive areas again This re-adds support for layer-shell exclusive areas (initially removed in commit a9473204) The Beansprout bar will now render inside the non-exclusive area and the usable area for calculating the window layouts is based on the non- exclusive area minus the beansprout bar's area Implements: #13 --- src/Bar.zig | 13 ++++++++---- src/Output.zig | 56 +++++++++++++++++++++++++++++++++++++------------- 2 files changed, 51 insertions(+), 18 deletions(-) diff --git a/src/Bar.zig b/src/Bar.zig index 02bfc48..99d012b 100644 --- a/src/Bar.zig +++ b/src/Bar.zig @@ -35,6 +35,8 @@ pending_manage: PendingManage = .{}, pending_render: PendingRender = .{}, const PendingManage = struct { + /// Recalculate bar geometry (size and position) on the next manage cycle + /// Set when output dimensions, position, scale, or exclusive zones change output_geometry: bool = false, }; @@ -159,7 +161,10 @@ pub fn manage(bar: *Bar) !void { const logical_font_height = @divFloor(bar.fcft_fonts.height, @as(i32, bar.font_scale)); 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)); + // Use the non-exclusive area so the bar sits adjacent to any external layer shell + // surfaces rather than overlapping them. + const base = output.non_exclusive_area; + const width: u31 = @as(u31, @intCast(base.width)) -| @as(u31, @intCast(options.margins.left + options.margins.right)); if (bar.geometry.width != width or bar.geometry.height != height) { bar.geometry.width = width; @@ -171,10 +176,10 @@ pub fn manage(bar: *Bar) !void { bar.surfaces.wl_surface.setOpaqueRegion(opaque_region); } - const x = output.geometry.x + options.margins.left; + const x = base.x + options.margins.left; const y = switch (options.position) { - .top => output.geometry.y + options.margins.top, - .bottom => output.geometry.y + output.geometry.height - bar.geometry.height - options.margins.bottom, + .top => base.y + options.margins.top, + .bottom => base.y + base.height - bar.geometry.height - options.margins.bottom, }; bar.pending_render.position = .{ .x = x, .y = y }; bar.pending_render.draw = true; diff --git a/src/Output.zig b/src/Output.zig index 36ba31d..64f1ba3 100644 --- a/src/Output.zig +++ b/src/Output.zig @@ -15,15 +15,15 @@ wl_output: ?*wl.Output = null, /// Friendly name of this output name: ?[]const u8 = null, -/// Output geometry scale: u31 = 1, +/// The rect for the entire output, this includes space that's taken by widgets like bars and not +/// made available to windows. geometry: Rect = .{}, -/// Area available for window layout (output geometry minus bar space) -/// Maybe I'll re-add support for layer shell exclusive areas later, -/// but adding that makes it more work for me and I don't personally -/// know of anything that makes me want them since external bars won't -/// work with beansprout. +/// Output geometry minus layer shell exclusive zones +non_exclusive_area: Rect = .{}, + +/// Area available for window layout (non_exclusive_area minus bar space) usable_geometry: Rect = .{}, wallpaper: ?Wallpaper = null, @@ -64,6 +64,7 @@ const TagLayoutOverride = struct { const PendingManage = struct { position: ?struct { x: i32, y: i32 } = null, dimensions: ?struct { width: u31, height: u31 } = null, + non_exclusive_area: ?Rect = null, tags: ?u32 = null, primary_ratio: ?f32 = null, @@ -106,6 +107,7 @@ pub fn create(context: *Context, river_output_v1: *river.OutputV1) !*Output { output.windows.init(); output.river_output_v1.setListener(*Output, riverOutputListener, output); + output.river_layer_shell_output_v1.setListener(*Output, riverLayerShellOutputListener, output); return output; } @@ -200,8 +202,28 @@ pub fn prevWindow(output: *Output, current: *Window) ?*Window { } } -// Used for the river_output_v1 interface -fn riverOutputListener(river_output_v1: *river.OutputV1, event: river.OutputV1.Event, output: *Output) void { +fn riverLayerShellOutputListener( + _: *river.LayerShellOutputV1, + event: river.LayerShellOutputV1.Event, + output: *Output, +) void { + switch (event) { + .non_exclusive_area => |area| { + output.pending_manage.non_exclusive_area = .{ + .x = area.x, + .y = area.y, + .width = @intCast(area.width), + .height = @intCast(area.height), + }; + }, + } +} + +fn riverOutputListener( + river_output_v1: *river.OutputV1, + event: river.OutputV1.Event, + output: *Output, +) void { assert(output.river_output_v1 == river_output_v1); switch (event) { .removed => { @@ -286,7 +308,14 @@ pub fn manage(output: *Output) void { output.geometry.height = dimensions.height; } - if (output.pending_manage.position != null or output.pending_manage.dimensions != null) { + if (output.pending_manage.non_exclusive_area) |non_exclusive_area| { + output.non_exclusive_area = non_exclusive_area; + } + + if (output.pending_manage.position != null or + output.pending_manage.dimensions != null or + output.pending_manage.non_exclusive_area != null) + { if (output.wallpaper) |*wallpaper| { wallpaper.pending_render.draw = true; } @@ -391,11 +420,10 @@ pub fn manage(output: *Output) void { }; } - // Compute usable geometry from output geometry minus bar space. - // We don't use non_exclusive_area from layer shell since we don't support - // other layer shell clients with exclusive zones (layer shell clients that - // don't use exclusive areas are fine). - output.usable_geometry = output.geometry; + // Compute usable geometry starting from the non-exclusive area reported by the layer shell, + // then additionally subtract space for beansprout's own bar (which uses river shell, not layer + // shell, so it's not included in the exclusive zone calculation). + output.usable_geometry = output.non_exclusive_area; if (output.bar) |bar| { if (bar.geometry.height > 0) { const bar_height: i32 = bar.geometry.height; From b35962606dc1f67a9037b96d366325df7e285d75 Mon Sep 17 00:00:00 2001 From: Zhongheng Liu Date: Tue, 28 Apr 2026 20:40:12 +0200 Subject: [PATCH 4/4] impl autoexec support for a single script file --- src/Config.zig | 11 +++++++++++ src/main.zig | 7 ++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/Config.zig b/src/Config.zig index b89bfe3..2a3c023 100644 --- a/src/Config.zig +++ b/src/Config.zig @@ -16,6 +16,9 @@ border_color_focused: RiverColor = utils.parseRgbaComptime("0x89b4fa"), /// Color of unfocused windows' borders in 0xRRGGBBAA or 0xRRGGBB form border_color_unfocused: RiverColor = utils.parseRgbaComptime("0x1e1e2e"), +/// Autoexec script path +autoexec_path: ?[]const u8 = null, + /// Number of windows in the primary stack /// This is a global default, but each tagmask can have its own value primary_count: u8 = 1, @@ -70,6 +73,7 @@ pub const PrimarySide = enum { left, right }; pub const AttachMode = enum { top, bottom }; const NodeName = enum { + autoexec, attach_mode, primary_count, primary_ratio, @@ -225,6 +229,13 @@ fn load(config: *Config, reader: *Io.Reader) !void { } // Next, we have to check the specifics for the NodeName switch (name) { + .autoexec => { + const autoexec_path = node.arg(&parser, 0); + if (autoexec_path) |path| { + config.autoexec_path = utils.gpa.dupe(u8, path) catch @panic("Out of memory"); + } + logDebugSettingNode(name, autoexec_path.?); + }, .primary_count => { const count_str = utils.stripQuotes(node.arg(&parser, 0) orelse ""); // Use @max to ensure a minimum of 1 diff --git a/src/main.zig b/src/main.zig index 6da367a..db012ab 100644 --- a/src/main.zig +++ b/src/main.zig @@ -84,7 +84,12 @@ pub fn main() !void { .config = config, }); defer context.destroy(); - + if (context.config.autoexec_path) |path| { + std.log.debug("Path: {s}", .{path}); + const argv = [_][]const u8{"/bin/sh", "-c", path }; + var child = std.process.Child.init(&argv, utils.gpa); + try child.spawn(); + } try run(wl_display, context); }