Add window title and wm info to Bar
This commit adds the focused window title to the left side of the bar and some WM info (primary count/ratio and # of visible/total windows) to the right side. It also adds new vertical_padding and horizontal_padding config options for the bar.
This commit is contained in:
parent
062748967c
commit
efd0222899
9 changed files with 231 additions and 51 deletions
133
src/Bar.zig
133
src/Bar.zig
|
|
@ -55,7 +55,12 @@ 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,
|
||||
};
|
||||
|
||||
pub fn init(context: *Context, output: *Output, options: Options) !Bar {
|
||||
|
|
@ -98,12 +103,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 +119,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 +148,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,9 +222,11 @@ 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();
|
||||
|
||||
// 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,
|
||||
|
|
@ -220,24 +238,78 @@ pub fn draw(bar: *Bar) !void {
|
|||
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");
|
||||
var time_buf: [255:0]u8 = undefined;
|
||||
var time_writer = Io.Writer.fixed(&time_buf);
|
||||
try dt.strftime(&time_writer, "%H:%M");
|
||||
|
||||
// Convert date string to Unicode codepoints
|
||||
const codepoints = try utils.utf8ToCodepoints(fbs.getWritten());
|
||||
defer utils.gpa.free(codepoints);
|
||||
const time_codepoints = try utils.utf8ToCodepoints(time_writer.buffered());
|
||||
defer utils.gpa.free(time_codepoints);
|
||||
|
||||
const text_width = try bar.textWidth(codepoints);
|
||||
var x: i32 = @divFloor(buffer.width - text_width, 2);
|
||||
const y: i32 = @divFloor(buffer.height - bar.fcft_fonts.height, 2);
|
||||
// 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);
|
||||
|
||||
// Actually render the unicode codepoints
|
||||
try bar.renderChars(codepoints, buffer, &x, y, color);
|
||||
// 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);
|
||||
|
||||
// Finally, attach the buffer to the surface
|
||||
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
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
var right_x: i32 = buffer.width - right_text_width - options.horizontal_padding;
|
||||
try bar.renderChars(
|
||||
right_truncated,
|
||||
buffer,
|
||||
&right_x,
|
||||
y,
|
||||
text_color,
|
||||
);
|
||||
|
||||
// Finally, put the time in the center of the bar
|
||||
try bar.renderChars(time_codepoints, buffer, ¢er_x, y, text_color);
|
||||
|
||||
// Really finally, 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);
|
||||
|
|
@ -245,7 +317,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;
|
||||
|
|
@ -262,6 +333,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,
|
||||
|
|
@ -365,7 +455,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 {
|
||||
|
|
@ -382,9 +472,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 Io = std.Io;
|
||||
|
||||
const wayland = @import("wayland");
|
||||
const wl = wayland.client.wl;
|
||||
|
|
|
|||
|
|
@ -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 (output.bar) |*bar| {
|
||||
bar.deinit();
|
||||
output.bar = null;
|
||||
}
|
||||
// Create new bar if configured
|
||||
if (new_config.bar_config) |bar_config| {
|
||||
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;
|
||||
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;
|
||||
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;
|
||||
};
|
||||
}
|
||||
} else {
|
||||
// New config doesn't have a bar; destroy existing one
|
||||
if (output.bar) |*bar| {
|
||||
bar.deinit();
|
||||
output.bar = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
@ -580,6 +586,10 @@ pub fn manage(output: *Output) void {
|
|||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (output.bar) |*bar| {
|
||||
bar.pending_render.draw = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (output.bar) |*bar| {
|
||||
|
|
@ -794,6 +804,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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -188,6 +188,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
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ const NodeName = enum {
|
|||
text_color,
|
||||
background_color,
|
||||
position,
|
||||
vertical_padding,
|
||||
horizontal_padding,
|
||||
margins,
|
||||
};
|
||||
const MarginsNodeName = enum { top, right, bottom, left };
|
||||
|
|
@ -18,11 +20,23 @@ 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,
|
||||
|
||||
/// 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,
|
||||
|
||||
pub fn toBarOptions(config: BarConfig) Bar.Options {
|
||||
return .{
|
||||
|
|
@ -36,6 +50,8 @@ pub fn toBarOptions(config: BarConfig) Bar.Options {
|
|||
.bottom = config.margin_bottom,
|
||||
.left = config.margin_left,
|
||||
},
|
||||
.vertical_padding = config.vertical_padding,
|
||||
.horizontal_padding = config.horizontal_padding,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -76,6 +92,7 @@ pub fn load(config: *Config, parser: *kdl.Parser, hostname: ?[]const u8) !void {
|
|||
logWarnInvalidNodeArg(name, val_str);
|
||||
}
|
||||
},
|
||||
|
||||
.margins => next_child_block = .margins,
|
||||
inline .background_color,
|
||||
.text_color,
|
||||
|
|
@ -86,6 +103,16 @@ 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;
|
||||
},
|
||||
}
|
||||
} else {
|
||||
helpers.logWarnInvalidNode(node.name);
|
||||
|
|
@ -117,7 +144,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;
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue