mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-08-25 08:31:43 +00:00
apprt: own desktop notification truncation
Make the fixed-size desktop notification payload a named Message type and initialize it through a constructor. Keeping UTF-8 boundary truncation with the payload owns the buffer capacities and sentinel termination at the message boundary, while stream_handler only forwards the title and body.
This commit is contained in:
@@ -16,6 +16,47 @@ pub const Message = union(enum) {
|
||||
/// we want this union to be.
|
||||
pub const WriteReq = MessageData(u8, 255);
|
||||
|
||||
/// A fixed-size desktop notification payload sent to the app thread.
|
||||
pub const DesktopNotification = struct {
|
||||
/// Desktop notification title.
|
||||
title: [63:0]u8,
|
||||
|
||||
/// Desktop notification body.
|
||||
body: [255:0]u8,
|
||||
|
||||
pub fn init(title: []const u8, body: []const u8) DesktopNotification {
|
||||
var result: DesktopNotification = undefined;
|
||||
copyUtf8Z(result.title.len, &result.title, title);
|
||||
copyUtf8Z(result.body.len, &result.body, body);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// UTF-8 continuation bytes occupy the range 0x80 through 0xBF.
|
||||
fn isUtf8ContinuationByte(byte: u8) bool {
|
||||
return switch (byte) {
|
||||
0x80...0xBF => true,
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
|
||||
/// Copy as much of `src` as fits, backing up from a UTF-8 continuation
|
||||
/// byte so valid input is never truncated in the middle of a codepoint.
|
||||
fn copyUtf8Z(
|
||||
comptime capacity: usize,
|
||||
dst: *[capacity:0]u8,
|
||||
src: []const u8,
|
||||
) void {
|
||||
var len = @min(src.len, capacity);
|
||||
while (len > 0 and
|
||||
len < src.len and
|
||||
isUtf8ContinuationByte(src[len])) : (len -= 1)
|
||||
{}
|
||||
|
||||
@memcpy(dst[0..len], src[0..len]);
|
||||
dst[len] = 0;
|
||||
}
|
||||
};
|
||||
|
||||
/// Set the title of the surface.
|
||||
/// TODO: we should change this to a "WriteReq" style structure in
|
||||
/// the termio message so that we can more efficiently send strings
|
||||
@@ -52,13 +93,7 @@ pub const Message = union(enum) {
|
||||
child_exited: ChildExited,
|
||||
|
||||
/// Show a desktop notification.
|
||||
desktop_notification: struct {
|
||||
/// Desktop notification title.
|
||||
title: [63:0]u8,
|
||||
|
||||
/// Desktop notification body.
|
||||
body: [255:0]u8,
|
||||
},
|
||||
desktop_notification: DesktopNotification,
|
||||
|
||||
/// Health status change for the renderer.
|
||||
renderer_health: renderer.Health,
|
||||
@@ -195,3 +230,78 @@ pub fn newConfig(
|
||||
|
||||
return copy;
|
||||
}
|
||||
|
||||
test "DesktopNotification init" {
|
||||
const notification = Message.DesktopNotification.init("Title", "Body");
|
||||
|
||||
try std.testing.expectEqualStrings("Title", std.mem.sliceTo(¬ification.title, 0));
|
||||
try std.testing.expectEqualStrings("Body", std.mem.sliceTo(¬ification.body, 0));
|
||||
}
|
||||
|
||||
test "copyUtf8Z handles len at the final byte of every UTF-8 sequence length" {
|
||||
const DesktopNotification = Message.DesktopNotification;
|
||||
|
||||
var dst_1_byte: [1:0]u8 = undefined;
|
||||
const src_ending_in_1_byte_codepoint = "ab";
|
||||
try std.testing.expect(!DesktopNotification.isUtf8ContinuationByte(
|
||||
src_ending_in_1_byte_codepoint[dst_1_byte.len],
|
||||
));
|
||||
DesktopNotification.copyUtf8Z(
|
||||
dst_1_byte.len,
|
||||
&dst_1_byte,
|
||||
src_ending_in_1_byte_codepoint,
|
||||
);
|
||||
try std.testing.expectEqualStrings("a", std.mem.sliceTo(&dst_1_byte, 0));
|
||||
|
||||
var dst_2_bytes: [2:0]u8 = undefined;
|
||||
const src_ending_in_2_byte_codepoint = "aЯ";
|
||||
try std.testing.expect(DesktopNotification.isUtf8ContinuationByte(
|
||||
src_ending_in_2_byte_codepoint[dst_2_bytes.len],
|
||||
));
|
||||
DesktopNotification.copyUtf8Z(
|
||||
dst_2_bytes.len,
|
||||
&dst_2_bytes,
|
||||
src_ending_in_2_byte_codepoint,
|
||||
);
|
||||
try std.testing.expectEqualStrings("a", std.mem.sliceTo(&dst_2_bytes, 0));
|
||||
|
||||
var dst_3_bytes: [3:0]u8 = undefined;
|
||||
const src_ending_in_3_byte_codepoint = "a€";
|
||||
try std.testing.expect(DesktopNotification.isUtf8ContinuationByte(
|
||||
src_ending_in_3_byte_codepoint[dst_3_bytes.len],
|
||||
));
|
||||
DesktopNotification.copyUtf8Z(
|
||||
dst_3_bytes.len,
|
||||
&dst_3_bytes,
|
||||
src_ending_in_3_byte_codepoint,
|
||||
);
|
||||
try std.testing.expectEqualStrings("a", std.mem.sliceTo(&dst_3_bytes, 0));
|
||||
|
||||
var dst_4_bytes: [4:0]u8 = undefined;
|
||||
const src_ending_in_4_byte_codepoint = "a😀";
|
||||
try std.testing.expect(DesktopNotification.isUtf8ContinuationByte(
|
||||
src_ending_in_4_byte_codepoint[dst_4_bytes.len],
|
||||
));
|
||||
DesktopNotification.copyUtf8Z(
|
||||
dst_4_bytes.len,
|
||||
&dst_4_bytes,
|
||||
src_ending_in_4_byte_codepoint,
|
||||
);
|
||||
try std.testing.expectEqualStrings("a", std.mem.sliceTo(&dst_4_bytes, 0));
|
||||
}
|
||||
|
||||
test "copyUtf8Z keeps a complete codepoint at the truncation boundary" {
|
||||
var dst: [5:0]u8 = undefined;
|
||||
|
||||
Message.DesktopNotification.copyUtf8Z(dst.len, &dst, "abcЯz");
|
||||
|
||||
try std.testing.expectEqualStrings("abcЯ", std.mem.sliceTo(&dst, 0));
|
||||
}
|
||||
|
||||
test "copyUtf8Z preserves UTF-8 that fits" {
|
||||
var dst: [5:0]u8 = undefined;
|
||||
|
||||
Message.DesktopNotification.copyUtf8Z(dst.len, &dst, "abcЯ");
|
||||
|
||||
try std.testing.expectEqualStrings("abcЯ", std.mem.sliceTo(&dst, 0));
|
||||
}
|
||||
|
||||
@@ -16,27 +16,6 @@ const posix = std.posix;
|
||||
|
||||
const log = std.log.scoped(.io_handler);
|
||||
|
||||
/// UTF-8 continuation bytes occupy the range 0x80 through 0xBF.
|
||||
fn isUtf8ContinuationByte(byte: u8) bool {
|
||||
return switch (byte) {
|
||||
0x80...0xBF => true,
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
|
||||
/// Copy as much of `src` as fits, backing up from a UTF-8 continuation byte
|
||||
/// so valid input is never truncated in the middle of a codepoint.
|
||||
fn copyUtf8Z(comptime capacity: usize, dst: *[capacity:0]u8, src: []const u8) void {
|
||||
var len = @min(src.len, capacity);
|
||||
while (len > 0 and
|
||||
len < src.len and
|
||||
isUtf8ContinuationByte(src[len])) : (len -= 1)
|
||||
{}
|
||||
|
||||
@memcpy(dst[0..len], src[0..len]);
|
||||
dst[len] = 0;
|
||||
}
|
||||
|
||||
/// This is used as the handler for the terminal.Stream type. This is
|
||||
/// stateful and is expected to live for the entire lifetime of the terminal.
|
||||
/// It is NOT VALID to stop a stream handler, create a new one, and use that
|
||||
@@ -1367,20 +1346,9 @@ pub const StreamHandler = struct {
|
||||
title: []const u8,
|
||||
body: []const u8,
|
||||
) !void {
|
||||
var message = apprt.surface.Message{ .desktop_notification = undefined };
|
||||
|
||||
copyUtf8Z(
|
||||
message.desktop_notification.title.len,
|
||||
&message.desktop_notification.title,
|
||||
title,
|
||||
);
|
||||
copyUtf8Z(
|
||||
message.desktop_notification.body.len,
|
||||
&message.desktop_notification.body,
|
||||
body,
|
||||
);
|
||||
|
||||
self.surfaceMessageWriter(message);
|
||||
self.surfaceMessageWriter(.{
|
||||
.desktop_notification = .init(title, body),
|
||||
});
|
||||
}
|
||||
|
||||
/// Send a report to the pty.
|
||||
@@ -1490,53 +1458,3 @@ pub const StreamHandler = struct {
|
||||
self.surfaceMessageWriter(.{ .progress_report = report });
|
||||
}
|
||||
};
|
||||
|
||||
test "copyUtf8Z handles len at the final byte of every UTF-8 sequence length" {
|
||||
var dst_1_byte: [1:0]u8 = undefined;
|
||||
const src_ending_in_1_byte_codepoint = "ab";
|
||||
try std.testing.expect(!isUtf8ContinuationByte(
|
||||
src_ending_in_1_byte_codepoint[dst_1_byte.len],
|
||||
));
|
||||
copyUtf8Z(dst_1_byte.len, &dst_1_byte, src_ending_in_1_byte_codepoint);
|
||||
try std.testing.expectEqualStrings("a", std.mem.sliceTo(&dst_1_byte, 0));
|
||||
|
||||
var dst_2_bytes: [2:0]u8 = undefined;
|
||||
const src_ending_in_2_byte_codepoint = "aЯ";
|
||||
try std.testing.expect(isUtf8ContinuationByte(
|
||||
src_ending_in_2_byte_codepoint[dst_2_bytes.len],
|
||||
));
|
||||
copyUtf8Z(dst_2_bytes.len, &dst_2_bytes, src_ending_in_2_byte_codepoint);
|
||||
try std.testing.expectEqualStrings("a", std.mem.sliceTo(&dst_2_bytes, 0));
|
||||
|
||||
var dst_3_bytes: [3:0]u8 = undefined;
|
||||
const src_ending_in_3_byte_codepoint = "a€";
|
||||
try std.testing.expect(isUtf8ContinuationByte(
|
||||
src_ending_in_3_byte_codepoint[dst_3_bytes.len],
|
||||
));
|
||||
copyUtf8Z(dst_3_bytes.len, &dst_3_bytes, src_ending_in_3_byte_codepoint);
|
||||
try std.testing.expectEqualStrings("a", std.mem.sliceTo(&dst_3_bytes, 0));
|
||||
|
||||
var dst_4_bytes: [4:0]u8 = undefined;
|
||||
const src_ending_in_4_byte_codepoint = "a😀";
|
||||
try std.testing.expect(isUtf8ContinuationByte(
|
||||
src_ending_in_4_byte_codepoint[dst_4_bytes.len],
|
||||
));
|
||||
copyUtf8Z(dst_4_bytes.len, &dst_4_bytes, src_ending_in_4_byte_codepoint);
|
||||
try std.testing.expectEqualStrings("a", std.mem.sliceTo(&dst_4_bytes, 0));
|
||||
}
|
||||
|
||||
test "copyUtf8Z keeps a complete codepoint at the truncation boundary" {
|
||||
var dst: [5:0]u8 = undefined;
|
||||
|
||||
copyUtf8Z(dst.len, &dst, "abcЯz");
|
||||
|
||||
try std.testing.expectEqualStrings("abcЯ", std.mem.sliceTo(&dst, 0));
|
||||
}
|
||||
|
||||
test "copyUtf8Z preserves UTF-8 that fits" {
|
||||
var dst: [5:0]u8 = undefined;
|
||||
|
||||
copyUtf8Z(dst.len, &dst, "abcЯ");
|
||||
|
||||
try std.testing.expectEqualStrings("abcЯ", std.mem.sliceTo(&dst, 0));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user