ghostty/src/termio/message.zig

209 lines
6.8 KiB
Zig

const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const renderer = @import("../renderer.zig");
const terminal = @import("../terminal/main.zig");
const termio = @import("../termio.zig");
/// The messages that can be sent to an IO thread.
///
/// This is not a tiny structure (~40 bytes at the time of writing this comment),
/// but the messages are IO thread sends are also very few. At the current size
/// we can queue 26,000 messages before consuming a MB of RAM.
pub const Message = union(enum) {
/// Represents a write request. Magic number comes from the largest
/// other union value. It can be upped if we add a larger union member
/// in the future.
pub const WriteReq = MessageData(u8, 38);
pub const Resize = struct {
/// The grid size for the given screen size with padding applied.
grid_size: renderer.GridSize,
/// The full screen (drawable) size. This does NOT include padding.
/// This should be sent on to the renderer.
screen_size: renderer.ScreenSize,
/// The padding, so that the terminal implementation can subtract
/// this to send to the pty.
padding: renderer.Padding,
};
/// The derived configuration to update the implementation with. This
/// is allocated via the allocator and is expected to be freed when done.
change_config: struct {
alloc: Allocator,
ptr: *termio.Impl.DerivedConfig,
},
/// Activate or deactivate the inspector.
inspector: bool,
/// Resize the window.
resize: Resize,
/// Clear the screen.
clear_screen: struct {
/// Include clearing the history
history: bool,
},
/// Scroll the viewport
scroll_viewport: terminal.Terminal.ScrollViewport,
/// Jump forward/backward n prompts.
jump_to_prompt: isize,
/// Send this when a synchronized output mode is started. This will
/// start the timer so that the output mode is disabled after a
/// period of time so that a bad actor can't hang the terminal.
start_synchronized_output: void,
/// Enable or disable linefeed mode (mode 20).
linefeed_mode: bool,
/// The child exited abnormally. The termio state is marked
/// as process exited but the surface hasn't been notified to
/// close because termio can use this to update the terminal
/// with an error message.
child_exited_abnormally: struct {
code: u32,
runtime_ms: u64,
},
/// Write where the data fits in the union.
write_small: WriteReq.Small,
/// Write where the data pointer is stable.
write_stable: WriteReq.Stable,
/// Write where the data is allocated and must be freed.
write_alloc: WriteReq.Alloc,
/// Return a write request for the given data. This will use
/// write_small if it fits or write_alloc otherwise. This should NOT
/// be used for stable pointers which can be manually set to write_stable.
pub fn writeReq(alloc: Allocator, data: anytype) !Message {
return switch (try WriteReq.init(alloc, data)) {
.stable => unreachable,
.small => |v| Message{ .write_small = v },
.alloc => |v| Message{ .write_alloc = v },
};
}
};
/// Creates a union that can be used to accommodate data that fit within an array,
/// are a stable pointer, or require deallocation. This is helpful for thread
/// messaging utilities.
pub fn MessageData(comptime Elem: type, comptime small_size: comptime_int) type {
return union(enum) {
pub const Self = @This();
pub const Small = struct {
pub const Max = small_size;
pub const Array = [Max]Elem;
pub const Len = std.math.IntFittingRange(0, small_size);
data: Array = undefined,
len: Len = 0,
};
pub const Alloc = struct {
alloc: Allocator,
data: []Elem,
};
pub const Stable = []const Elem;
/// A small write where the data fits into this union size.
small: Small,
/// A stable pointer so we can just pass the slice directly through.
/// This is useful i.e. for const data.
stable: Stable,
/// Allocated and must be freed with the provided allocator. This
/// should be rarely used.
alloc: Alloc,
/// Initializes the union for a given data type. This will
/// attempt to fit into a small value if possible, otherwise
/// will allocate and put into alloc.
///
/// This can't and will never detect stable pointers.
pub fn init(alloc: Allocator, data: anytype) !Self {
switch (@typeInfo(@TypeOf(data))) {
.Pointer => |info| {
assert(info.size == .Slice);
assert(info.child == Elem);
// If it fits in our small request, do that.
if (data.len <= Small.Max) {
var buf: Small.Array = undefined;
@memcpy(buf[0..data.len], data);
return Self{
.small = .{
.data = buf,
.len = @intCast(data.len),
},
};
}
// Otherwise, allocate
const buf = try alloc.dupe(Elem, data);
errdefer alloc.free(buf);
return Self{
.alloc = .{
.alloc = alloc,
.data = buf,
},
};
},
else => unreachable,
}
}
};
}
test {
std.testing.refAllDecls(@This());
}
test {
// Ensure we don't grow our IO message size without explicitly wanting to.
const testing = std.testing;
try testing.expectEqual(@as(usize, 40), @sizeOf(Message));
}
test "MessageData init small" {
const testing = std.testing;
const alloc = testing.allocator;
const Data = MessageData(u8, 10);
const input = "hello!";
const io = try Data.init(alloc, @as([]const u8, input));
try testing.expect(io == .small);
}
test "MessageData init alloc" {
const testing = std.testing;
const alloc = testing.allocator;
const Data = MessageData(u8, 10);
const input = "hello! " ** 100;
const io = try Data.init(alloc, @as([]const u8, input));
try testing.expect(io == .alloc);
io.alloc.alloc.free(io.alloc.data);
}
test "MessageData small fits non-u8 sized data" {
const testing = std.testing;
const alloc = testing.allocator;
const len = 500;
const Data = MessageData(u8, len);
const input: []const u8 = "X" ** len;
const io = try Data.init(alloc, input);
try testing.expect(io == .small);
}