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:
commit
f7dba63ed5
13 changed files with 414 additions and 126 deletions
44
build.zig
44
build.zig
|
|
@ -118,46 +118,4 @@ pub fn build(b: *std.Build) !void {
|
||||||
}
|
}
|
||||||
|
|
||||||
const version = manifest.version;
|
const version = manifest.version;
|
||||||
/// Needed until https://github.com/ziglang/zig/issues/22775
|
const manifest = @import("build.zig.zon");
|
||||||
/// 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");
|
|
||||||
|
|
|
||||||
|
|
@ -126,10 +126,10 @@ do not re-trigger rules.
|
||||||
|
|
||||||
## Bar
|
## Bar
|
||||||
|
|
||||||
The bar is an optional widget that shows the time on your screen. Right now, that's it.
|
The bar is an optional widget that displays configurable components in three
|
||||||
It is only created when a `bar` block is present in the config. All settings have
|
slots: left, center, and right. It is only created when a `bar` block is present in
|
||||||
defaults, with the color based on the Catppuccin Mocha theme. An empty block can be used
|
the config. All settings have defaults, with the color based on the Catppuccin
|
||||||
to enable the widget with all defaults:
|
Mocha theme. An empty block can be used to enable the widget with all defaults:
|
||||||
|
|
||||||
```kdl
|
```kdl
|
||||||
bar {
|
bar {
|
||||||
|
|
@ -144,6 +144,12 @@ bar {
|
||||||
| `text_color` | color | `0xcdd6f4` | Text color |
|
| `text_color` | color | `0xcdd6f4` | Text color |
|
||||||
| `background_color` | color | `0x1e1e2e` | Background color |
|
| `background_color` | color | `0x1e1e2e` | Background color |
|
||||||
| `position` | enum | `top` | Bar position (`top` or `bottom`) |
|
| `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
|
### Margins
|
||||||
|
|
||||||
|
|
|
||||||
10
docs/TODO.md
10
docs/TODO.md
|
|
@ -2,16 +2,14 @@
|
||||||
|
|
||||||
These are in rough order of my priority, though no promises I do them in this order.
|
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)
|
- [ ] Add build-time options for including the wallpaper (and maybe bar)
|
||||||
- [ ] Check pointer position and only warp if not on focused window already
|
- [ ] Check pointer position and only warp if not on focused window already
|
||||||
- [ ] Change focus direction when closing window
|
- [ ] Change focus direction when closing window
|
||||||
- [ ] Use set_xcursor_theme request
|
- [ ] Use set_xcursor_theme request
|
||||||
- [ ] Support configuring bar item positions (left/center/right)
|
|
||||||
- [ ] Add focused window title to bar
|
|
||||||
- [ ] Support overriding config location
|
- [ ] Support overriding config location
|
||||||
- [ ] Add support for center-primary layout
|
- [ ] 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 per-output bar visibility
|
||||||
- [ ] Support more window rule options (e.g. ssd/csd)
|
- [ ] Support more window rule options (e.g. ssd/csd)
|
||||||
- [ ] Support solid `background-color` fallback (no wallpaper)
|
- [ ] 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] 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] 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] 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)
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ borders {
|
||||||
// Bar widget; shows the time
|
// Bar widget; shows the time
|
||||||
bar {
|
bar {
|
||||||
position top
|
position top
|
||||||
|
time_format "%H:%M"
|
||||||
}
|
}
|
||||||
// Tag overlay widget; shown briefly when switching tags
|
// Tag overlay widget; shown briefly when switching tags
|
||||||
// Remove this block to disable the overlay entirely
|
// Remove this block to disable the overlay entirely
|
||||||
|
|
|
||||||
|
|
@ -114,8 +114,9 @@ initialization do not re-trigger rules.
|
||||||
|
|
||||||
# BAR
|
# BAR
|
||||||
|
|
||||||
The bar is an optional widget that shows the time. It is only created when a
|
The bar is an optional widget that displays configurable components in three
|
||||||
*bar* block is present:
|
slots: left, center, and right. It is only created when a *bar* block is present
|
||||||
|
in the config:
|
||||||
|
|
||||||
```
|
```
|
||||||
bar {
|
bar {
|
||||||
|
|
@ -135,9 +136,30 @@ bar {
|
||||||
*position* *top*|*bottom*
|
*position* *top*|*bottom*
|
||||||
Bar position. (Default: *top*)
|
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*
|
The bar also supports *margins* and *anchors* child blocks; see *TAG OVERLAY*
|
||||||
for their format.
|
for their format.
|
||||||
|
|
||||||
|
An empty block can be used to enable the widget with all defaults.
|
||||||
|
|
||||||
# TAG OVERLAY
|
# TAG OVERLAY
|
||||||
|
|
||||||
The tag overlay is an optional widget that briefly shows tag state when
|
The tag overlay is an optional widget that briefly shows tag state when
|
||||||
|
|
|
||||||
176
src/Bar.zig
176
src/Bar.zig
|
|
@ -7,6 +7,9 @@ const Bar = @This();
|
||||||
/// Standard base DPI at a scale of 1
|
/// Standard base DPI at a scale of 1
|
||||||
const base_dpi = 96;
|
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,
|
context: *Context,
|
||||||
|
|
||||||
/// The timezone of the computer.
|
/// The timezone of the computer.
|
||||||
|
|
@ -43,6 +46,7 @@ pub const PendingRender = struct {
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const Position = enum { top, bottom };
|
pub const Position = enum { top, bottom };
|
||||||
|
pub const Component = enum { title, clock, wm_info, none };
|
||||||
|
|
||||||
pub const Options = struct {
|
pub const Options = struct {
|
||||||
/// Comma separated list of FontConfig formatted font specifications
|
/// 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
|
/// Whether the bar is at the top or bottom of the screen
|
||||||
position: Position = .top,
|
position: Position = .top,
|
||||||
/// Directional margins top, right, bottom, left, in pixels
|
/// 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 {
|
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,
|
.node = node,
|
||||||
},
|
},
|
||||||
.configured = true,
|
.configured = true,
|
||||||
|
.pending_manage = .{ .output_geometry = true },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(bar: *Bar) void {
|
pub fn deinit(bar: *Bar) void {
|
||||||
bar.configured = false;
|
bar.configured = false;
|
||||||
bar.timezone.deinit();
|
bar.timezone.deinit();
|
||||||
|
bar.fcft_fonts.destroy();
|
||||||
if (bar.surfaces) |surfaces| {
|
if (bar.surfaces) |surfaces| {
|
||||||
surfaces.node.destroy();
|
surfaces.node.destroy();
|
||||||
surfaces.river_shell_surface.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 {
|
pub fn manage(bar: *Bar) !void {
|
||||||
if (!bar.configured) return;
|
if (!bar.configured) return;
|
||||||
defer bar.pending_manage = .{};
|
defer bar.pending_manage = .{};
|
||||||
|
|
@ -131,10 +162,9 @@ pub fn manage(bar: *Bar) !void {
|
||||||
bar.font_scale = scale;
|
bar.font_scale = scale;
|
||||||
}
|
}
|
||||||
|
|
||||||
const vertical_padding = 5;
|
|
||||||
const logical_font_height = @divFloor(bar.fcft_fonts.height, @as(i32, bar.font_scale));
|
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 height: u31 = @intCast(logical_font_height + 2 * options.vertical_padding);
|
||||||
const width: u31 = output.geometry.width;
|
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) {
|
if (bar.geometry.width != width or bar.geometry.height != height) {
|
||||||
bar.geometry.width = width;
|
bar.geometry.width = width;
|
||||||
|
|
@ -206,46 +236,90 @@ pub fn draw(bar: *Bar) !void {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Set-up text color
|
// Set-up text color
|
||||||
const text_color = options.text_color;
|
const text_color = pixman.Image.createSolidFill(&options.text_color) orelse return error.FailedToCreatePixmanImage;
|
||||||
const color = pixman.Image.createSolidFill(&text_color) orelse return error.FailedToCreatePixmanImage;
|
defer _ = text_color.unref();
|
||||||
defer _ = color.unref();
|
|
||||||
|
|
||||||
// Get the current time in seconds since the epoch,
|
// Y is shared between all components
|
||||||
// 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);
|
|
||||||
const y: i32 = @divFloor(buffer.height - bar.fcft_fonts.height, 2);
|
const y: i32 = @divFloor(buffer.height - bar.fcft_fonts.height, 2);
|
||||||
|
|
||||||
// Actually render the unicode codepoints
|
// Pre-compute codepoints for each component type
|
||||||
try bar.renderChars(codepoints, buffer, &x, y, color);
|
|
||||||
|
|
||||||
// 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, ¢er_x, y, text_color);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attach the buffer to the surface
|
||||||
const surfaces = bar.surfaces orelse return error.NoSurfaces;
|
const surfaces = bar.surfaces orelse return error.NoSurfaces;
|
||||||
const wl_surface = surfaces.wl_surface;
|
const wl_surface = surfaces.wl_surface;
|
||||||
|
// sync_next_commit ensures frame-perfect application
|
||||||
surfaces.river_shell_surface.syncNextCommit();
|
surfaces.river_shell_surface.syncNextCommit();
|
||||||
wl_surface.setBufferScale(scale);
|
wl_surface.setBufferScale(scale);
|
||||||
wl_surface.attach(buffer.wl_buffer, 0, 0);
|
wl_surface.attach(buffer.wl_buffer, 0, 0);
|
||||||
|
|
@ -253,7 +327,6 @@ pub fn draw(bar: *Bar) !void {
|
||||||
wl_surface.commit();
|
wl_surface.commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: This should be moved to utils once fonts are in config
|
|
||||||
/// Computes the pixel width of a text string.
|
/// Computes the pixel width of a text string.
|
||||||
fn textWidth(bar: *Bar, text: []const u32) !i32 {
|
fn textWidth(bar: *Bar, text: []const u32) !i32 {
|
||||||
var width: i32 = 0;
|
var width: i32 = 0;
|
||||||
|
|
@ -270,6 +343,25 @@ fn textWidth(bar: *Bar, text: []const u32) !i32 {
|
||||||
return width;
|
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
|
// Borrowed and modified from https://git.sr.ht/~novakane/zig-fcft-example
|
||||||
fn renderChars(
|
fn renderChars(
|
||||||
bar: *Bar,
|
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 });
|
log.debug("Scaling font DPI: base={d} scale={d}", .{ base_dpi, scale });
|
||||||
const scaled = try arena_alloc.dupeZ(
|
const scaled = try arena_alloc.dupeZ(
|
||||||
u8,
|
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);
|
try list.append(arena_alloc, scaled);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -390,10 +482,10 @@ fn getFcftFonts(fonts: []const u8, scale: u31) !*fcft.Font {
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const assert = std.debug.assert;
|
const assert = std.debug.assert;
|
||||||
const io = std.io;
|
const fmt = std.fmt;
|
||||||
const mem = std.mem;
|
const mem = std.mem;
|
||||||
const process = std.process;
|
const process = std.process;
|
||||||
const unicode = std.unicode;
|
const Io = std.Io;
|
||||||
|
|
||||||
const wayland = @import("wayland");
|
const wayland = @import("wayland");
|
||||||
const wl = wayland.client.wl;
|
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;
|
const has_bar = new_config.bar_config != null;
|
||||||
if (had_bar or has_bar) {
|
if (had_bar or has_bar) {
|
||||||
var out_it = context.wm.outputs.iterator(.forward);
|
var out_it = context.wm.outputs.iterator(.forward);
|
||||||
while (out_it.next()) |output| {
|
while (out_it.next()) |output| {
|
||||||
// Destroy existing bar
|
if (new_config.bar_config) |bar_config| {
|
||||||
if (output.bar) |*bar| {
|
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();
|
bar.deinit();
|
||||||
output.bar = null;
|
output.bar = null;
|
||||||
}
|
continue;
|
||||||
// Create new bar if configured
|
};
|
||||||
if (new_config.bar_config) |bar_config| {
|
} else {
|
||||||
|
// No bar; we need to initialize a new one
|
||||||
output.bar = Bar.init(context, output, bar_config.toBarOptions()) catch |e| {
|
output.bar = Bar.init(context, output, bar_config.toBarOptions()) catch |e| {
|
||||||
log.err("Failed to create bar: {}", .{e});
|
log.err("Failed to create bar: {}", .{e});
|
||||||
continue;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -523,10 +523,16 @@ pub fn manage(output: *Output) void {
|
||||||
Config.min_primary_ratio,
|
Config.min_primary_ratio,
|
||||||
Config.max_primary_ratio,
|
Config.max_primary_ratio,
|
||||||
);
|
);
|
||||||
|
if (output.bar) |*bar| {
|
||||||
|
bar.pending_render.draw = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (output.pending_manage.primary_count) |primary_count| {
|
if (output.pending_manage.primary_count) |primary_count| {
|
||||||
// Don't allow less than 1 primary
|
// Don't allow less than 1 primary
|
||||||
output.primary_count = @max(1, primary_count);
|
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| {
|
if (output.pending_manage.single_window_ratio) |single_window_ratio| {
|
||||||
output.single_window_ratio = std.math.clamp(
|
output.single_window_ratio = std.math.clamp(
|
||||||
|
|
@ -554,6 +560,33 @@ pub fn manage(output: *Output) void {
|
||||||
|
|
||||||
output.tags = new_tags;
|
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
|
// Show tag overlay and arm the hide timer
|
||||||
if (output.tag_overlay) |*tag_overlay| {
|
if (output.tag_overlay) |*tag_overlay| {
|
||||||
if (tag_overlay.surfaces) |_| {
|
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| {
|
if (output.bar) |*bar| {
|
||||||
|
|
@ -794,6 +831,15 @@ pub fn occupiedTags(output: *Output) u32 {
|
||||||
return occupied_tags;
|
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 std = @import("std");
|
||||||
const assert = std.debug.assert;
|
const assert = std.debug.assert;
|
||||||
const mem = std.mem;
|
const mem = std.mem;
|
||||||
|
|
|
||||||
|
|
@ -137,9 +137,15 @@ pub fn manage(seat: *Seat) void {
|
||||||
switch (pending_window) {
|
switch (pending_window) {
|
||||||
.window => |window| {
|
.window => |window| {
|
||||||
if (seat.focused_window) |focused| {
|
if (seat.focused_window) |focused| {
|
||||||
// Tell the previously focused Window that it's no longer focused
|
|
||||||
if (focused != window) {
|
if (focused != window) {
|
||||||
|
// Tell the previously focused Window that it's no longer focused
|
||||||
focused.pending_render.focused = false;
|
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;
|
seat.focused_window = window;
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,9 @@ pub fn create(context: *Context, river_window_v1: *river.WindowV1, output: ?*Out
|
||||||
.output = output,
|
.output = output,
|
||||||
.tags = if (output) |o| o.tags else 0x0001,
|
.tags = if (output) |o| o.tags else 0x0001,
|
||||||
.link = undefined, // Handled by the wl.list
|
.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);
|
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| {
|
while (it.next()) |seat| {
|
||||||
if (seat.focused_window == window) {
|
if (seat.focused_window == window) {
|
||||||
// Find another window to focus and warp pointer there
|
// 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) {
|
if (next_focus != window) {
|
||||||
seat.pending_manage.window = .{ .window = next_focus };
|
seat.pending_manage.window = .{ .window = next_focus };
|
||||||
seat.pending_manage.should_warp_pointer = true;
|
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")
|
utils.gpa.dupe(u8, std.mem.span(t)) catch @panic("Out of memory")
|
||||||
else
|
else
|
||||||
null;
|
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| {
|
.parent => |ev| {
|
||||||
// Nothing to do if ev.parent is null
|
// Nothing to do if ev.parent is null
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,13 @@ const NodeName = enum {
|
||||||
text_color,
|
text_color,
|
||||||
background_color,
|
background_color,
|
||||||
position,
|
position,
|
||||||
|
left,
|
||||||
|
center,
|
||||||
|
right,
|
||||||
|
vertical_padding,
|
||||||
|
horizontal_padding,
|
||||||
margins,
|
margins,
|
||||||
|
time_format,
|
||||||
};
|
};
|
||||||
const MarginsNodeName = enum { top, right, bottom, left };
|
const MarginsNodeName = enum { top, right, bottom, left };
|
||||||
|
|
||||||
|
|
@ -18,11 +24,34 @@ const MarginsNodeName = enum { top, right, bottom, left };
|
||||||
fonts: ?[]const u8 = null,
|
fonts: ?[]const u8 = null,
|
||||||
text_color: pixman.Color = utils.parseRgbaPixmanComptime("0xcdd6f4"),
|
text_color: pixman.Color = utils.parseRgbaPixmanComptime("0xcdd6f4"),
|
||||||
background_color: pixman.Color = utils.parseRgbaPixmanComptime("0x1e1e2e"),
|
background_color: pixman.Color = utils.parseRgbaPixmanComptime("0x1e1e2e"),
|
||||||
|
|
||||||
|
/// Whether the bar is at the top or bottom of the screen
|
||||||
position: Bar.Position = .top,
|
position: Bar.Position = .top,
|
||||||
margin_top: i32 = 0,
|
|
||||||
margin_right: i32 = 0,
|
/// Which component to show on the left side of the bar
|
||||||
margin_bottom: i32 = 0,
|
left: Bar.Component = .title,
|
||||||
margin_left: i32 = 0,
|
/// 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 {
|
pub fn toBarOptions(config: BarConfig) Bar.Options {
|
||||||
return .{
|
return .{
|
||||||
|
|
@ -30,12 +59,18 @@ pub fn toBarOptions(config: BarConfig) Bar.Options {
|
||||||
.text_color = config.text_color,
|
.text_color = config.text_color,
|
||||||
.background_color = config.background_color,
|
.background_color = config.background_color,
|
||||||
.position = config.position,
|
.position = config.position,
|
||||||
|
.left = config.left,
|
||||||
|
.center = config.center,
|
||||||
|
.right = config.right,
|
||||||
.margins = .{
|
.margins = .{
|
||||||
.top = config.margin_top,
|
.top = config.margin_top,
|
||||||
.right = config.margin_right,
|
.right = config.margin_right,
|
||||||
.bottom = config.margin_bottom,
|
.bottom = config.margin_bottom,
|
||||||
.left = config.margin_left,
|
.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);
|
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,
|
.margins => next_child_block = .margins,
|
||||||
inline .background_color,
|
inline .background_color,
|
||||||
.text_color,
|
.text_color,
|
||||||
|
|
@ -86,6 +133,25 @@ pub fn load(config: *Config, parser: *kdl.Parser, hostname: ?[]const u8) !void {
|
||||||
};
|
};
|
||||||
logDebugSettingNode(name, val_str);
|
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 {
|
} else {
|
||||||
helpers.logWarnInvalidNode(node.name);
|
helpers.logWarnInvalidNode(node.name);
|
||||||
|
|
@ -117,7 +183,7 @@ fn loadMarginsBlock(config: *Config, parser: *kdl.Parser, hostname: ?[]const u8)
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const val_str = utils.stripQuotes(node.arg(parser, 0) orelse "");
|
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);
|
logWarnInvalidNodeArg(name, val_str);
|
||||||
continue;
|
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 {
|
inline fn logWarnMissingNodeArg(node_name: NodeName, comptime arg: []const u8) void {
|
||||||
log.warn("\"bar.{s}\" missing " ++ arg ++ " argument. Ignoring", .{@tagName(node_name)});
|
log.warn("\"bar.{s}\" missing " ++ arg ++ " argument. Ignoring", .{@tagName(node_name)});
|
||||||
}
|
}
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const fmt = std.fmt;
|
const fmt = std.fmt;
|
||||||
|
const Io = std.Io;
|
||||||
|
|
||||||
const kdl = @import("kdl");
|
const kdl = @import("kdl");
|
||||||
const pixman = @import("pixman");
|
const pixman = @import("pixman");
|
||||||
|
const zeit = @import("zeit");
|
||||||
|
|
||||||
const utils = @import("../utils.zig");
|
const utils = @import("../utils.zig");
|
||||||
const Bar = @import("../Bar.zig");
|
const Bar = @import("../Bar.zig");
|
||||||
|
|
|
||||||
14
src/main.zig
14
src/main.zig
|
|
@ -129,19 +129,17 @@ fn run(wl_display: *wl.Display, context: *Context) !void {
|
||||||
fatal("wl_display flush failed: E{s}", .{@tagName(errno)});
|
fatal("wl_display flush failed: E{s}", .{@tagName(errno)});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the number of milliseconds to the top of the next minute
|
// Get the number of milliseconds to the top of the next second
|
||||||
const time = std.time.timestamp();
|
const time_ns = std.time.nanoTimestamp();
|
||||||
if (time < 0) {
|
const ns_per_sec = std.time.ns_per_s;
|
||||||
log.err("Got a negative time ({d})", .{time});
|
const remainder_ns = @mod(time_ns, ns_per_sec);
|
||||||
return error.InvalidTime;
|
const timeout: i32 = @intCast(@divFloor(ns_per_sec - remainder_ns, std.time.ns_per_ms));
|
||||||
}
|
|
||||||
const timeout: i32 = @intCast((@divFloor(time, 60) * 60 + 60 - time) * 1000);
|
|
||||||
|
|
||||||
const poll_rc = posix.poll(&pollfds, timeout) catch |err| {
|
const poll_rc = posix.poll(&pollfds, timeout) catch |err| {
|
||||||
fatal("Failed to poll {s}", .{@errorName(err)});
|
fatal("Failed to poll {s}", .{@errorName(err)});
|
||||||
};
|
};
|
||||||
if (poll_rc == 0) {
|
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.
|
// and need to update the clock.
|
||||||
var it = context.wm.outputs.iterator(.forward);
|
var it = context.wm.outputs.iterator(.forward);
|
||||||
while (it.next()) |output| {
|
while (it.next()) |output| {
|
||||||
|
|
|
||||||
|
|
@ -193,6 +193,21 @@ pub fn stripQuotes(s: []const u8) []const u8 {
|
||||||
return s;
|
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
|
/// Report that the given WaylandGlobal wasn't advertised and exit the program
|
||||||
pub fn interfaceNotAdvertised(comptime WaylandGlobal: type) noreturn {
|
pub fn interfaceNotAdvertised(comptime WaylandGlobal: type) noreturn {
|
||||||
fatal("{s} not advertised. Exiting", .{WaylandGlobal.interface.name});
|
fatal("{s} not advertised. Exiting", .{WaylandGlobal.interface.name});
|
||||||
|
|
@ -207,6 +222,7 @@ const std = @import("std");
|
||||||
const fatal = std.process.fatal;
|
const fatal = std.process.fatal;
|
||||||
const fmt = std.fmt;
|
const fmt = std.fmt;
|
||||||
const mem = std.mem;
|
const mem = std.mem;
|
||||||
|
const unicode = std.unicode;
|
||||||
|
|
||||||
const wayland = @import("wayland");
|
const wayland = @import("wayland");
|
||||||
const river = wayland.client.river;
|
const river = wayland.client.river;
|
||||||
|
|
@ -447,3 +463,41 @@ test "tokenizeShell quotes mid-token" {
|
||||||
try testing.expectEqual(1, result.len);
|
try testing.expectEqual(1, result.len);
|
||||||
try testing.expectEqualStrings("foobar bazqux", result[0]);
|
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]);
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue