Compare commits

...
Sign in to create a new pull request.

2 commits

Author SHA1 Message Date
Zhongheng Liu
417f85ebd7 add fork note 2026-04-28 17:54:43 +02:00
Zhongheng Liu
f4f056f991 Custom command parsing from config file and handling 2026-04-28 17:38:57 +02:00
4 changed files with 51 additions and 5 deletions

View file

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

View file

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

View file

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

View file

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