mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-09-20 12:38:08 +00:00
ABI BREAKING: This removes `ghostty_terminal_mode_get` and `_mode_set`. We can now represent these operations completely with standard `ghostty_terminal_get` and `ghostty_terminal_set`, which makes it much more flexible to preserve ABI in the future. This is all centered around a new `GhosttyTerminalModeConfig` structure that is an in or out parameter depending on use case. This also adds a new `GHOSTTY_TERMINAL_OPT_MODE_DEFAULT` option that can be used to set the _default_ value of mode that happens when a RIS event (full reset) is sent.
4976 lines
163 KiB
Zig
4976 lines
163 KiB
Zig
const builtin = @import("builtin");
|
|
const std = @import("std");
|
|
const testing = std.testing;
|
|
const build_options = @import("terminal_options");
|
|
const lib = @import("../lib.zig");
|
|
const CAllocator = lib.alloc.Allocator;
|
|
pub const ZigTerminal = @import("../Terminal.zig");
|
|
const Action = @import("../stream.zig").Action;
|
|
const osc = @import("../osc.zig");
|
|
const Stream = @import("../stream_terminal.zig").Stream;
|
|
const Screen = @import("../Screen.zig");
|
|
const ScreenSet = @import("../ScreenSet.zig");
|
|
const PageList = @import("../PageList.zig");
|
|
const apc = @import("../apc.zig");
|
|
const kitty = @import("../kitty/key.zig");
|
|
const kitty_gfx_c = @import("kitty_graphics.zig");
|
|
const modes = @import("../modes.zig");
|
|
const point = @import("../point.zig");
|
|
const size = @import("../size.zig");
|
|
const device_attributes = @import("../device_attributes.zig");
|
|
const device_status = @import("../device_status.zig");
|
|
const size_report = @import("../size_report.zig");
|
|
const cell_c = @import("cell.zig");
|
|
const row_c = @import("row.zig");
|
|
const grid_ref_c = @import("grid_ref.zig");
|
|
const grid_ref_tracked_c = @import("grid_ref_tracked.zig");
|
|
const selection_c = @import("selection.zig");
|
|
const style_c = @import("style.zig");
|
|
const color = @import("../color.zig");
|
|
const clipboard = @import("../clipboard.zig");
|
|
const c_io = @import("io.zig");
|
|
const snapshot_core = @import("../snapshot/main.zig");
|
|
const Result = @import("result.zig").Result;
|
|
const assert = @import("../../quirks.zig").inlineAssert;
|
|
|
|
const Handler = @import("../stream_terminal.zig").Handler;
|
|
|
|
const max_path_bytes = if (builtin.os.tag == .freestanding) 4096 else std.fs.max_path_bytes;
|
|
|
|
const log = std.log.scoped(.terminal_c);
|
|
|
|
/// C terminals do not retain replay bytes unless the embedding application
|
|
/// opts in through `GHOSTTY_TERMINAL_OPT_CONTINUATION_MAX_BYTES`.
|
|
pub const default_continuation_max_bytes: usize = 0;
|
|
|
|
/// Owns the `std.Io` implementation retained by every C terminal.
|
|
///
|
|
/// Snapshot decoding creates this before the native terminal exists and
|
|
/// transfers it into the final C wrapper after READY.
|
|
pub const Io = struct {
|
|
impl: Impl,
|
|
|
|
/// Platform-specific storage backing the public `std.Io` value.
|
|
const Impl = if (builtin.os.tag != .freestanding)
|
|
*std.Io.Threaded
|
|
else
|
|
void;
|
|
|
|
/// Allocation failures possible while constructing an I/O owner.
|
|
pub const Error = error{OutOfMemory};
|
|
|
|
/// Allocate the native I/O implementation when the platform requires it.
|
|
pub fn init(alloc: std.mem.Allocator) Error!Io {
|
|
if (comptime builtin.os.tag == .freestanding) return .{ .impl = {} };
|
|
|
|
const ptr = alloc.create(std.Io.Threaded) catch
|
|
return error.OutOfMemory;
|
|
ptr.* = .init_single_threaded;
|
|
return .{ .impl = ptr };
|
|
}
|
|
|
|
/// Return the value passed to native terminal construction and decoding.
|
|
pub fn io(self: Io) std.Io {
|
|
if (comptime builtin.os.tag == .freestanding) {
|
|
return std.Io.failing;
|
|
}
|
|
return self.impl.io();
|
|
}
|
|
|
|
/// Release an I/O implementation that has not already been transferred.
|
|
pub fn deinit(self: Io, alloc: std.mem.Allocator) void {
|
|
if (comptime builtin.os.tag != .freestanding) {
|
|
self.impl.deinit();
|
|
alloc.destroy(self.impl);
|
|
}
|
|
}
|
|
};
|
|
|
|
/// Wrapper around ZigTerminal that tracks additional state for C API usage,
|
|
/// such as the persistent VT stream needed to handle escape sequences split
|
|
/// across multiple vt_write calls.
|
|
const TerminalWrapper = struct {
|
|
terminal: *ZigTerminal,
|
|
/// C construction has no I/O argument, so the wrapper retains the owner
|
|
/// created by `new` or transferred from snapshot decoding until `free`.
|
|
/// Freestanding owners contain no native allocation and expose failing I/O.
|
|
io: Io,
|
|
/// We also need to store a temp dir path for some operations (e.g., kitty
|
|
/// graphics). This provides stable storage for the API calls.
|
|
tmp_dir_path: [max_path_bytes]u8,
|
|
stream: Stream,
|
|
effects: Effects = .{},
|
|
tracked_grid_refs: std.AutoArrayHashMapUnmanaged(*grid_ref_tracked_c.TrackedGridRef, void) = .{},
|
|
|
|
/// Fetches a `TerminalWrapper` reference from a `Handler`.
|
|
fn fromHandler(handler: *Handler) *TerminalWrapper {
|
|
const stream_ptr: *Stream = @fieldParentPtr("handler", handler);
|
|
return @alignCast(@fieldParentPtr("stream", stream_ptr));
|
|
}
|
|
};
|
|
|
|
/// A single MIME representation in a clipboard write.
|
|
///
|
|
/// C: GhosttyClipboardContent
|
|
pub const ClipboardContent = extern struct {
|
|
mime: lib.String,
|
|
data: lib.String,
|
|
};
|
|
|
|
/// A protocol-neutral request to replace or clear clipboard contents.
|
|
///
|
|
/// C: GhosttyClipboardWrite
|
|
pub const ClipboardWrite = extern struct {
|
|
size: usize,
|
|
location: clipboard.Location,
|
|
contents: ?[*]const ClipboardContent,
|
|
contents_len: usize,
|
|
};
|
|
|
|
/// A request to show a desktop notification.
|
|
///
|
|
/// C: GhosttyTerminalDesktopNotification
|
|
pub const DesktopNotification = extern struct {
|
|
size: usize,
|
|
title: lib.String,
|
|
body: lib.String,
|
|
};
|
|
|
|
/// C: GhosttyTerminalProgressState
|
|
pub const ProgressState = osc.Command.ProgressReport.State;
|
|
|
|
/// A progress report emitted by the running program.
|
|
///
|
|
/// C: GhosttyTerminalProgressReport
|
|
pub const ProgressReport = extern struct {
|
|
size: usize,
|
|
state: ProgressState,
|
|
progress: i8,
|
|
};
|
|
|
|
/// A terminal mode and boolean value used for mode configuration.
|
|
///
|
|
/// C: GhosttyTerminalModeConfig
|
|
pub const ModeConfig = extern struct {
|
|
mode: modes.ModeTag.Backing,
|
|
value: bool,
|
|
|
|
fn toMode(self: ModeConfig) ?modes.Mode {
|
|
const tag: modes.ModeTag = @bitCast(self.mode);
|
|
return modes.modeFromInt(tag.value, tag.ansi);
|
|
}
|
|
};
|
|
|
|
/// C callback state for terminal effects. Trampolines are always
|
|
/// installed on the stream handler; they check these fields and
|
|
/// no-op when the corresponding callback is null.
|
|
const Effects = struct {
|
|
userdata: ?*anyopaque = null,
|
|
write_pty: ?WritePtyFn = null,
|
|
bell: ?BellFn = null,
|
|
color_scheme: ?ColorSchemeFn = null,
|
|
desktop_notification: ?DesktopNotificationFn = null,
|
|
device_attributes_cb: ?DeviceAttributesFn = null,
|
|
enquiry: ?EnquiryFn = null,
|
|
xtversion: ?XtversionFn = null,
|
|
title_changed: ?TitleChangedFn = null,
|
|
pwd_changed: ?PwdChangedFn = null,
|
|
progress_report: ?ProgressReportFn = null,
|
|
size_cb: ?SizeFn = null,
|
|
clipboard_write: ?ClipboardWriteFn = null,
|
|
|
|
/// Scratch buffer for DA1 feature codes. The device attributes
|
|
/// trampoline converts C feature codes into this buffer and returns
|
|
/// a slice pointing into it. Storing it here ensures the slice
|
|
/// remains valid after the trampoline returns, since the caller
|
|
/// (`reportDeviceAttributes`) reads it before any re-entrant call.
|
|
da_features_buf: [64]device_attributes.Primary.Feature = undefined,
|
|
|
|
/// C function pointer type for the write_pty callback.
|
|
pub const WritePtyFn = *const fn (Terminal, ?*anyopaque, [*]const u8, usize) callconv(lib.calling_conv) void;
|
|
|
|
/// C function pointer type for the bell callback.
|
|
pub const BellFn = *const fn (Terminal, ?*anyopaque) callconv(lib.calling_conv) void;
|
|
|
|
/// C function pointer type for the color_scheme callback.
|
|
/// Returns true and fills out_scheme if a color scheme is available,
|
|
/// or returns false to silently ignore the query.
|
|
pub const ColorSchemeFn = *const fn (Terminal, ?*anyopaque, *device_status.ColorScheme) callconv(lib.calling_conv) bool;
|
|
|
|
/// C function pointer type for the enquiry callback.
|
|
/// Returns the response bytes. The memory must remain valid
|
|
/// until the callback returns.
|
|
pub const EnquiryFn = *const fn (Terminal, ?*anyopaque) callconv(lib.calling_conv) lib.String;
|
|
|
|
/// C function pointer type for the xtversion callback.
|
|
/// Returns the version string (e.g. "ghostty 1.2.3"). The memory
|
|
/// must remain valid until the callback returns. An empty string
|
|
/// (len=0) causes the default "libghostty" to be reported.
|
|
pub const XtversionFn = *const fn (Terminal, ?*anyopaque) callconv(lib.calling_conv) lib.String;
|
|
|
|
/// C function pointer type for the clipboard_write callback. The request
|
|
/// and its contents are borrowed and only valid for the callback duration.
|
|
pub const ClipboardWriteFn = *const fn (Terminal, ?*anyopaque, *const ClipboardWrite) callconv(lib.calling_conv) clipboard.WriteResult;
|
|
|
|
/// C function pointer type for the desktop_notification callback. The
|
|
/// request and its strings are borrowed for the callback duration.
|
|
pub const DesktopNotificationFn = *const fn (Terminal, ?*anyopaque, *const DesktopNotification) callconv(lib.calling_conv) void;
|
|
|
|
/// C function pointer type for the title_changed callback.
|
|
pub const TitleChangedFn = *const fn (Terminal, ?*anyopaque) callconv(lib.calling_conv) void;
|
|
|
|
/// C function pointer type for the pwd_changed callback.
|
|
pub const PwdChangedFn = *const fn (Terminal, ?*anyopaque) callconv(lib.calling_conv) void;
|
|
|
|
/// C function pointer type for the progress_report callback.
|
|
pub const ProgressReportFn = *const fn (Terminal, ?*anyopaque, *const ProgressReport) callconv(lib.calling_conv) void;
|
|
|
|
/// C function pointer type for the size callback.
|
|
/// Returns true and fills out_size if size is available,
|
|
/// or returns false to silently ignore the query.
|
|
pub const SizeFn = *const fn (Terminal, ?*anyopaque, *size_report.Size) callconv(lib.calling_conv) bool;
|
|
|
|
/// C function pointer type for the device_attributes callback.
|
|
/// Returns true and fills out_attrs if attributes are available,
|
|
/// or returns false to silently ignore the query.
|
|
pub const DeviceAttributesFn = *const fn (Terminal, ?*anyopaque, *CDeviceAttributes) callconv(lib.calling_conv) bool;
|
|
|
|
/// C-compatible device attributes struct.
|
|
/// C: GhosttyDeviceAttributes
|
|
pub const CDeviceAttributes = extern struct {
|
|
primary: Primary,
|
|
secondary: Secondary,
|
|
tertiary: Tertiary,
|
|
|
|
pub const Primary = extern struct {
|
|
conformance_level: u16,
|
|
features: [64]u16,
|
|
num_features: usize,
|
|
};
|
|
|
|
pub const Secondary = extern struct {
|
|
device_type: u16,
|
|
firmware_version: u16,
|
|
rom_cartridge: u16,
|
|
};
|
|
|
|
pub const Tertiary = extern struct {
|
|
unit_id: u32,
|
|
};
|
|
};
|
|
|
|
fn writePtyTrampoline(handler: *Handler, data: [:0]const u8) void {
|
|
const wrapper = TerminalWrapper.fromHandler(handler);
|
|
const func = wrapper.effects.write_pty orelse return;
|
|
func(@ptrCast(wrapper), wrapper.effects.userdata, data.ptr, data.len);
|
|
}
|
|
|
|
fn bellTrampoline(handler: *Handler) void {
|
|
const wrapper = TerminalWrapper.fromHandler(handler);
|
|
const func = wrapper.effects.bell orelse return;
|
|
func(@ptrCast(wrapper), wrapper.effects.userdata);
|
|
}
|
|
|
|
fn clipboardWriteTrampoline(handler: *Handler, write: clipboard.Write) clipboard.WriteResult {
|
|
const wrapper = TerminalWrapper.fromHandler(handler);
|
|
const func = wrapper.effects.clipboard_write orelse return .unsupported;
|
|
|
|
// Most protocols currently produce one representation, so keep that
|
|
// path allocation-free while supporting arbitrary multi-MIME writes.
|
|
var stack_contents: [4]ClipboardContent = undefined;
|
|
const contents: []ClipboardContent = if (write.contents.len <= stack_contents.len)
|
|
stack_contents[0..write.contents.len]
|
|
else
|
|
wrapper.terminal.gpa().alloc(ClipboardContent, write.contents.len) catch
|
|
return .io_error;
|
|
defer if (write.contents.len > stack_contents.len)
|
|
wrapper.terminal.gpa().free(contents);
|
|
|
|
for (contents, write.contents) |*c_content, content| {
|
|
c_content.* = .{
|
|
.mime = .{
|
|
.ptr = content.mime.ptr,
|
|
.len = content.mime.len,
|
|
},
|
|
.data = .{
|
|
.ptr = content.data.ptr,
|
|
.len = content.data.len,
|
|
},
|
|
};
|
|
}
|
|
|
|
const request: ClipboardWrite = .{
|
|
.size = @sizeOf(ClipboardWrite),
|
|
.location = write.location,
|
|
.contents = if (contents.len > 0) contents.ptr else null,
|
|
.contents_len = contents.len,
|
|
};
|
|
return func(@ptrCast(wrapper), wrapper.effects.userdata, &request);
|
|
}
|
|
|
|
fn desktopNotificationTrampoline(
|
|
handler: *Handler,
|
|
notification: Action.ShowDesktopNotification,
|
|
) void {
|
|
const wrapper = TerminalWrapper.fromHandler(handler);
|
|
const func = wrapper.effects.desktop_notification orelse return;
|
|
const request: DesktopNotification = .{
|
|
.size = @sizeOf(DesktopNotification),
|
|
.title = .{
|
|
.ptr = notification.title.ptr,
|
|
.len = notification.title.len,
|
|
},
|
|
.body = .{
|
|
.ptr = notification.body.ptr,
|
|
.len = notification.body.len,
|
|
},
|
|
};
|
|
func(@ptrCast(wrapper), wrapper.effects.userdata, &request);
|
|
}
|
|
|
|
fn colorSchemeTrampoline(handler: *Handler) ?device_status.ColorScheme {
|
|
const wrapper = TerminalWrapper.fromHandler(handler);
|
|
const func = wrapper.effects.color_scheme orelse return null;
|
|
var scheme: device_status.ColorScheme = undefined;
|
|
if (func(@ptrCast(wrapper), wrapper.effects.userdata, &scheme)) return scheme;
|
|
return null;
|
|
}
|
|
|
|
fn deviceAttributesTrampoline(handler: *Handler) device_attributes.Attributes {
|
|
const wrapper = TerminalWrapper.fromHandler(handler);
|
|
const func = wrapper.effects.device_attributes_cb orelse return .{};
|
|
|
|
// Get our attributes from the callback.
|
|
var c_attrs: CDeviceAttributes = undefined;
|
|
if (!func(@ptrCast(wrapper), wrapper.effects.userdata, &c_attrs)) return .{};
|
|
|
|
// Note below we use a lot of enumFromInt but its always safe
|
|
// because all our types are non-exhaustive enums.
|
|
|
|
const n: usize = @min(c_attrs.primary.num_features, 64);
|
|
for (0..n) |i| wrapper.effects.da_features_buf[i] = @enumFromInt(c_attrs.primary.features[i]);
|
|
|
|
return .{
|
|
.primary = .{
|
|
.conformance_level = @enumFromInt(c_attrs.primary.conformance_level),
|
|
.features = wrapper.effects.da_features_buf[0..n],
|
|
},
|
|
.secondary = .{
|
|
.device_type = @enumFromInt(c_attrs.secondary.device_type),
|
|
.firmware_version = c_attrs.secondary.firmware_version,
|
|
.rom_cartridge = c_attrs.secondary.rom_cartridge,
|
|
},
|
|
.tertiary = .{
|
|
.unit_id = c_attrs.tertiary.unit_id,
|
|
},
|
|
};
|
|
}
|
|
|
|
fn enquiryTrampoline(handler: *Handler) []const u8 {
|
|
const wrapper = TerminalWrapper.fromHandler(handler);
|
|
const func = wrapper.effects.enquiry orelse return "";
|
|
const result = func(@ptrCast(wrapper), wrapper.effects.userdata);
|
|
if (result.len == 0) return "";
|
|
return result.ptr[0..result.len];
|
|
}
|
|
|
|
fn xtversionTrampoline(handler: *Handler) []const u8 {
|
|
const wrapper = TerminalWrapper.fromHandler(handler);
|
|
const func = wrapper.effects.xtversion orelse return "";
|
|
const result = func(@ptrCast(wrapper), wrapper.effects.userdata);
|
|
if (result.len == 0) return "";
|
|
return result.ptr[0..result.len];
|
|
}
|
|
|
|
fn titleChangedTrampoline(handler: *Handler) void {
|
|
const wrapper = TerminalWrapper.fromHandler(handler);
|
|
const func = wrapper.effects.title_changed orelse return;
|
|
func(@ptrCast(wrapper), wrapper.effects.userdata);
|
|
}
|
|
|
|
fn pwdChangedTrampoline(handler: *Handler) void {
|
|
const wrapper = TerminalWrapper.fromHandler(handler);
|
|
const func = wrapper.effects.pwd_changed orelse return;
|
|
func(@ptrCast(wrapper), wrapper.effects.userdata);
|
|
}
|
|
|
|
fn progressReportTrampoline(
|
|
handler: *Handler,
|
|
report: osc.Command.ProgressReport,
|
|
) void {
|
|
const wrapper = TerminalWrapper.fromHandler(handler);
|
|
const func = wrapper.effects.progress_report orelse return;
|
|
const c_report: ProgressReport = .{
|
|
.size = @sizeOf(ProgressReport),
|
|
.state = @enumFromInt(@intFromEnum(report.state)),
|
|
.progress = if (report.progress) |value| @intCast(value) else -1,
|
|
};
|
|
func(@ptrCast(wrapper), wrapper.effects.userdata, &c_report);
|
|
}
|
|
|
|
fn sizeTrampoline(handler: *Handler) ?size_report.Size {
|
|
const wrapper = TerminalWrapper.fromHandler(handler);
|
|
const func = wrapper.effects.size_cb orelse return null;
|
|
var s: size_report.Size = undefined;
|
|
if (func(@ptrCast(wrapper), wrapper.effects.userdata, &s)) return s;
|
|
return null;
|
|
}
|
|
};
|
|
|
|
/// C: GhosttyTerminal
|
|
pub const Terminal = ?*TerminalWrapper;
|
|
|
|
/// C: GhosttyTerminalCompressionMode
|
|
pub const CompressionMode = ZigTerminal.CompressionMode;
|
|
|
|
/// C: GhosttyTerminalCompressionResult
|
|
pub const CompressionResult = ZigTerminal.CompressionResult;
|
|
|
|
pub fn zigTerminal(terminal_: Terminal) ?*ZigTerminal {
|
|
return (terminal_ orelse return null).terminal;
|
|
}
|
|
|
|
/// Attach the persistent C stream and I/O owner to a heap-stable terminal.
|
|
/// The caller retains ownership of both inputs if wrapper allocation fails.
|
|
fn wrap(
|
|
alloc: std.mem.Allocator,
|
|
t: *ZigTerminal,
|
|
io: Io,
|
|
continuation_max_bytes: usize,
|
|
) error{OutOfMemory}!Terminal {
|
|
const wrapper = alloc.create(TerminalWrapper) catch
|
|
return error.OutOfMemory;
|
|
|
|
// Trampolines are always installed so setting C callbacks later takes
|
|
// effect immediately.
|
|
var handler: Stream.Handler = t.vtHandler();
|
|
handler.effects = .{
|
|
.write_pty = &Effects.writePtyTrampoline,
|
|
.bell = &Effects.bellTrampoline,
|
|
.color_scheme = &Effects.colorSchemeTrampoline,
|
|
.desktop_notification = &Effects.desktopNotificationTrampoline,
|
|
.device_attributes = &Effects.deviceAttributesTrampoline,
|
|
.enquiry = &Effects.enquiryTrampoline,
|
|
.xtversion = &Effects.xtversionTrampoline,
|
|
.title_changed = &Effects.titleChangedTrampoline,
|
|
.pwd_changed = &Effects.pwdChangedTrampoline,
|
|
.progress_report = &Effects.progressReportTrampoline,
|
|
.size = &Effects.sizeTrampoline,
|
|
.clipboard_write = &Effects.clipboardWriteTrampoline,
|
|
};
|
|
|
|
wrapper.* = .{
|
|
.terminal = t,
|
|
.io = io,
|
|
.tmp_dir_path = undefined,
|
|
.stream = Stream.init(.{
|
|
.allocator = alloc,
|
|
.handler = handler,
|
|
.continuation_max_bytes = continuation_max_bytes,
|
|
}),
|
|
};
|
|
return wrapper;
|
|
}
|
|
|
|
pub const RestoreContinuationError = error{
|
|
OutOfMemory,
|
|
ContinuationDisabled,
|
|
ContinuationUnavailable,
|
|
InvalidContinuation,
|
|
};
|
|
|
|
/// Replay a decoded continuation exactly once into a newly created C terminal
|
|
/// and verify that the persistent stream exports the identical canonical
|
|
/// bytes. The terminal remains valid on error and may be freed normally.
|
|
pub fn restoreContinuation(
|
|
terminal_: Terminal,
|
|
continuation: []const u8,
|
|
) RestoreContinuationError!void {
|
|
const wrapper = terminal_ orelse return error.InvalidContinuation;
|
|
if (continuation.len > 0) wrapper.stream.nextSlice(continuation);
|
|
|
|
var exported: std.Io.Writer.Allocating = .init(wrapper.terminal.gpa());
|
|
defer exported.deinit();
|
|
wrapper.stream.writeContinuation(&exported.writer) catch |err| switch (err) {
|
|
error.WriteFailed => return error.OutOfMemory,
|
|
error.ContinuationDisabled => return error.ContinuationDisabled,
|
|
error.ContinuationUnavailable => return error.ContinuationUnavailable,
|
|
};
|
|
if (!std.mem.eql(u8, continuation, exported.written())) {
|
|
return error.InvalidContinuation;
|
|
}
|
|
}
|
|
|
|
pub const FromDecodedError = error{
|
|
OutOfMemory,
|
|
InvalidContinuation,
|
|
};
|
|
|
|
/// Transfer a core snapshot result into a caller-owned C terminal.
|
|
///
|
|
/// This function consumes `io` on every path. The decoded terminal is
|
|
/// transferred only after its final heap address has been allocated; its
|
|
/// continuation remains in `decoded` and is replayed before returning.
|
|
/// Replay uses a temporary exact-size tracker which is disabled before the
|
|
/// terminal crosses the C ABI, restoring the ordinary C default policy.
|
|
pub fn fromDecoded(
|
|
alloc: std.mem.Allocator,
|
|
io: Io,
|
|
decoded: *snapshot_core.Decoded,
|
|
) FromDecodedError!Terminal {
|
|
const native = alloc.create(ZigTerminal) catch {
|
|
io.deinit(alloc);
|
|
return error.OutOfMemory;
|
|
};
|
|
native.* = decoded.toOwned();
|
|
|
|
const continuation = switch (decoded.continuation) {
|
|
.ground => "",
|
|
.bytes => |bytes| bytes,
|
|
};
|
|
|
|
// Non-ground state needs tracking only long enough to verify that replay
|
|
// reconstructed the exact canonical continuation. Ground state needs no
|
|
// tracker at all.
|
|
const terminal = wrap(alloc, native, io, continuation.len) catch |err| {
|
|
native.deinit(alloc);
|
|
alloc.destroy(native);
|
|
io.deinit(alloc);
|
|
return err;
|
|
};
|
|
errdefer free(terminal);
|
|
|
|
if (continuation.len > 0) {
|
|
restoreContinuation(terminal, continuation) catch |err| return switch (err) {
|
|
// The decoded bytes exactly fit this fresh tracker's cap, so
|
|
// losing them while replaying can only be an allocation failure.
|
|
error.OutOfMemory,
|
|
error.ContinuationUnavailable,
|
|
=> error.OutOfMemory,
|
|
error.ContinuationDisabled,
|
|
error.InvalidContinuation,
|
|
=> error.InvalidContinuation,
|
|
};
|
|
}
|
|
|
|
// Decoder validation limits are not terminal runtime policy. A restored
|
|
// terminal always starts with the same disabled tracking default as new.
|
|
setContinuationMaxBytes(terminal.?, default_continuation_max_bytes);
|
|
return terminal;
|
|
}
|
|
|
|
const NewError = error{
|
|
InvalidValue,
|
|
OutOfMemory,
|
|
};
|
|
|
|
pub fn new(
|
|
alloc_: ?*const CAllocator,
|
|
result: *Terminal,
|
|
cols: size.CellCountInt,
|
|
rows: size.CellCountInt,
|
|
) callconv(lib.calling_conv) Result {
|
|
result.* = new_(alloc_, cols, rows) catch |err| {
|
|
result.* = null;
|
|
return switch (err) {
|
|
error.InvalidValue => .invalid_value,
|
|
error.OutOfMemory => .out_of_memory,
|
|
};
|
|
};
|
|
|
|
return .success;
|
|
}
|
|
|
|
fn new_(
|
|
alloc_: ?*const CAllocator,
|
|
cols: size.CellCountInt,
|
|
rows: size.CellCountInt,
|
|
) NewError!Terminal {
|
|
if (cols == 0 or rows == 0) return error.InvalidValue;
|
|
|
|
const alloc = lib.alloc.default(alloc_);
|
|
const t = alloc.create(ZigTerminal) catch
|
|
return error.OutOfMemory;
|
|
errdefer alloc.destroy(t);
|
|
|
|
const io = try Io.init(alloc);
|
|
errdefer io.deinit(alloc);
|
|
|
|
// Setup our terminal
|
|
t.* = try .init(
|
|
io.io(),
|
|
alloc,
|
|
.{
|
|
.cols = cols,
|
|
.rows = rows,
|
|
},
|
|
);
|
|
errdefer t.deinit(alloc);
|
|
|
|
// libghostty-vt embedders don't necessarily install Ghostty's shell
|
|
// integration, so don't assume OSC 133 prompts can be redrawn on resize.
|
|
// Shells can still opt in with OSC 133;A;redraw=1.
|
|
t.flags.shell_redraws_prompt = .false;
|
|
|
|
return try wrap(
|
|
alloc,
|
|
t,
|
|
io,
|
|
default_continuation_max_bytes,
|
|
);
|
|
}
|
|
|
|
pub fn vt_write(
|
|
terminal_: Terminal,
|
|
ptr: [*]const u8,
|
|
len: usize,
|
|
) callconv(lib.calling_conv) void {
|
|
const wrapper = terminal_ orelse return;
|
|
wrapper.stream.nextSlice(ptr[0..len]);
|
|
}
|
|
|
|
pub const ContinuationWriteError = error{
|
|
InvalidValue,
|
|
WriteFailed,
|
|
ContinuationDisabled,
|
|
ContinuationUnavailable,
|
|
};
|
|
|
|
/// Write the exact replay-safe continuation for a C terminal to a Zig writer.
|
|
/// Snapshot encoding uses this helper so it can preflight continuation state
|
|
/// without converting its allocator or writer through the C ABI.
|
|
pub fn continuationWriteIo(
|
|
terminal_: Terminal,
|
|
writer: *std.Io.Writer,
|
|
) ContinuationWriteError!void {
|
|
// Keep handle validation here so the three public output forms and the
|
|
// snapshot encoder all use exactly the same continuation preflight.
|
|
const wrapper = terminal_ orelse return error.InvalidValue;
|
|
|
|
// Stream owns the tracker and distinguishes disabled tracking from a
|
|
// tracker that lost bytes after allocation failure or limit overflow.
|
|
try wrapper.stream.writeContinuation(writer);
|
|
}
|
|
|
|
pub const ContinuationAllocError = error{
|
|
InvalidValue,
|
|
OutOfMemory,
|
|
ContinuationDisabled,
|
|
ContinuationUnavailable,
|
|
};
|
|
|
|
/// Return an allocator-owned copy of the terminal's replay-safe continuation.
|
|
/// The caller owns the returned slice and must free it with `alloc`.
|
|
///
|
|
/// If `allow_untracked_ground` is true, disabled tracking is accepted only
|
|
/// when the stream is provably at ground and an owned empty slice is returned.
|
|
pub fn continuationAllocIo(
|
|
terminal_: Terminal,
|
|
alloc: std.mem.Allocator,
|
|
allow_untracked_ground: bool,
|
|
) ContinuationAllocError![]u8 {
|
|
const wrapper = terminal_ orelse return error.InvalidValue;
|
|
if (allow_untracked_ground and
|
|
wrapper.stream.continuation == null and
|
|
wrapper.stream.ground())
|
|
{
|
|
return alloc.dupe(u8, "") catch error.OutOfMemory;
|
|
}
|
|
|
|
// The allocating writer gives snapshot encoding and the public allocation
|
|
// API one common way to obtain an exact owned continuation.
|
|
var aw: std.Io.Writer.Allocating = .init(alloc);
|
|
defer aw.deinit();
|
|
|
|
// A write failure from an Allocating writer can only be allocation failure;
|
|
// no external callback participates in this path.
|
|
continuationWriteIo(terminal_, &aw.writer) catch |err| switch (err) {
|
|
error.InvalidValue => return error.InvalidValue,
|
|
error.WriteFailed => return error.OutOfMemory,
|
|
error.ContinuationDisabled => return error.ContinuationDisabled,
|
|
error.ContinuationUnavailable => return error.ContinuationUnavailable,
|
|
};
|
|
|
|
// Transfer the buffer out before the deferred writer cleanup runs.
|
|
return aw.toOwnedSlice() catch error.OutOfMemory;
|
|
}
|
|
|
|
/// Map errors that are intrinsic to continuation export. Public callback
|
|
/// failures receive finer classification in `continuation_write` below.
|
|
fn continuationErrorResult(err: ContinuationWriteError) Result {
|
|
return switch (err) {
|
|
error.InvalidValue => .invalid_value,
|
|
error.WriteFailed => .io_error,
|
|
error.ContinuationDisabled => .invalid_value,
|
|
error.ContinuationUnavailable => .invalid_value,
|
|
};
|
|
}
|
|
|
|
pub fn continuation_write(
|
|
terminal_: Terminal,
|
|
writer: c_io.Writer,
|
|
) callconv(lib.calling_conv) Result {
|
|
// Reject the missing callback before invoking the common helper so this is
|
|
// classified as a bad argument rather than a write failure.
|
|
if (writer.write == null) return .invalid_value;
|
|
|
|
// The callback was validated above, so invalid_write can only mean output
|
|
// accounting overflow. Keep that distinct from callback rejection.
|
|
var adapter: c_io.WriterAdapter = .init(writer);
|
|
continuationWriteIo(terminal_, &adapter.interface) catch |err| {
|
|
if (err == error.WriteFailed) {
|
|
if (adapter.invalid_write) return .limit_exceeded;
|
|
if (adapter.callback_failed) return .io_error;
|
|
}
|
|
return continuationErrorResult(err);
|
|
};
|
|
return .success;
|
|
}
|
|
|
|
pub fn continuation_buf(
|
|
terminal_: Terminal,
|
|
out_: ?[*]u8,
|
|
out_len: usize,
|
|
out_written_: ?*usize,
|
|
) callconv(lib.calling_conv) Result {
|
|
const out_written = out_written_ orelse return .invalid_value;
|
|
// All failure paths leave deterministic output metadata.
|
|
out_written.* = 0;
|
|
if (out_ == null and out_len != 0) return .invalid_value;
|
|
|
|
if (out_ == null) {
|
|
// A null/zero destination is the explicit size-query form. Discarding
|
|
// runs the real exporter, so disabled or unavailable tracking is still
|
|
// detected before a required length is reported.
|
|
var discarding: std.Io.Writer.Discarding = .init(&.{});
|
|
continuationWriteIo(terminal_, &discarding.writer) catch |err|
|
|
return continuationErrorResult(err);
|
|
out_written.* = @intCast(discarding.count);
|
|
return .out_of_space;
|
|
}
|
|
|
|
// Fixed writers report WriteFailed when capacity is exhausted. The stream
|
|
// exporter itself remains all-or-nothing from the API's perspective.
|
|
var writer: std.Io.Writer = .fixed(out_.?[0..out_len]);
|
|
continuationWriteIo(terminal_, &writer) catch |err| switch (err) {
|
|
error.WriteFailed => {
|
|
// Re-run against a counter to return the full required capacity,
|
|
// not merely the prefix that fit in the caller's buffer.
|
|
var discarding: std.Io.Writer.Discarding = .init(&.{});
|
|
continuationWriteIo(terminal_, &discarding.writer) catch |count_err|
|
|
return continuationErrorResult(count_err);
|
|
out_written.* = @intCast(discarding.count);
|
|
return .out_of_space;
|
|
},
|
|
else => return continuationErrorResult(err),
|
|
};
|
|
|
|
// `end` is the initialized prefix of the fixed destination.
|
|
out_written.* = writer.end;
|
|
return .success;
|
|
}
|
|
|
|
pub fn continuation_alloc(
|
|
terminal_: Terminal,
|
|
alloc_: ?*const CAllocator,
|
|
out_ptr_: ?*?[*]u8,
|
|
out_len_: ?*usize,
|
|
) callconv(lib.calling_conv) Result {
|
|
const out_ptr = out_ptr_ orelse return .invalid_value;
|
|
const out_len = out_len_ orelse return .invalid_value;
|
|
// Make ownership unambiguous even if validation or allocation fails.
|
|
out_ptr.* = null;
|
|
out_len.* = 0;
|
|
|
|
// Resolve NULL to libghostty-vt's default allocator before entering the
|
|
// shared Zig allocation path.
|
|
const bytes = continuationAllocIo(
|
|
terminal_,
|
|
lib.alloc.default(alloc_),
|
|
false,
|
|
) catch |err| return switch (err) {
|
|
error.InvalidValue => .invalid_value,
|
|
error.OutOfMemory => .out_of_memory,
|
|
error.ContinuationDisabled => .invalid_value,
|
|
error.ContinuationUnavailable => .invalid_value,
|
|
};
|
|
|
|
// Ownership crosses the ABI here; callers release this exact pointer and
|
|
// length with ghostty_free and the same allocator selection.
|
|
out_ptr.* = bytes.ptr;
|
|
out_len.* = bytes.len;
|
|
return .success;
|
|
}
|
|
|
|
fn continuationMaxBytes(wrapper: *const TerminalWrapper) usize {
|
|
// Absence of a tracker is the public representation of disabled tracking.
|
|
return if (wrapper.stream.continuation) |tracker|
|
|
tracker.max_bytes
|
|
else
|
|
0;
|
|
}
|
|
|
|
/// Change continuation tracking policy without disturbing normal VT parser
|
|
/// state. Bytes which were not retained while disabled or after exceeding an
|
|
/// earlier cap cannot be reconstructed: export remains unavailable until a
|
|
/// later feed reaches ground or contains a new replay start.
|
|
fn setContinuationMaxBytes(wrapper: *TerminalWrapper, max_bytes: usize) void {
|
|
if (max_bytes == 0) {
|
|
// Disabling releases retained bytes immediately. Parser and UTF-8 state
|
|
// continue normally; only future replay/export information is lost.
|
|
if (wrapper.stream.continuation) |*tracker| tracker.deinit();
|
|
wrapper.stream.continuation = null;
|
|
return;
|
|
}
|
|
|
|
if (wrapper.stream.continuation) |*tracker| {
|
|
// Changing a live cap preserves retained bytes when they still fit.
|
|
tracker.max_bytes = max_bytes;
|
|
if (tracker.bytes.items.len > max_bytes) {
|
|
// Once a retained prefix is discarded it cannot be reconstructed
|
|
// from parser state alone. Mark it broken until Stream observes a
|
|
// ground state or a new replay-safe sequence start.
|
|
tracker.bytes.clearRetainingCapacity();
|
|
tracker.broken = true;
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Enabling from zero starts an empty tracker owned by the terminal's
|
|
// allocator. It can immediately track only if no earlier bytes are needed.
|
|
wrapper.stream.continuation = .init(wrapper.terminal.gpa(), max_bytes);
|
|
if (!wrapper.stream.ground()) {
|
|
// The parser was already mid-sequence while tracking was disabled, so
|
|
// exporting now would omit an unknown prefix.
|
|
wrapper.stream.continuation.?.broken = true;
|
|
}
|
|
}
|
|
|
|
pub fn compression_activity(
|
|
terminal_: Terminal,
|
|
out_activity_: ?*u64,
|
|
) callconv(lib.calling_conv) Result {
|
|
const t: *ZigTerminal = (terminal_ orelse return .invalid_value).terminal;
|
|
const out_activity = out_activity_ orelse return .invalid_value;
|
|
out_activity.* = t.compressionActivity();
|
|
return .success;
|
|
}
|
|
|
|
pub fn compress(
|
|
terminal_: Terminal,
|
|
mode_: c_int,
|
|
out_result_: ?*CompressionResult,
|
|
) callconv(lib.calling_conv) Result {
|
|
const t: *ZigTerminal = (terminal_ orelse return .invalid_value).terminal;
|
|
const out_result = out_result_ orelse return .invalid_value;
|
|
const mode = std.enums.fromInt(CompressionMode, mode_) orelse return .invalid_value;
|
|
out_result.* = t.compress(mode);
|
|
return .success;
|
|
}
|
|
|
|
/// C: GhosttyTerminalOption
|
|
pub const Option = enum(c_int) {
|
|
userdata = 0,
|
|
write_pty = 1,
|
|
bell = 2,
|
|
enquiry = 3,
|
|
xtversion = 4,
|
|
title_changed = 5,
|
|
size_cb = 6,
|
|
color_scheme = 7,
|
|
device_attributes = 8,
|
|
title = 9,
|
|
pwd = 10,
|
|
color_foreground = 11,
|
|
color_background = 12,
|
|
color_cursor = 13,
|
|
color_palette = 14,
|
|
kitty_image_storage_limit = 15,
|
|
kitty_image_medium_file = 16,
|
|
kitty_image_medium_temp_file = 17,
|
|
kitty_image_medium_shared_mem = 18,
|
|
apc_max_bytes = 19,
|
|
apc_max_bytes_kitty = 20,
|
|
selection = 21,
|
|
default_cursor_style = 22,
|
|
default_cursor_blink = 23,
|
|
glyph_protocol = 24,
|
|
pwd_changed = 25,
|
|
clipboard_write = 26,
|
|
scrollback_max_bytes = 27,
|
|
scrollback_max_lines = 28,
|
|
desktop_notification = 29,
|
|
progress_report = 30,
|
|
continuation_max_bytes = 31,
|
|
title_report = 32,
|
|
mode_default = 33,
|
|
mode = 34,
|
|
|
|
/// Input type expected for setting the option.
|
|
pub fn InType(comptime self: Option) type {
|
|
return switch (self) {
|
|
.userdata => ?*const anyopaque,
|
|
.write_pty => ?Effects.WritePtyFn,
|
|
.bell => ?Effects.BellFn,
|
|
.color_scheme => ?Effects.ColorSchemeFn,
|
|
.desktop_notification => ?Effects.DesktopNotificationFn,
|
|
.device_attributes => ?Effects.DeviceAttributesFn,
|
|
.enquiry => ?Effects.EnquiryFn,
|
|
.xtversion => ?Effects.XtversionFn,
|
|
.title_changed => ?Effects.TitleChangedFn,
|
|
.pwd_changed => ?Effects.PwdChangedFn,
|
|
.progress_report => ?Effects.ProgressReportFn,
|
|
.size_cb => ?Effects.SizeFn,
|
|
.clipboard_write => ?Effects.ClipboardWriteFn,
|
|
.title, .pwd => ?*const lib.String,
|
|
.color_foreground, .color_background, .color_cursor => ?*const color.RGB.C,
|
|
.color_palette => ?*const color.PaletteC,
|
|
.kitty_image_storage_limit => ?*const u64,
|
|
.kitty_image_medium_file,
|
|
.kitty_image_medium_shared_mem,
|
|
.glyph_protocol,
|
|
.title_report,
|
|
=> ?*const bool,
|
|
.kitty_image_medium_temp_file => ?*const lib.String,
|
|
.apc_max_bytes,
|
|
.apc_max_bytes_kitty,
|
|
.scrollback_max_bytes,
|
|
.scrollback_max_lines,
|
|
.continuation_max_bytes,
|
|
=> ?*const usize,
|
|
.selection => ?*const selection_c.CSelection,
|
|
.default_cursor_style => ?*const TerminalCursorStyle,
|
|
.default_cursor_blink => ?*const bool,
|
|
.mode, .mode_default => ?*const ModeConfig,
|
|
};
|
|
}
|
|
};
|
|
|
|
pub fn set(
|
|
terminal_: Terminal,
|
|
option: Option,
|
|
value: ?*const anyopaque,
|
|
) callconv(lib.calling_conv) Result {
|
|
if (comptime std.debug.runtime_safety) {
|
|
_ = std.enums.fromInt(Option, @intFromEnum(option)) orelse {
|
|
log.warn("terminal_set invalid option value={d}", .{@intFromEnum(option)});
|
|
return .invalid_value;
|
|
};
|
|
}
|
|
|
|
const wrapper = terminal_ orelse return .invalid_value;
|
|
|
|
return switch (option) {
|
|
inline else => |comptime_option| setTyped(
|
|
wrapper,
|
|
comptime_option,
|
|
@ptrCast(@alignCast(value)),
|
|
),
|
|
};
|
|
}
|
|
|
|
fn setTyped(
|
|
wrapper: *TerminalWrapper,
|
|
comptime option: Option,
|
|
value: option.InType(),
|
|
) Result {
|
|
switch (option) {
|
|
.userdata => wrapper.effects.userdata = @constCast(value),
|
|
.write_pty => wrapper.effects.write_pty = value,
|
|
.bell => wrapper.effects.bell = value,
|
|
.color_scheme => wrapper.effects.color_scheme = value,
|
|
.desktop_notification => wrapper.effects.desktop_notification = value,
|
|
.device_attributes => wrapper.effects.device_attributes_cb = value,
|
|
.enquiry => wrapper.effects.enquiry = value,
|
|
.xtversion => wrapper.effects.xtversion = value,
|
|
.title_changed => wrapper.effects.title_changed = value,
|
|
.pwd_changed => wrapper.effects.pwd_changed = value,
|
|
.progress_report => wrapper.effects.progress_report = value,
|
|
.size_cb => wrapper.effects.size_cb = value,
|
|
.clipboard_write => wrapper.effects.clipboard_write = value,
|
|
.title_report => wrapper.stream.handler.title_report = if (value) |ptr|
|
|
ptr.*
|
|
else
|
|
false,
|
|
.title => {
|
|
const str = if (value) |v| v.ptr[0..v.len] else "";
|
|
wrapper.terminal.setTitle(str) catch return .out_of_memory;
|
|
},
|
|
.pwd => {
|
|
const str = if (value) |v| v.ptr[0..v.len] else "";
|
|
wrapper.terminal.setPwd(str) catch return .out_of_memory;
|
|
},
|
|
.color_foreground => {
|
|
wrapper.terminal.colors.foreground.default = if (value) |v| .fromC(v.*) else null;
|
|
wrapper.terminal.flags.dirty.palette = true;
|
|
},
|
|
.color_background => {
|
|
wrapper.terminal.colors.background.default = if (value) |v| .fromC(v.*) else null;
|
|
wrapper.terminal.flags.dirty.palette = true;
|
|
},
|
|
.color_cursor => {
|
|
wrapper.terminal.colors.cursor.default = if (value) |v| .fromC(v.*) else null;
|
|
wrapper.terminal.flags.dirty.palette = true;
|
|
},
|
|
.color_palette => {
|
|
wrapper.terminal.colors.palette.changeDefault(
|
|
if (value) |v| color.paletteZval(v) else color.default,
|
|
);
|
|
wrapper.terminal.flags.dirty.palette = true;
|
|
},
|
|
.kitty_image_storage_limit => {
|
|
if (comptime !build_options.kitty_graphics) return .success;
|
|
const limit: usize = if (value) |v| @intCast(v.*) else 0;
|
|
var it = wrapper.terminal.screens.all.iterator();
|
|
while (it.next()) |entry| {
|
|
const screen = entry.value.*;
|
|
screen.kitty_images.setLimit(screen.io, screen.alloc, screen, limit);
|
|
}
|
|
},
|
|
.kitty_image_medium_file,
|
|
.kitty_image_medium_shared_mem,
|
|
=> {
|
|
if (comptime !build_options.kitty_graphics) return .success;
|
|
const val = (value orelse return .success).*;
|
|
var it = wrapper.terminal.screens.all.iterator();
|
|
while (it.next()) |entry| {
|
|
const screen = entry.value.*;
|
|
switch (option) {
|
|
.kitty_image_medium_file => screen.kitty_images.image_limits.file = val,
|
|
.kitty_image_medium_shared_mem => screen.kitty_images.image_limits.shared_memory = val,
|
|
else => unreachable,
|
|
}
|
|
}
|
|
},
|
|
.kitty_image_medium_temp_file => {
|
|
if (comptime !build_options.kitty_graphics) return .success;
|
|
if (value) |v| {
|
|
if (v.len > wrapper.tmp_dir_path.len) return .out_of_memory;
|
|
@memcpy(wrapper.tmp_dir_path[0..v.len], v.ptr[0..v.len]);
|
|
var it = wrapper.terminal.screens.all.iterator();
|
|
while (it.next()) |entry| {
|
|
const screen = entry.value.*;
|
|
screen.kitty_images.image_limits.temporary_file = .{
|
|
.enabled = .{ .directory = wrapper.tmp_dir_path[0..v.len] },
|
|
};
|
|
}
|
|
} else {
|
|
var it = wrapper.terminal.screens.all.iterator();
|
|
while (it.next()) |entry| {
|
|
const screen = entry.value.*;
|
|
screen.kitty_images.image_limits.temporary_file = .disabled;
|
|
}
|
|
}
|
|
},
|
|
.apc_max_bytes => {
|
|
wrapper.stream.handler.apc_handler.max_bytes = if (value) |ptr|
|
|
.initFull(ptr.*)
|
|
else
|
|
.{};
|
|
},
|
|
.apc_max_bytes_kitty => {
|
|
if (value) |ptr| {
|
|
wrapper.stream.handler.apc_handler.max_bytes.put(.kitty, ptr.*);
|
|
} else {
|
|
wrapper.stream.handler.apc_handler.max_bytes.remove(.kitty);
|
|
}
|
|
},
|
|
.glyph_protocol => {
|
|
const enabled = (value orelse return .success).*;
|
|
wrapper.stream.handler.apc_handler.enable(.glyph, enabled);
|
|
if (!enabled) wrapper.terminal.glyph_glossary.clearAndFree(wrapper.terminal.gpa());
|
|
},
|
|
.selection => {
|
|
if (value) |ptr| {
|
|
const sel = ptr.toZig() orelse return .invalid_value;
|
|
wrapper.terminal.screens.active.select(sel) catch return .out_of_memory;
|
|
} else {
|
|
wrapper.terminal.screens.active.clearSelection();
|
|
}
|
|
},
|
|
.default_cursor_style => {
|
|
const style = (if (value) |ptr| ptr.* else TerminalCursorStyle.block).toZig() orelse return .invalid_value;
|
|
wrapper.terminal.setDefaultCursorStyle(style);
|
|
},
|
|
.default_cursor_blink => {
|
|
const blink = if (value) |ptr| ptr.* else false;
|
|
wrapper.terminal.setDefaultCursorBlink(blink);
|
|
},
|
|
.scrollback_max_bytes => wrapper.terminal.setScrollbackMaxBytes(
|
|
if (value) |ptr| ptr.* else null,
|
|
),
|
|
.scrollback_max_lines => wrapper.terminal.setScrollbackMaxLines(
|
|
if (value) |ptr| ptr.* else null,
|
|
),
|
|
.continuation_max_bytes => setContinuationMaxBytes(
|
|
wrapper,
|
|
if (value) |ptr| ptr.* else default_continuation_max_bytes,
|
|
),
|
|
.mode, .mode_default => {
|
|
const config = (value orelse return .invalid_value).*;
|
|
const mode = config.toMode() orelse return .invalid_value;
|
|
switch (option) {
|
|
.mode => wrapper.terminal.modes.set(mode, config.value),
|
|
.mode_default => {
|
|
if (!modes.defaultConfigurable(mode)) return .invalid_value;
|
|
wrapper.terminal.modes.setDefault(mode, config.value);
|
|
},
|
|
else => unreachable,
|
|
}
|
|
},
|
|
}
|
|
return .success;
|
|
}
|
|
|
|
/// C: GhosttyTerminalCursorStyle
|
|
pub const TerminalCursorStyle = enum(c_int) {
|
|
bar = 0,
|
|
block = 1,
|
|
underline = 2,
|
|
block_hollow = 3,
|
|
_,
|
|
|
|
fn toZig(self: TerminalCursorStyle) ?Screen.CursorStyle {
|
|
return switch (self) {
|
|
.bar => .bar,
|
|
.block => .block,
|
|
.underline => .underline,
|
|
.block_hollow => .block_hollow,
|
|
_ => null,
|
|
};
|
|
}
|
|
};
|
|
|
|
/// C: GhosttyDeviceAttributes
|
|
pub const DeviceAttributes = Effects.CDeviceAttributes;
|
|
|
|
/// C: GhosttyTerminalScrollViewport
|
|
pub const ScrollViewport = ZigTerminal.ScrollViewport.C;
|
|
|
|
pub fn scroll_viewport(
|
|
terminal_: Terminal,
|
|
behavior: ScrollViewport,
|
|
) callconv(lib.calling_conv) void {
|
|
const t: *ZigTerminal = (terminal_ orelse return).terminal;
|
|
t.scrollViewport(switch (behavior.tag) {
|
|
.top => .top,
|
|
.bottom => .bottom,
|
|
.delta => .{ .delta = behavior.value.delta },
|
|
.row => .{ .row = behavior.value.row },
|
|
});
|
|
}
|
|
|
|
pub fn resize(
|
|
terminal_: Terminal,
|
|
cols: size.CellCountInt,
|
|
rows: size.CellCountInt,
|
|
cell_width_px: u32,
|
|
cell_height_px: u32,
|
|
) callconv(lib.calling_conv) Result {
|
|
const wrapper = terminal_ orelse return .invalid_value;
|
|
wrapper.stream.handler.resize(.{
|
|
.cols = cols,
|
|
.rows = rows,
|
|
.cell_size_px = .{
|
|
.width = cell_width_px,
|
|
.height = cell_height_px,
|
|
},
|
|
}) catch |err| return switch (err) {
|
|
error.InvalidValue => .invalid_value,
|
|
error.OutOfMemory => .out_of_memory,
|
|
};
|
|
|
|
return .success;
|
|
}
|
|
|
|
pub fn reset(terminal_: Terminal) callconv(lib.calling_conv) void {
|
|
const t: *ZigTerminal = (terminal_ orelse return).terminal;
|
|
t.fullReset();
|
|
}
|
|
|
|
/// C: GhosttyKittyGraphics
|
|
pub const KittyGraphics = kitty_gfx_c.KittyGraphics;
|
|
|
|
/// C: GhosttyTerminalScreen
|
|
pub const TerminalScreen = ScreenSet.Key;
|
|
|
|
/// C: GhosttyTerminalScrollbar
|
|
pub const TerminalScrollbar = PageList.Scrollbar.C;
|
|
|
|
/// C: GhosttyTerminalData
|
|
pub const TerminalData = enum(c_int) {
|
|
invalid = 0,
|
|
cols = 1,
|
|
rows = 2,
|
|
cursor_x = 3,
|
|
cursor_y = 4,
|
|
cursor_pending_wrap = 5,
|
|
active_screen = 6,
|
|
cursor_visible = 7,
|
|
kitty_keyboard_flags = 8,
|
|
scrollbar = 9,
|
|
cursor_style = 10,
|
|
mouse_tracking = 11,
|
|
title = 12,
|
|
pwd = 13,
|
|
total_rows = 14,
|
|
scrollback_rows = 15,
|
|
width_px = 16,
|
|
height_px = 17,
|
|
color_foreground = 18,
|
|
color_background = 19,
|
|
color_cursor = 20,
|
|
color_palette = 21,
|
|
color_foreground_default = 22,
|
|
color_background_default = 23,
|
|
color_cursor_default = 24,
|
|
color_palette_default = 25,
|
|
kitty_image_storage_limit = 26,
|
|
kitty_image_medium_file = 27,
|
|
kitty_image_medium_temp_file = 28,
|
|
kitty_image_medium_shared_mem = 29,
|
|
kitty_graphics = 30,
|
|
selection = 31,
|
|
viewport_active = 32,
|
|
vt_processing_error = 33,
|
|
scrollback_max_bytes = 34,
|
|
scrollback_max_lines = 35,
|
|
continuation_max_bytes = 36,
|
|
mode = 37,
|
|
|
|
/// Output type expected for querying the data of the given kind.
|
|
pub fn OutType(comptime self: TerminalData) type {
|
|
return switch (self) {
|
|
.invalid => void,
|
|
.cols, .rows, .cursor_x, .cursor_y => size.CellCountInt,
|
|
.cursor_pending_wrap,
|
|
.cursor_visible,
|
|
.mouse_tracking,
|
|
.viewport_active,
|
|
.vt_processing_error,
|
|
=> bool,
|
|
.active_screen => TerminalScreen,
|
|
.kitty_keyboard_flags => u8,
|
|
.scrollbar => TerminalScrollbar,
|
|
.cursor_style => style_c.Style,
|
|
.title, .pwd => lib.String,
|
|
.total_rows,
|
|
.scrollback_rows,
|
|
.scrollback_max_bytes,
|
|
.scrollback_max_lines,
|
|
.continuation_max_bytes,
|
|
=> usize,
|
|
.width_px, .height_px => u32,
|
|
.color_foreground,
|
|
.color_background,
|
|
.color_cursor,
|
|
.color_foreground_default,
|
|
.color_background_default,
|
|
.color_cursor_default,
|
|
=> color.RGB.C,
|
|
.color_palette, .color_palette_default => color.PaletteC,
|
|
.kitty_image_storage_limit => u64,
|
|
.kitty_image_medium_file,
|
|
.kitty_image_medium_shared_mem,
|
|
=> bool,
|
|
.kitty_image_medium_temp_file => lib.String,
|
|
.kitty_graphics => KittyGraphics,
|
|
.selection => selection_c.CSelection,
|
|
.mode => ModeConfig,
|
|
};
|
|
}
|
|
};
|
|
|
|
pub fn get(
|
|
terminal_: Terminal,
|
|
data: TerminalData,
|
|
out: ?*anyopaque,
|
|
) callconv(lib.calling_conv) Result {
|
|
if (comptime std.debug.runtime_safety) {
|
|
_ = std.enums.fromInt(TerminalData, @intFromEnum(data)) orelse {
|
|
log.warn("terminal_get invalid data value={d}", .{@intFromEnum(data)});
|
|
return .invalid_value;
|
|
};
|
|
}
|
|
|
|
const out_ptr = out orelse return .invalid_value;
|
|
|
|
return switch (data) {
|
|
.invalid => .invalid_value,
|
|
inline else => |comptime_data| getTyped(
|
|
terminal_,
|
|
comptime_data,
|
|
@ptrCast(@alignCast(out_ptr)),
|
|
),
|
|
};
|
|
}
|
|
|
|
pub fn get_multi(
|
|
terminal_: Terminal,
|
|
count: usize,
|
|
keys: ?[*]const TerminalData,
|
|
values: ?[*]?*anyopaque,
|
|
out_written: ?*usize,
|
|
) callconv(lib.calling_conv) Result {
|
|
const k = keys orelse return .invalid_value;
|
|
const v = values orelse return .invalid_value;
|
|
|
|
for (0..count) |i| {
|
|
const result = get(terminal_, k[i], v[i]);
|
|
if (result != .success) {
|
|
if (out_written) |w| w.* = i;
|
|
return result;
|
|
}
|
|
}
|
|
if (out_written) |w| w.* = count;
|
|
return .success;
|
|
}
|
|
|
|
fn getTyped(
|
|
terminal_: Terminal,
|
|
comptime data: TerminalData,
|
|
out: *data.OutType(),
|
|
) Result {
|
|
const wrapper = terminal_ orelse return .invalid_value;
|
|
const t: *ZigTerminal = wrapper.terminal;
|
|
switch (data) {
|
|
.invalid => return .invalid_value,
|
|
.cols => out.* = t.cols,
|
|
.rows => out.* = t.rows,
|
|
.cursor_x => out.* = t.screens.active.cursor.x,
|
|
.cursor_y => out.* = t.screens.active.cursor.y,
|
|
.cursor_pending_wrap => out.* = t.screens.active.cursor.pending_wrap,
|
|
.active_screen => out.* = t.screens.active_key,
|
|
.cursor_visible => out.* = t.modes.get(.cursor_visible),
|
|
.kitty_keyboard_flags => out.* = @as(u8, t.screens.active.kitty_keyboard.current().int()),
|
|
.scrollbar => out.* = t.screens.active.pages.scrollbar().cval(),
|
|
.cursor_style => out.* = .fromStyle(t.screens.active.cursor.style),
|
|
.mouse_tracking => out.* = t.modes.get(.mouse_event_x10) or
|
|
t.modes.get(.mouse_event_normal) or
|
|
t.modes.get(.mouse_event_button) or
|
|
t.modes.get(.mouse_event_any),
|
|
.title => {
|
|
const title = t.getTitle() orelse "";
|
|
out.* = .{ .ptr = title.ptr, .len = title.len };
|
|
},
|
|
.pwd => {
|
|
const pwd = t.getPwd() orelse "";
|
|
out.* = .{ .ptr = pwd.ptr, .len = pwd.len };
|
|
},
|
|
.total_rows => out.* = t.screens.active.pages.total_rows,
|
|
.scrollback_rows => out.* = t.screens.active.pages.total_rows - t.rows,
|
|
.width_px => out.* = t.width_px,
|
|
.height_px => out.* = t.height_px,
|
|
.color_foreground => out.* = (t.colors.foreground.get() orelse return .no_value).cval(),
|
|
.color_background => out.* = (t.colors.background.get() orelse return .no_value).cval(),
|
|
.color_cursor => out.* = (t.colors.cursor.get() orelse return .no_value).cval(),
|
|
.color_foreground_default => out.* = (t.colors.foreground.default orelse return .no_value).cval(),
|
|
.color_background_default => out.* = (t.colors.background.default orelse return .no_value).cval(),
|
|
.color_cursor_default => out.* = (t.colors.cursor.default orelse return .no_value).cval(),
|
|
.color_palette => out.* = color.paletteCval(&t.colors.palette.current),
|
|
.color_palette_default => out.* = color.paletteCval(&t.colors.palette.original),
|
|
.kitty_image_storage_limit => {
|
|
if (comptime !build_options.kitty_graphics) return .no_value;
|
|
out.* = @intCast(t.screens.active.kitty_images.total_limit);
|
|
},
|
|
.kitty_image_medium_file => {
|
|
if (comptime !build_options.kitty_graphics) return .no_value;
|
|
out.* = t.screens.active.kitty_images.image_limits.file;
|
|
},
|
|
.kitty_image_medium_temp_file => {
|
|
if (comptime !build_options.kitty_graphics) return .no_value;
|
|
const dir = switch (t.screens.active.kitty_images.image_limits.temporary_file) {
|
|
.enabled => |d| d.directory,
|
|
.disabled => "",
|
|
};
|
|
out.* = .{ .ptr = dir.ptr, .len = dir.len };
|
|
},
|
|
.kitty_image_medium_shared_mem => {
|
|
if (comptime !build_options.kitty_graphics) return .no_value;
|
|
out.* = t.screens.active.kitty_images.image_limits.shared_memory;
|
|
},
|
|
.kitty_graphics => {
|
|
if (comptime !build_options.kitty_graphics) return .no_value;
|
|
out.* = &t.screens.active.kitty_images;
|
|
},
|
|
.selection => out.* = selection_c.CSelection.fromZig(
|
|
t.screens.active.selection orelse return .no_value,
|
|
),
|
|
.viewport_active => out.* = t.screens.active.pages.viewport == .active,
|
|
.vt_processing_error => out.* = wrapper.stream.handler.semantic_failure,
|
|
.scrollback_max_bytes => {
|
|
const max = t.screens.get(.primary).?.pages.limits.bytes.explicit;
|
|
if (max == std.math.maxInt(usize)) return .no_value;
|
|
out.* = max;
|
|
},
|
|
.scrollback_max_lines => {
|
|
const max = t.screens.get(.primary).?.pages.limits.lines.explicit;
|
|
if (max == std.math.maxInt(usize)) return .no_value;
|
|
out.* = max;
|
|
},
|
|
.continuation_max_bytes => out.* = continuationMaxBytes(wrapper),
|
|
.mode => {
|
|
const mode = out.toMode() orelse return .invalid_value;
|
|
out.value = t.modes.get(mode);
|
|
},
|
|
}
|
|
|
|
return .success;
|
|
}
|
|
|
|
pub fn grid_ref(
|
|
terminal_: Terminal,
|
|
pt: point.Point.C,
|
|
out_ref: ?*grid_ref_c.CGridRef,
|
|
) callconv(lib.calling_conv) Result {
|
|
const t: *ZigTerminal = (terminal_ orelse return .invalid_value).terminal;
|
|
const zig_pt: point.Point = .fromC(pt);
|
|
const p = t.screens.active.pages.pin(zig_pt) orelse
|
|
return .invalid_value;
|
|
if (out_ref) |out| out.* = grid_ref_c.CGridRef.fromPin(p);
|
|
return .success;
|
|
}
|
|
|
|
pub fn grid_ref_track(
|
|
terminal_: Terminal,
|
|
pt: point.Point.C,
|
|
out_ref: ?*grid_ref_tracked_c.CTrackedGridRef,
|
|
) callconv(lib.calling_conv) Result {
|
|
const wrapper = terminal_ orelse return .invalid_value;
|
|
const out = out_ref orelse return .invalid_value;
|
|
out.* = null;
|
|
|
|
const t: *ZigTerminal = wrapper.terminal;
|
|
const list = &t.screens.active.pages;
|
|
const p = list.pin(.fromC(pt)) orelse return .invalid_value;
|
|
const tracked_pin = list.trackPin(p) catch return .out_of_memory;
|
|
|
|
const alloc = t.gpa();
|
|
const ref = alloc.create(grid_ref_tracked_c.TrackedGridRef) catch {
|
|
list.untrackPin(tracked_pin);
|
|
return .out_of_memory;
|
|
};
|
|
ref.* = .{
|
|
.alloc = alloc,
|
|
.terminal = wrapper,
|
|
.screen_key = t.screens.active_key,
|
|
.screen_generation = t.screens.generation(t.screens.active_key),
|
|
.pin = tracked_pin,
|
|
};
|
|
|
|
// Store the tracked ref in the terminal so that when we free
|
|
// the terminal the tracked ref can be detached safely.
|
|
wrapper.tracked_grid_refs.putNoClobber(
|
|
alloc,
|
|
ref,
|
|
{},
|
|
) catch {
|
|
list.untrackPin(tracked_pin);
|
|
alloc.destroy(ref);
|
|
return .out_of_memory;
|
|
};
|
|
|
|
out.* = ref;
|
|
return .success;
|
|
}
|
|
|
|
pub fn point_from_grid_ref(
|
|
terminal_: Terminal,
|
|
ref: *const grid_ref_c.CGridRef,
|
|
tag: point.Tag,
|
|
out: ?*point.Coordinate,
|
|
) callconv(lib.calling_conv) Result {
|
|
const t: *ZigTerminal = (terminal_ orelse return .invalid_value).terminal;
|
|
const p = ref.toPin() orelse return .invalid_value;
|
|
const pt = t.screens.active.pages.pointFromPin(tag, p) orelse
|
|
return .no_value;
|
|
if (out) |o| o.* = pt.coord();
|
|
return .success;
|
|
}
|
|
|
|
pub fn free(terminal_: Terminal) callconv(lib.calling_conv) void {
|
|
const wrapper = terminal_ orelse return;
|
|
const t = wrapper.terminal;
|
|
const alloc = t.gpa();
|
|
|
|
for (wrapper.tracked_grid_refs.keys()) |ref| ref.terminal = null;
|
|
wrapper.tracked_grid_refs.deinit(alloc);
|
|
wrapper.stream.deinit();
|
|
t.deinit(alloc);
|
|
wrapper.io.deinit(alloc);
|
|
alloc.destroy(t);
|
|
alloc.destroy(wrapper);
|
|
}
|
|
|
|
fn testEnableContinuation(terminal: Terminal) !void {
|
|
const limit: usize = 1024;
|
|
try testing.expectEqual(
|
|
Result.success,
|
|
set(terminal, .continuation_max_bytes, &limit),
|
|
);
|
|
}
|
|
|
|
test "new/free" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
|
|
try testing.expect(t != null);
|
|
free(t);
|
|
}
|
|
|
|
test "new invalid value" {
|
|
var t: Terminal = null;
|
|
|
|
try testing.expectEqual(Result.invalid_value, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
0,
|
|
24,
|
|
));
|
|
try testing.expect(t == null);
|
|
|
|
try testing.expectEqual(Result.invalid_value, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
0,
|
|
));
|
|
try testing.expect(t == null);
|
|
}
|
|
|
|
test "continuation option and data" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
var value: usize = 0;
|
|
try testing.expectEqual(
|
|
Result.success,
|
|
get(t, .continuation_max_bytes, @ptrCast(&value)),
|
|
);
|
|
try testing.expectEqual(default_continuation_max_bytes, value);
|
|
|
|
const custom: usize = 1234;
|
|
try testing.expectEqual(
|
|
Result.success,
|
|
set(t, .continuation_max_bytes, &custom),
|
|
);
|
|
try testing.expectEqual(
|
|
Result.success,
|
|
get(t, .continuation_max_bytes, @ptrCast(&value)),
|
|
);
|
|
try testing.expectEqual(custom, value);
|
|
|
|
try testing.expectEqual(
|
|
Result.success,
|
|
set(t, .continuation_max_bytes, null),
|
|
);
|
|
try testing.expectEqual(
|
|
Result.success,
|
|
get(t, .continuation_max_bytes, @ptrCast(&value)),
|
|
);
|
|
try testing.expectEqual(default_continuation_max_bytes, value);
|
|
|
|
const disabled: usize = 0;
|
|
try testing.expectEqual(
|
|
Result.success,
|
|
set(t, .continuation_max_bytes, &disabled),
|
|
);
|
|
try testing.expectEqual(
|
|
Result.success,
|
|
get(t, .continuation_max_bytes, @ptrCast(&value)),
|
|
);
|
|
try testing.expectEqual(@as(usize, 0), value);
|
|
|
|
var written: usize = 99;
|
|
try testing.expectEqual(
|
|
Result.invalid_value,
|
|
continuation_buf(t, null, 0, &written),
|
|
);
|
|
try testing.expectEqual(@as(usize, 0), written);
|
|
}
|
|
|
|
test "continuation buffer and allocator export exact suffix" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
try testEnableContinuation(t);
|
|
|
|
vt_write(t, "A\x1b[31", 5);
|
|
|
|
var required: usize = 0;
|
|
try testing.expectEqual(
|
|
Result.out_of_space,
|
|
continuation_buf(t, null, 0, &required),
|
|
);
|
|
try testing.expectEqual(@as(usize, 4), required);
|
|
|
|
var short: [2]u8 = undefined;
|
|
try testing.expectEqual(
|
|
Result.out_of_space,
|
|
continuation_buf(t, &short, short.len, &required),
|
|
);
|
|
try testing.expectEqual(@as(usize, 4), required);
|
|
|
|
var buf: [8]u8 = undefined;
|
|
var written: usize = 0;
|
|
try testing.expectEqual(
|
|
Result.success,
|
|
continuation_buf(t, &buf, buf.len, &written),
|
|
);
|
|
try testing.expectEqualStrings("\x1b[31", buf[0..written]);
|
|
|
|
var out_ptr: ?[*]u8 = null;
|
|
var out_len: usize = 0;
|
|
try testing.expectEqual(Result.success, continuation_alloc(
|
|
t,
|
|
&lib.alloc.test_allocator,
|
|
&out_ptr,
|
|
&out_len,
|
|
));
|
|
const allocated = out_ptr orelse return error.TestExpectedEqual;
|
|
defer lib.alloc.default(&lib.alloc.test_allocator).free(allocated[0..out_len]);
|
|
try testing.expectEqualStrings("\x1b[31", allocated[0..out_len]);
|
|
|
|
vt_write(t, "m", 1);
|
|
try testing.expectEqual(
|
|
Result.out_of_space,
|
|
continuation_buf(t, null, 0, &required),
|
|
);
|
|
try testing.expectEqual(@as(usize, 0), required);
|
|
}
|
|
|
|
test "continuation export tracks split UTF-8" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
try testEnableContinuation(t);
|
|
|
|
const prefix = [_]u8{ 0xF0, 0x9F };
|
|
vt_write(t, &prefix, prefix.len);
|
|
|
|
var buf: [4]u8 = undefined;
|
|
var written: usize = 0;
|
|
try testing.expectEqual(
|
|
Result.success,
|
|
continuation_buf(t, &buf, buf.len, &written),
|
|
);
|
|
try testing.expectEqualSlices(u8, &prefix, buf[0..written]);
|
|
|
|
const suffix = [_]u8{ 0x98, 0x80 };
|
|
vt_write(t, &suffix, suffix.len);
|
|
try testing.expectEqual(
|
|
Result.out_of_space,
|
|
continuation_buf(t, null, 0, &written),
|
|
);
|
|
try testing.expectEqual(@as(usize, 0), written);
|
|
}
|
|
|
|
test "continuation callback writer reports success and failure" {
|
|
const Sink = struct {
|
|
bytes: [8]u8 = undefined,
|
|
len: usize = 0,
|
|
|
|
fn write(
|
|
userdata: ?*anyopaque,
|
|
data: [*]const u8,
|
|
len: usize,
|
|
) callconv(lib.calling_conv) bool {
|
|
const self: *@This() = @ptrCast(@alignCast(userdata.?));
|
|
if (len > self.bytes.len - self.len) return false;
|
|
@memcpy(self.bytes[self.len..][0..len], data[0..len]);
|
|
self.len += len;
|
|
return true;
|
|
}
|
|
|
|
fn fail(
|
|
_: ?*anyopaque,
|
|
_: [*]const u8,
|
|
_: usize,
|
|
) callconv(lib.calling_conv) bool {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
try testEnableContinuation(t);
|
|
vt_write(t, "\x1b[31", 4);
|
|
|
|
var sink: Sink = .{};
|
|
try testing.expectEqual(Result.success, continuation_write(t, .{
|
|
.write = &Sink.write,
|
|
.userdata = &sink,
|
|
}));
|
|
try testing.expectEqualStrings("\x1b[31", sink.bytes[0..sink.len]);
|
|
|
|
try testing.expectEqual(Result.io_error, continuation_write(t, .{
|
|
.write = &Sink.fail,
|
|
}));
|
|
try testing.expectEqual(
|
|
Result.invalid_value,
|
|
continuation_write(t, .{}),
|
|
);
|
|
try testing.expectEqual(Result.invalid_value, continuation_write(null, .{
|
|
.write = &Sink.fail,
|
|
}));
|
|
}
|
|
|
|
test "continuation runtime reconfiguration recovers safely" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
const enough: usize = 8;
|
|
try testing.expectEqual(
|
|
Result.success,
|
|
set(t, .continuation_max_bytes, &enough),
|
|
);
|
|
vt_write(t, "\x1b[123", 5);
|
|
const too_small: usize = 2;
|
|
try testing.expectEqual(
|
|
Result.success,
|
|
set(t, .continuation_max_bytes, &too_small),
|
|
);
|
|
|
|
var written: usize = 99;
|
|
try testing.expectEqual(
|
|
Result.invalid_value,
|
|
continuation_buf(t, null, 0, &written),
|
|
);
|
|
try testing.expectEqual(@as(usize, 0), written);
|
|
|
|
// Raising the limit cannot reconstruct discarded bytes, but a new ESC is
|
|
// a complete replay start and repairs tracking without first grounding.
|
|
try testing.expectEqual(
|
|
Result.success,
|
|
set(t, .continuation_max_bytes, &enough),
|
|
);
|
|
vt_write(t, "\x1b[", 2);
|
|
var buf: [8]u8 = undefined;
|
|
try testing.expectEqual(
|
|
Result.success,
|
|
continuation_buf(t, &buf, buf.len, &written),
|
|
);
|
|
try testing.expectEqualStrings("\x1b[", buf[0..written]);
|
|
|
|
const disabled: usize = 0;
|
|
try testing.expectEqual(
|
|
Result.success,
|
|
set(t, .continuation_max_bytes, &disabled),
|
|
);
|
|
try testing.expectEqual(
|
|
Result.success,
|
|
set(t, .continuation_max_bytes, &enough),
|
|
);
|
|
try testing.expectEqual(
|
|
Result.invalid_value,
|
|
continuation_buf(t, null, 0, &written),
|
|
);
|
|
|
|
// Completing the sequence reaches ground and resets the broken marker.
|
|
vt_write(t, "m", 1);
|
|
try testing.expectEqual(
|
|
Result.out_of_space,
|
|
continuation_buf(t, null, 0, &written),
|
|
);
|
|
try testing.expectEqual(@as(usize, 0), written);
|
|
}
|
|
|
|
test "continuation internal allocation and restoration" {
|
|
var source: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&source,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(source);
|
|
try testEnableContinuation(source);
|
|
vt_write(source, "\x1b[31", 4);
|
|
|
|
const alloc = lib.alloc.default(&lib.alloc.test_allocator);
|
|
const bytes = try continuationAllocIo(source, alloc, false);
|
|
defer alloc.free(bytes);
|
|
try testing.expectEqualStrings("\x1b[31", bytes);
|
|
|
|
var restored: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&restored,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(restored);
|
|
try testEnableContinuation(restored);
|
|
try restoreContinuation(restored, bytes);
|
|
|
|
const reexported = try continuationAllocIo(restored, alloc, false);
|
|
defer alloc.free(reexported);
|
|
try testing.expectEqualStrings(bytes, reexported);
|
|
|
|
var invalid: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&invalid,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(invalid);
|
|
try testEnableContinuation(invalid);
|
|
try testing.expectError(
|
|
error.InvalidContinuation,
|
|
restoreContinuation(invalid, "A"),
|
|
);
|
|
}
|
|
|
|
test "set scrollback limits" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
3,
|
|
));
|
|
defer free(t);
|
|
|
|
const primary = t.?.terminal.screens.get(.primary).?;
|
|
|
|
const max_bytes: usize = 0;
|
|
try testing.expectEqual(
|
|
Result.success,
|
|
set(t, .scrollback_max_bytes, &max_bytes),
|
|
);
|
|
try testing.expectEqual(
|
|
@as(usize, 0),
|
|
primary.pages.limits.bytes.explicit,
|
|
);
|
|
try testing.expect(primary.no_scrollback);
|
|
|
|
try testing.expectEqual(
|
|
Result.success,
|
|
set(t, .scrollback_max_bytes, null),
|
|
);
|
|
try testing.expectEqual(
|
|
std.math.maxInt(usize),
|
|
primary.pages.limits.bytes.explicit,
|
|
);
|
|
try testing.expect(!primary.no_scrollback);
|
|
|
|
const max_lines: usize = 12;
|
|
try testing.expectEqual(
|
|
Result.success,
|
|
set(t, .scrollback_max_lines, &max_lines),
|
|
);
|
|
try testing.expectEqual(
|
|
max_lines,
|
|
primary.pages.limits.lines.explicit,
|
|
);
|
|
|
|
try testing.expectEqual(
|
|
Result.success,
|
|
set(t, .scrollback_max_lines, null),
|
|
);
|
|
try testing.expectEqual(
|
|
std.math.maxInt(usize),
|
|
primary.pages.limits.lines.explicit,
|
|
);
|
|
}
|
|
|
|
test "free null" {
|
|
free(null);
|
|
}
|
|
|
|
test "scroll_viewport" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
5,
|
|
2,
|
|
));
|
|
defer free(t);
|
|
|
|
const zt = t.?.terminal;
|
|
|
|
var viewport_active: bool = false;
|
|
try testing.expectEqual(Result.success, get(t, .viewport_active, @ptrCast(&viewport_active)));
|
|
try testing.expect(viewport_active);
|
|
|
|
// Write "hello" on the first line
|
|
vt_write(t, "hello", 5);
|
|
|
|
// Push "hello" into scrollback with 3 newlines (index = ESC D)
|
|
vt_write(t, "\x1bD\x1bD\x1bD", 6);
|
|
{
|
|
// Viewport should be empty now since hello scrolled off
|
|
const str = try zt.plainString(testing.allocator);
|
|
defer testing.allocator.free(str);
|
|
try testing.expectEqualStrings("", str);
|
|
}
|
|
|
|
// Scroll to top: "hello" should be visible again
|
|
scroll_viewport(t, .{ .tag = .top, .value = undefined });
|
|
try testing.expectEqual(Result.success, get(t, .viewport_active, @ptrCast(&viewport_active)));
|
|
try testing.expect(!viewport_active);
|
|
{
|
|
const str = try zt.plainString(testing.allocator);
|
|
defer testing.allocator.free(str);
|
|
try testing.expectEqualStrings("hello", str);
|
|
}
|
|
|
|
// Scroll to bottom: viewport should be empty again
|
|
scroll_viewport(t, .{ .tag = .bottom, .value = undefined });
|
|
try testing.expectEqual(Result.success, get(t, .viewport_active, @ptrCast(&viewport_active)));
|
|
try testing.expect(viewport_active);
|
|
{
|
|
const str = try zt.plainString(testing.allocator);
|
|
defer testing.allocator.free(str);
|
|
try testing.expectEqualStrings("", str);
|
|
}
|
|
|
|
// Scroll up by delta to bring "hello" back into view
|
|
scroll_viewport(t, .{ .tag = .delta, .value = .{ .delta = -3 } });
|
|
try testing.expectEqual(Result.success, get(t, .viewport_active, @ptrCast(&viewport_active)));
|
|
try testing.expect(!viewport_active);
|
|
{
|
|
const str = try zt.plainString(testing.allocator);
|
|
defer testing.allocator.free(str);
|
|
try testing.expectEqualStrings("hello", str);
|
|
}
|
|
}
|
|
|
|
test "scroll_viewport row" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
5,
|
|
2,
|
|
));
|
|
defer free(t);
|
|
|
|
const zt = t.?.terminal;
|
|
|
|
// Write 4 rows so that rows "1" and "2" are pushed into scrollback:
|
|
// total rows is 4, viewport length is 2.
|
|
vt_write(t, "1\r\n2\r\n3\r\n4", 10);
|
|
|
|
var viewport_active: bool = false;
|
|
try testing.expectEqual(Result.success, get(t, .viewport_active, @ptrCast(&viewport_active)));
|
|
try testing.expect(viewport_active);
|
|
|
|
// Row 0 is the top of the scrollback.
|
|
scroll_viewport(t, .{ .tag = .row, .value = .{ .row = 0 } });
|
|
try testing.expectEqual(Result.success, get(t, .viewport_active, @ptrCast(&viewport_active)));
|
|
try testing.expect(!viewport_active);
|
|
{
|
|
const str = try zt.plainString(testing.allocator);
|
|
defer testing.allocator.free(str);
|
|
try testing.expectEqualStrings("1\n2", str);
|
|
}
|
|
|
|
// An absolute row within the scrollback becomes the first visible
|
|
// row and round-trips through the scrollbar offset.
|
|
scroll_viewport(t, .{ .tag = .row, .value = .{ .row = 1 } });
|
|
{
|
|
const str = try zt.plainString(testing.allocator);
|
|
defer testing.allocator.free(str);
|
|
try testing.expectEqualStrings("2\n3", str);
|
|
}
|
|
var scrollbar_data: TerminalScrollbar = undefined;
|
|
try testing.expectEqual(Result.success, get(t, .scrollbar, @ptrCast(&scrollbar_data)));
|
|
try testing.expectEqual(@as(u64, 4), scrollbar_data.total);
|
|
try testing.expectEqual(@as(u64, 1), scrollbar_data.offset);
|
|
try testing.expectEqual(@as(u64, 2), scrollbar_data.len);
|
|
|
|
// A row past the end clamps to the active area.
|
|
scroll_viewport(t, .{ .tag = .row, .value = .{ .row = 9999 } });
|
|
try testing.expectEqual(Result.success, get(t, .viewport_active, @ptrCast(&viewport_active)));
|
|
try testing.expect(viewport_active);
|
|
{
|
|
const str = try zt.plainString(testing.allocator);
|
|
defer testing.allocator.free(str);
|
|
try testing.expectEqualStrings("3\n4", str);
|
|
}
|
|
try testing.expectEqual(Result.success, get(t, .scrollbar, @ptrCast(&scrollbar_data)));
|
|
try testing.expectEqual(@as(u64, 2), scrollbar_data.offset);
|
|
}
|
|
|
|
test "scroll_viewport row alt screen" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
5,
|
|
2,
|
|
));
|
|
defer free(t);
|
|
|
|
// Enter the alternate screen, which has no scrollback.
|
|
vt_write(t, "\x1b[?1049h", 8);
|
|
var screen: TerminalScreen = undefined;
|
|
try testing.expectEqual(Result.success, get(t, .active_screen, @ptrCast(&screen)));
|
|
try testing.expectEqual(TerminalScreen.alternate, screen);
|
|
|
|
// Scrolling to any row keeps the viewport on the active area.
|
|
var viewport_active: bool = false;
|
|
scroll_viewport(t, .{ .tag = .row, .value = .{ .row = 0 } });
|
|
try testing.expectEqual(Result.success, get(t, .viewport_active, @ptrCast(&viewport_active)));
|
|
try testing.expect(viewport_active);
|
|
scroll_viewport(t, .{ .tag = .row, .value = .{ .row = 9999 } });
|
|
try testing.expectEqual(Result.success, get(t, .viewport_active, @ptrCast(&viewport_active)));
|
|
try testing.expect(viewport_active);
|
|
|
|
// With no scrollback the scrollbar covers exactly the active area.
|
|
var scrollbar_data: TerminalScrollbar = undefined;
|
|
try testing.expectEqual(Result.success, get(t, .scrollbar, @ptrCast(&scrollbar_data)));
|
|
try testing.expectEqual(@as(u64, 2), scrollbar_data.total);
|
|
try testing.expectEqual(@as(u64, 0), scrollbar_data.offset);
|
|
try testing.expectEqual(@as(u64, 2), scrollbar_data.len);
|
|
}
|
|
|
|
test "scroll_viewport null" {
|
|
scroll_viewport(null, .{ .tag = .top, .value = undefined });
|
|
scroll_viewport(null, .{ .tag = .row, .value = .{ .row = 1 } });
|
|
}
|
|
|
|
test "compression invalid arguments" {
|
|
var activity: u64 = undefined;
|
|
var compression_result: CompressionResult = undefined;
|
|
|
|
try testing.expectEqual(
|
|
Result.invalid_value,
|
|
compression_activity(null, &activity),
|
|
);
|
|
try testing.expectEqual(
|
|
Result.invalid_value,
|
|
compress(null, @intFromEnum(CompressionMode.incremental), &compression_result),
|
|
);
|
|
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
try testing.expectEqual(
|
|
Result.invalid_value,
|
|
compression_activity(t, null),
|
|
);
|
|
try testing.expectEqual(
|
|
Result.invalid_value,
|
|
compress(t, @intFromEnum(CompressionMode.incremental), null),
|
|
);
|
|
try testing.expectEqual(
|
|
Result.invalid_value,
|
|
compress(t, -1, &compression_result),
|
|
);
|
|
}
|
|
|
|
test "compression activity and incremental scheduling" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
var initial_activity: u64 = undefined;
|
|
try testing.expectEqual(
|
|
Result.success,
|
|
compression_activity(t, &initial_activity),
|
|
);
|
|
|
|
const line = "repeated and compressible terminal history\r\n";
|
|
const repeat = 4_000;
|
|
const input = try testing.allocator.alloc(u8, line.len * repeat);
|
|
defer testing.allocator.free(input);
|
|
for (0..repeat) |i|
|
|
@memcpy(input[i * line.len ..][0..line.len], line);
|
|
vt_write(t, input.ptr, input.len);
|
|
|
|
var activity: u64 = undefined;
|
|
try testing.expectEqual(
|
|
Result.success,
|
|
compression_activity(t, &activity),
|
|
);
|
|
try testing.expect(activity != initial_activity);
|
|
|
|
var compression_result: CompressionResult = undefined;
|
|
for (0..1_000) |_| {
|
|
try testing.expectEqual(
|
|
Result.success,
|
|
compress(
|
|
t,
|
|
@intFromEnum(CompressionMode.incremental),
|
|
&compression_result,
|
|
),
|
|
);
|
|
|
|
switch (compression_result) {
|
|
.pending => continue,
|
|
.complete => break,
|
|
.unsupported => unreachable,
|
|
}
|
|
} else return error.TestUnexpectedResult;
|
|
|
|
// Compression changes storage representation, not the activity token.
|
|
var final_activity: u64 = undefined;
|
|
try testing.expectEqual(
|
|
Result.success,
|
|
compression_activity(t, &final_activity),
|
|
);
|
|
try testing.expectEqual(activity, final_activity);
|
|
|
|
try testing.expectEqual(
|
|
Result.success,
|
|
compress(t, @intFromEnum(CompressionMode.full), &compression_result),
|
|
);
|
|
try testing.expectEqual(CompressionResult.complete, compression_result);
|
|
}
|
|
|
|
test "reset" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
vt_write(t, "Hello", 5);
|
|
reset(t);
|
|
|
|
const str = try t.?.terminal.plainString(testing.allocator);
|
|
defer testing.allocator.free(str);
|
|
try testing.expectEqualStrings("", str);
|
|
}
|
|
|
|
test "reset null" {
|
|
reset(null);
|
|
}
|
|
|
|
test "resize" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
try testing.expectEqual(Result.success, resize(t, 40, 12, 9, 18));
|
|
try testing.expectEqual(40, t.?.terminal.cols);
|
|
try testing.expectEqual(12, t.?.terminal.rows);
|
|
}
|
|
|
|
test "resize null" {
|
|
try testing.expectEqual(Result.invalid_value, resize(null, 80, 24, 9, 18));
|
|
}
|
|
|
|
test "resize invalid value" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
try testing.expectEqual(Result.invalid_value, resize(t, 0, 24, 9, 18));
|
|
try testing.expectEqual(Result.invalid_value, resize(t, 80, 0, 9, 18));
|
|
}
|
|
|
|
test "resize shrinks both axes with cursor at bottom" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
// CSI 24;1H -> park the cursor on the bottom row (1-based).
|
|
const move = "\x1b[24;1H";
|
|
vt_write(t, move, move.len);
|
|
|
|
// Shrink both axes; pre-resize cursor.y sits past the new bottom row.
|
|
// Previously this underflowed in PageList.resizeCols.
|
|
try testing.expectEqual(Result.success, resize(t, 79, 23, 8, 16));
|
|
try testing.expectEqual(79, t.?.terminal.cols);
|
|
try testing.expectEqual(23, t.?.terminal.rows);
|
|
}
|
|
|
|
test "set and get mode" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
// DEC mode 25 (cursor_visible) defaults to true
|
|
const cursor_visible: modes.ModeTag.Backing = @bitCast(modes.ModeTag{ .value = 25, .ansi = false });
|
|
var config: ModeConfig = .{ .mode = cursor_visible, .value = undefined };
|
|
try testing.expectEqual(Result.success, get(t, .mode, @ptrCast(&config)));
|
|
try testing.expect(config.value);
|
|
|
|
// Set it to false
|
|
config.value = false;
|
|
try testing.expectEqual(Result.success, set(t, .mode, @ptrCast(&config)));
|
|
try testing.expectEqual(Result.success, get(t, .mode, @ptrCast(&config)));
|
|
try testing.expect(!config.value);
|
|
|
|
// ANSI mode 4 (insert) defaults to false
|
|
const insert: modes.ModeTag.Backing = @bitCast(modes.ModeTag{ .value = 4, .ansi = true });
|
|
config.mode = insert;
|
|
try testing.expectEqual(Result.success, get(t, .mode, @ptrCast(&config)));
|
|
try testing.expect(!config.value);
|
|
|
|
config.value = true;
|
|
try testing.expectEqual(Result.success, set(t, .mode, @ptrCast(&config)));
|
|
try testing.expectEqual(Result.success, get(t, .mode, @ptrCast(&config)));
|
|
try testing.expect(config.value);
|
|
}
|
|
|
|
test "set mode default updates current and reset value" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
const tag: modes.ModeTag.Backing = @bitCast(modes.ModeTag{
|
|
.value = 2027,
|
|
.ansi = false,
|
|
});
|
|
var config: ModeConfig = .{ .mode = tag, .value = true };
|
|
try testing.expectEqual(Result.success, set(
|
|
t,
|
|
.mode_default,
|
|
@ptrCast(&config),
|
|
));
|
|
try testing.expect(t.?.terminal.modes.get(.grapheme_cluster));
|
|
try testing.expect(t.?.terminal.modes.default.grapheme_cluster);
|
|
|
|
config.value = false;
|
|
try testing.expectEqual(Result.success, set(t, .mode, @ptrCast(&config)));
|
|
try testing.expect(!t.?.terminal.modes.get(.grapheme_cluster));
|
|
try testing.expect(t.?.terminal.modes.default.grapheme_cluster);
|
|
|
|
// Setting the default also unconditionally replaces the current value.
|
|
config.value = true;
|
|
try testing.expectEqual(Result.success, set(
|
|
t,
|
|
.mode_default,
|
|
@ptrCast(&config),
|
|
));
|
|
try testing.expect(t.?.terminal.modes.get(.grapheme_cluster));
|
|
|
|
config.value = false;
|
|
try testing.expectEqual(Result.success, set(t, .mode, @ptrCast(&config)));
|
|
vt_write(t, "\x1bc", 2);
|
|
try testing.expect(t.?.terminal.modes.get(.grapheme_cluster));
|
|
|
|
config.value = false;
|
|
try testing.expectEqual(Result.success, set(
|
|
t,
|
|
.mode_default,
|
|
@ptrCast(&config),
|
|
));
|
|
try testing.expect(!t.?.terminal.modes.get(.grapheme_cluster));
|
|
try testing.expect(!t.?.terminal.modes.default.grapheme_cluster);
|
|
}
|
|
|
|
test "set mode default validates configuration" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
try testing.expectEqual(Result.invalid_value, set(
|
|
t,
|
|
.mode_default,
|
|
null,
|
|
));
|
|
|
|
var config: ModeConfig = .{
|
|
.mode = @bitCast(modes.ModeTag{ .value = 9999, .ansi = false }),
|
|
.value = true,
|
|
};
|
|
try testing.expectEqual(Result.invalid_value, set(
|
|
t,
|
|
.mode_default,
|
|
@ptrCast(&config),
|
|
));
|
|
|
|
config.mode = @bitCast(modes.ModeTag{ .value = 1047, .ansi = false });
|
|
try testing.expectEqual(Result.invalid_value, set(
|
|
t,
|
|
.mode_default,
|
|
@ptrCast(&config),
|
|
));
|
|
}
|
|
|
|
test "set and get mode null" {
|
|
const tag: modes.ModeTag.Backing = @bitCast(modes.ModeTag{ .value = 25, .ansi = false });
|
|
var config: ModeConfig = .{ .mode = tag, .value = true };
|
|
try testing.expectEqual(Result.invalid_value, set(null, .mode, @ptrCast(&config)));
|
|
try testing.expectEqual(Result.invalid_value, get(null, .mode, @ptrCast(&config)));
|
|
}
|
|
|
|
test "set and get mode validate configuration" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
try testing.expectEqual(Result.invalid_value, set(t, .mode, null));
|
|
try testing.expectEqual(Result.invalid_value, get(t, .mode, null));
|
|
|
|
var config: ModeConfig = .{
|
|
.mode = @bitCast(modes.ModeTag{ .value = 9999, .ansi = false }),
|
|
.value = true,
|
|
};
|
|
try testing.expectEqual(Result.invalid_value, set(t, .mode, @ptrCast(&config)));
|
|
try testing.expectEqual(Result.invalid_value, get(t, .mode, @ptrCast(&config)));
|
|
}
|
|
|
|
test "vt_write" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
vt_write(t, "Hello", 5);
|
|
|
|
const str = try t.?.terminal.plainString(testing.allocator);
|
|
defer testing.allocator.free(str);
|
|
try testing.expectEqualStrings("Hello", str);
|
|
}
|
|
|
|
test "vt_write split escape sequence" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
// Write "Hello" in bold by splitting the CSI bold sequence across two writes.
|
|
// ESC [ 1 m = bold on, ESC [ 0 m = reset
|
|
// Split ESC from the rest of the CSI sequence.
|
|
vt_write(t, "Hello \x1b", 7);
|
|
vt_write(t, "[1mBold\x1b[0m", 10);
|
|
|
|
const str = try t.?.terminal.plainString(testing.allocator);
|
|
defer testing.allocator.free(str);
|
|
// If the escape sequence leaked, we'd see "[1mBold" as literal text.
|
|
try testing.expectEqualStrings("Hello Bold", str);
|
|
}
|
|
|
|
test "vt_write split combining mark after base at right edge" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
2,
|
|
2,
|
|
));
|
|
defer free(t);
|
|
|
|
// Put "å" in the final column, then send its combining low line in a
|
|
// separate write so the mark arrives while the cursor has a pending wrap.
|
|
vt_write(t, "xå", 3);
|
|
vt_write(t, "\xcc\xb2", 2);
|
|
|
|
const str = try t.?.terminal.plainString(testing.allocator);
|
|
defer testing.allocator.free(str);
|
|
try testing.expectEqualStrings("xå̲", str);
|
|
}
|
|
|
|
test "get cols and rows" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
var cols: size.CellCountInt = undefined;
|
|
var rows: size.CellCountInt = undefined;
|
|
try testing.expectEqual(Result.success, get(t, .cols, @ptrCast(&cols)));
|
|
try testing.expectEqual(Result.success, get(t, .rows, @ptrCast(&rows)));
|
|
try testing.expectEqual(80, cols);
|
|
try testing.expectEqual(24, rows);
|
|
}
|
|
|
|
test "get cursor position" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
vt_write(t, "Hello", 5);
|
|
|
|
var x: size.CellCountInt = undefined;
|
|
var y: size.CellCountInt = undefined;
|
|
try testing.expectEqual(Result.success, get(t, .cursor_x, @ptrCast(&x)));
|
|
try testing.expectEqual(Result.success, get(t, .cursor_y, @ptrCast(&y)));
|
|
try testing.expectEqual(5, x);
|
|
try testing.expectEqual(0, y);
|
|
}
|
|
|
|
test "get vt_processing_error" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
var processing_error: bool = true;
|
|
try testing.expectEqual(Result.success, get(
|
|
t,
|
|
.vt_processing_error,
|
|
@ptrCast(&processing_error),
|
|
));
|
|
try testing.expect(!processing_error);
|
|
|
|
// Force a non-graceful terminal-owned update failure through the public
|
|
// VT write path.
|
|
{
|
|
const alloc = t.?.terminal.screens.active.alloc;
|
|
t.?.terminal.screens.active.alloc = testing.failing_allocator;
|
|
defer t.?.terminal.screens.active.alloc = alloc;
|
|
|
|
const input = "\x1B]2;unavailable\x1B\\";
|
|
vt_write(t, input, input.len);
|
|
}
|
|
|
|
try testing.expectEqual(Result.success, get(
|
|
t,
|
|
.vt_processing_error,
|
|
@ptrCast(&processing_error),
|
|
));
|
|
try testing.expect(processing_error);
|
|
|
|
reset(t);
|
|
try testing.expectEqual(Result.success, get(
|
|
t,
|
|
.vt_processing_error,
|
|
@ptrCast(&processing_error),
|
|
));
|
|
try testing.expect(processing_error);
|
|
}
|
|
|
|
test "get null" {
|
|
var cols: size.CellCountInt = undefined;
|
|
try testing.expectEqual(Result.invalid_value, get(null, .cols, @ptrCast(&cols)));
|
|
}
|
|
|
|
test "get cursor_visible" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
var visible: bool = undefined;
|
|
try testing.expectEqual(Result.success, get(t, .cursor_visible, @ptrCast(&visible)));
|
|
try testing.expect(visible);
|
|
|
|
// DEC mode 25 controls cursor visibility
|
|
var config: ModeConfig = .{
|
|
.mode = @bitCast(modes.ModeTag{ .value = 25, .ansi = false }),
|
|
.value = false,
|
|
};
|
|
try testing.expectEqual(Result.success, set(t, .mode, @ptrCast(&config)));
|
|
try testing.expectEqual(Result.success, get(t, .cursor_visible, @ptrCast(&visible)));
|
|
try testing.expect(!visible);
|
|
}
|
|
|
|
test "get active_screen" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
var screen: TerminalScreen = undefined;
|
|
try testing.expectEqual(Result.success, get(t, .active_screen, @ptrCast(&screen)));
|
|
try testing.expectEqual(.primary, screen);
|
|
}
|
|
|
|
test "get kitty_keyboard_flags" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
var flags: u8 = undefined;
|
|
try testing.expectEqual(Result.success, get(t, .kitty_keyboard_flags, @ptrCast(&flags)));
|
|
try testing.expectEqual(0, flags);
|
|
|
|
// Push kitty flags via VT sequence: CSI > 3 u (push disambiguate | report_events)
|
|
vt_write(t, "\x1b[>3u", 5);
|
|
|
|
try testing.expectEqual(Result.success, get(t, .kitty_keyboard_flags, @ptrCast(&flags)));
|
|
try testing.expectEqual(3, flags);
|
|
}
|
|
|
|
test "get mouse_tracking" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
var tracking: bool = undefined;
|
|
try testing.expectEqual(Result.success, get(t, .mouse_tracking, @ptrCast(&tracking)));
|
|
try testing.expect(!tracking);
|
|
|
|
// Enable X10 mouse (DEC mode 9)
|
|
var config: ModeConfig = .{
|
|
.mode = @bitCast(modes.ModeTag{ .value = 9, .ansi = false }),
|
|
.value = true,
|
|
};
|
|
try testing.expectEqual(Result.success, set(t, .mode, @ptrCast(&config)));
|
|
try testing.expectEqual(Result.success, get(t, .mouse_tracking, @ptrCast(&tracking)));
|
|
try testing.expect(tracking);
|
|
|
|
// Disable X10, enable normal mouse (DEC mode 1000)
|
|
config.value = false;
|
|
try testing.expectEqual(Result.success, set(t, .mode, @ptrCast(&config)));
|
|
config = .{
|
|
.mode = @bitCast(modes.ModeTag{ .value = 1000, .ansi = false }),
|
|
.value = true,
|
|
};
|
|
try testing.expectEqual(Result.success, set(t, .mode, @ptrCast(&config)));
|
|
try testing.expectEqual(Result.success, get(t, .mouse_tracking, @ptrCast(&tracking)));
|
|
try testing.expect(tracking);
|
|
|
|
// Disable normal, enable button mouse (DEC mode 1002)
|
|
config.value = false;
|
|
try testing.expectEqual(Result.success, set(t, .mode, @ptrCast(&config)));
|
|
config = .{
|
|
.mode = @bitCast(modes.ModeTag{ .value = 1002, .ansi = false }),
|
|
.value = true,
|
|
};
|
|
try testing.expectEqual(Result.success, set(t, .mode, @ptrCast(&config)));
|
|
try testing.expectEqual(Result.success, get(t, .mouse_tracking, @ptrCast(&tracking)));
|
|
try testing.expect(tracking);
|
|
|
|
// Disable button, enable any mouse (DEC mode 1003)
|
|
config.value = false;
|
|
try testing.expectEqual(Result.success, set(t, .mode, @ptrCast(&config)));
|
|
config = .{
|
|
.mode = @bitCast(modes.ModeTag{ .value = 1003, .ansi = false }),
|
|
.value = true,
|
|
};
|
|
try testing.expectEqual(Result.success, set(t, .mode, @ptrCast(&config)));
|
|
try testing.expectEqual(Result.success, get(t, .mouse_tracking, @ptrCast(&tracking)));
|
|
try testing.expect(tracking);
|
|
|
|
// Disable all - should be false again
|
|
config.value = false;
|
|
try testing.expectEqual(Result.success, set(t, .mode, @ptrCast(&config)));
|
|
try testing.expectEqual(Result.success, get(t, .mouse_tracking, @ptrCast(&tracking)));
|
|
try testing.expect(!tracking);
|
|
}
|
|
|
|
test "get total_rows" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
var total: usize = undefined;
|
|
try testing.expectEqual(Result.success, get(t, .total_rows, @ptrCast(&total)));
|
|
try testing.expect(total >= 24);
|
|
}
|
|
|
|
test "get scrollback_rows" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
3,
|
|
));
|
|
defer free(t);
|
|
|
|
var scrollback: usize = undefined;
|
|
try testing.expectEqual(Result.success, get(t, .scrollback_rows, @ptrCast(&scrollback)));
|
|
try testing.expectEqual(@as(usize, 0), scrollback);
|
|
|
|
// Write enough lines to push content into scrollback
|
|
vt_write(t, "line1\r\nline2\r\nline3\r\nline4\r\nline5\r\n", 34);
|
|
|
|
try testing.expectEqual(Result.success, get(t, .scrollback_rows, @ptrCast(&scrollback)));
|
|
try testing.expectEqual(@as(usize, 2), scrollback);
|
|
}
|
|
|
|
test "get configured scrollback limits" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
3,
|
|
));
|
|
defer free(t);
|
|
|
|
var value: usize = undefined;
|
|
try testing.expectEqual(
|
|
Result.success,
|
|
get(t, .scrollback_max_bytes, @ptrCast(&value)),
|
|
);
|
|
try testing.expectEqual(@as(usize, 10_000), value);
|
|
try testing.expectEqual(
|
|
Result.no_value,
|
|
get(t, .scrollback_max_lines, @ptrCast(&value)),
|
|
);
|
|
|
|
const max_bytes: usize = 0;
|
|
const max_lines: usize = 12;
|
|
try testing.expectEqual(
|
|
Result.success,
|
|
set(t, .scrollback_max_bytes, &max_bytes),
|
|
);
|
|
try testing.expectEqual(
|
|
Result.success,
|
|
set(t, .scrollback_max_lines, &max_lines),
|
|
);
|
|
try testing.expectEqual(
|
|
Result.success,
|
|
get(t, .scrollback_max_bytes, @ptrCast(&value)),
|
|
);
|
|
try testing.expectEqual(max_bytes, value);
|
|
try testing.expectEqual(
|
|
Result.success,
|
|
get(t, .scrollback_max_lines, @ptrCast(&value)),
|
|
);
|
|
try testing.expectEqual(max_lines, value);
|
|
|
|
// The configured limits belong to the primary screen and remain
|
|
// readable while an alternate screen is active.
|
|
vt_write(t, "\x1b[?1049h", 8);
|
|
try testing.expectEqual(
|
|
Result.success,
|
|
get(t, .scrollback_max_bytes, @ptrCast(&value)),
|
|
);
|
|
try testing.expectEqual(max_bytes, value);
|
|
try testing.expectEqual(
|
|
Result.success,
|
|
get(t, .scrollback_max_lines, @ptrCast(&value)),
|
|
);
|
|
try testing.expectEqual(max_lines, value);
|
|
|
|
try testing.expectEqual(
|
|
Result.success,
|
|
set(t, .scrollback_max_bytes, null),
|
|
);
|
|
try testing.expectEqual(
|
|
Result.success,
|
|
set(t, .scrollback_max_lines, null),
|
|
);
|
|
try testing.expectEqual(
|
|
Result.no_value,
|
|
get(t, .scrollback_max_bytes, @ptrCast(&value)),
|
|
);
|
|
try testing.expectEqual(
|
|
Result.no_value,
|
|
get(t, .scrollback_max_lines, @ptrCast(&value)),
|
|
);
|
|
}
|
|
|
|
test "get invalid" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
try testing.expectEqual(Result.invalid_value, get(t, .invalid, null));
|
|
}
|
|
|
|
test "set default cursor style and blink" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
var default_style: TerminalCursorStyle = .bar;
|
|
var default_blink = true;
|
|
try testing.expectEqual(Result.success, set(t, .default_cursor_style, @ptrCast(&default_style)));
|
|
try testing.expectEqual(Result.success, set(t, .default_cursor_blink, @ptrCast(&default_blink)));
|
|
|
|
// Setting defaults applies them immediately while the cursor is still default.
|
|
try testing.expectEqual(Screen.CursorStyle.bar, t.?.terminal.screens.active.cursor.cursor_style);
|
|
try testing.expect(t.?.terminal.modes.get(.cursor_blinking));
|
|
|
|
// An explicit DECSCUSR style overrides the configured defaults.
|
|
vt_write(t, "\x1b[2 q", 5);
|
|
try testing.expectEqual(Screen.CursorStyle.block, t.?.terminal.screens.active.cursor.cursor_style);
|
|
try testing.expect(!t.?.terminal.modes.get(.cursor_blinking));
|
|
|
|
// Changing defaults does not override an explicit cursor style.
|
|
default_style = .underline;
|
|
try testing.expectEqual(Result.success, set(t, .default_cursor_style, @ptrCast(&default_style)));
|
|
try testing.expectEqual(Screen.CursorStyle.block, t.?.terminal.screens.active.cursor.cursor_style);
|
|
try testing.expect(!t.?.terminal.modes.get(.cursor_blinking));
|
|
|
|
// DECSCUSR reset restores the configured default style and blink.
|
|
vt_write(t, "\x1b[0 q", 5);
|
|
try testing.expectEqual(Screen.CursorStyle.underline, t.?.terminal.screens.active.cursor.cursor_style);
|
|
try testing.expect(t.?.terminal.modes.get(.cursor_blinking));
|
|
|
|
// RIS also restores cursor defaults from Terminal-owned state.
|
|
vt_write(t, "\x1b[2 q", 5);
|
|
reset(t);
|
|
try testing.expectEqual(Screen.CursorStyle.underline, t.?.terminal.screens.active.cursor.cursor_style);
|
|
try testing.expect(t.?.terminal.modes.get(.cursor_blinking));
|
|
}
|
|
|
|
test "set and get selection" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
vt_write(t, "Hello", 5);
|
|
|
|
var start_ref: grid_ref_c.CGridRef = .{};
|
|
try testing.expectEqual(Result.success, grid_ref(t, .{
|
|
.tag = .active,
|
|
.value = .{ .active = .{ .x = 0, .y = 0 } },
|
|
}, &start_ref));
|
|
|
|
var end_ref: grid_ref_c.CGridRef = .{};
|
|
try testing.expectEqual(Result.success, grid_ref(t, .{
|
|
.tag = .active,
|
|
.value = .{ .active = .{ .x = 4, .y = 0 } },
|
|
}, &end_ref));
|
|
|
|
var out: selection_c.CSelection = undefined;
|
|
try testing.expectEqual(Result.no_value, get(t, .selection, @ptrCast(&out)));
|
|
|
|
const sel: selection_c.CSelection = .{
|
|
.start = start_ref,
|
|
.end = end_ref,
|
|
.rectangle = true,
|
|
};
|
|
try testing.expectEqual(Result.success, set(t, .selection, @ptrCast(&sel)));
|
|
try testing.expect(t.?.terminal.screens.active.selection.?.tracked());
|
|
|
|
try testing.expectEqual(Result.success, get(t, .selection, @ptrCast(&out)));
|
|
try testing.expect(out.start.toPin().?.eql(start_ref.toPin().?));
|
|
try testing.expect(out.end.toPin().?.eql(end_ref.toPin().?));
|
|
try testing.expect(out.rectangle);
|
|
|
|
try testing.expectEqual(Result.success, set(t, .selection, null));
|
|
try testing.expect(t.?.terminal.screens.active.selection == null);
|
|
try testing.expectEqual(Result.no_value, get(t, .selection, @ptrCast(&out)));
|
|
}
|
|
|
|
test "selection derivation helpers" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
vt_write(t, " Hello \r\nWorld", 16);
|
|
|
|
var out: selection_c.CSelection = undefined;
|
|
|
|
var word_ref: grid_ref_c.CGridRef = .{};
|
|
try testing.expectEqual(Result.success, grid_ref(t, .{
|
|
.tag = .active,
|
|
.value = .{ .active = .{ .x = 3, .y = 0 } },
|
|
}, &word_ref));
|
|
|
|
var empty_ref: grid_ref_c.CGridRef = .{};
|
|
try testing.expectEqual(Result.success, grid_ref(t, .{
|
|
.tag = .active,
|
|
.value = .{ .active = .{ .x = 20, .y = 0 } },
|
|
}, &empty_ref));
|
|
|
|
var line_ref: grid_ref_c.CGridRef = .{};
|
|
try testing.expectEqual(Result.success, grid_ref(t, .{
|
|
.tag = .active,
|
|
.value = .{ .active = .{ .x = 0, .y = 0 } },
|
|
}, &line_ref));
|
|
|
|
var word_opts: selection_c.SelectWordOptions = .{
|
|
.ref = word_ref,
|
|
};
|
|
try testing.expectEqual(Result.success, selection_c.word(t, &word_opts, &out));
|
|
try testing.expectEqual(@as(u16, 2), out.start.toPin().?.x);
|
|
try testing.expectEqual(@as(u16, 6), out.end.toPin().?.x);
|
|
|
|
word_opts.ref = empty_ref;
|
|
try testing.expectEqual(Result.no_value, selection_c.word(t, &word_opts, &out));
|
|
|
|
var between_start_ref: grid_ref_c.CGridRef = .{};
|
|
try testing.expectEqual(Result.success, grid_ref(t, .{
|
|
.tag = .active,
|
|
.value = .{ .active = .{ .x = 20, .y = 1 } },
|
|
}, &between_start_ref));
|
|
|
|
var between_end_ref: grid_ref_c.CGridRef = .{};
|
|
try testing.expectEqual(Result.success, grid_ref(t, .{
|
|
.tag = .active,
|
|
.value = .{ .active = .{ .x = 0, .y = 1 } },
|
|
}, &between_end_ref));
|
|
|
|
var word_between_opts: selection_c.SelectWordBetweenOptions = .{
|
|
.start = between_start_ref,
|
|
.end = between_end_ref,
|
|
};
|
|
try testing.expectEqual(Result.success, selection_c.word_between(t, &word_between_opts, &out));
|
|
try testing.expectEqual(@as(u16, 0), out.start.toPin().?.x);
|
|
try testing.expectEqual(@as(u16, 1), out.start.toPin().?.y);
|
|
try testing.expectEqual(@as(u16, 4), out.end.toPin().?.x);
|
|
try testing.expectEqual(@as(u16, 1), out.end.toPin().?.y);
|
|
|
|
var line_opts: selection_c.SelectLineOptions = .{
|
|
.ref = line_ref,
|
|
};
|
|
try testing.expectEqual(Result.success, selection_c.line(t, &line_opts, &out));
|
|
try testing.expectEqual(@as(u16, 2), out.start.toPin().?.x);
|
|
try testing.expectEqual(@as(u16, 6), out.end.toPin().?.x);
|
|
|
|
try testing.expectEqual(Result.success, selection_c.all(t, &out));
|
|
try testing.expectEqual(@as(u16, 2), out.start.toPin().?.x);
|
|
try testing.expectEqual(@as(u16, 0), out.start.toPin().?.y);
|
|
try testing.expectEqual(@as(u16, 4), out.end.toPin().?.x);
|
|
try testing.expectEqual(@as(u16, 1), out.end.toPin().?.y);
|
|
|
|
try testing.expectEqual(Result.no_value, selection_c.output(t, line_ref, &out));
|
|
|
|
line_opts.size = @sizeOf(usize) - 1;
|
|
try testing.expectEqual(Result.invalid_value, selection_c.line(t, &line_opts, &out));
|
|
try testing.expectEqual(Result.invalid_value, selection_c.word(t, null, &out));
|
|
try testing.expectEqual(Result.invalid_value, selection_c.word(t, &word_opts, null));
|
|
try testing.expectEqual(Result.invalid_value, selection_c.word_between(t, null, &out));
|
|
try testing.expectEqual(Result.invalid_value, selection_c.word_between(t, &word_between_opts, null));
|
|
}
|
|
|
|
test "selection_adjust mutates snapshot end" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
vt_write(t, "Hello", 5);
|
|
|
|
var start_ref: grid_ref_c.CGridRef = .{};
|
|
try testing.expectEqual(Result.success, grid_ref(t, .{
|
|
.tag = .active,
|
|
.value = .{ .active = .{ .x = 0, .y = 0 } },
|
|
}, &start_ref));
|
|
|
|
var end_ref: grid_ref_c.CGridRef = .{};
|
|
try testing.expectEqual(Result.success, grid_ref(t, .{
|
|
.tag = .active,
|
|
.value = .{ .active = .{ .x = 1, .y = 0 } },
|
|
}, &end_ref));
|
|
|
|
var sel: selection_c.CSelection = .{
|
|
.start = start_ref,
|
|
.end = end_ref,
|
|
};
|
|
try testing.expectEqual(Result.success, selection_c.adjust(t, &sel, .right));
|
|
try testing.expectEqual(@as(u16, 0), sel.start.toPin().?.x);
|
|
try testing.expectEqual(@as(u16, 2), sel.end.toPin().?.x);
|
|
|
|
try testing.expectEqual(Result.success, selection_c.adjust(t, &sel, .left));
|
|
try testing.expectEqual(@as(u16, 0), sel.start.toPin().?.x);
|
|
try testing.expectEqual(@as(u16, 1), sel.end.toPin().?.x);
|
|
|
|
sel = .{
|
|
.start = end_ref,
|
|
.end = start_ref,
|
|
};
|
|
try testing.expectEqual(Result.success, selection_c.adjust(t, &sel, .right));
|
|
try testing.expectEqual(@as(u16, 1), sel.start.toPin().?.x);
|
|
try testing.expectEqual(@as(u16, 1), sel.end.toPin().?.x);
|
|
}
|
|
|
|
test "selection_order and selection_ordered" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
vt_write(t, "Hello\r\nWorld", 12);
|
|
|
|
var start_ref: grid_ref_c.CGridRef = .{};
|
|
try testing.expectEqual(Result.success, grid_ref(t, .{
|
|
.tag = .active,
|
|
.value = .{ .active = .{ .x = 3, .y = 0 } },
|
|
}, &start_ref));
|
|
|
|
var end_ref: grid_ref_c.CGridRef = .{};
|
|
try testing.expectEqual(Result.success, grid_ref(t, .{
|
|
.tag = .active,
|
|
.value = .{ .active = .{ .x = 1, .y = 1 } },
|
|
}, &end_ref));
|
|
|
|
const sel: selection_c.CSelection = .{
|
|
.start = start_ref,
|
|
.end = end_ref,
|
|
.rectangle = true,
|
|
};
|
|
|
|
var order: selection_c.Order = undefined;
|
|
try testing.expectEqual(Result.success, selection_c.order(t, &sel, &order));
|
|
try testing.expectEqual(selection_c.Order.mirrored_forward, order);
|
|
|
|
var out: selection_c.CSelection = undefined;
|
|
try testing.expectEqual(Result.success, selection_c.ordered(t, &sel, .forward, &out));
|
|
try testing.expectEqual(@as(u16, 1), out.start.toPin().?.x);
|
|
try testing.expectEqual(@as(u16, 0), out.start.toPin().?.y);
|
|
try testing.expectEqual(@as(u16, 3), out.end.toPin().?.x);
|
|
try testing.expectEqual(@as(u16, 1), out.end.toPin().?.y);
|
|
try testing.expect(out.rectangle);
|
|
|
|
try testing.expectEqual(Result.success, selection_c.ordered(t, &sel, .reverse, &out));
|
|
try testing.expectEqual(@as(u16, 3), out.start.toPin().?.x);
|
|
try testing.expectEqual(@as(u16, 1), out.start.toPin().?.y);
|
|
try testing.expectEqual(@as(u16, 1), out.end.toPin().?.x);
|
|
try testing.expectEqual(@as(u16, 0), out.end.toPin().?.y);
|
|
try testing.expect(out.rectangle);
|
|
}
|
|
|
|
test "selection_contains" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
vt_write(t, "Hello\r\nWorld", 12);
|
|
|
|
var start_ref: grid_ref_c.CGridRef = .{};
|
|
try testing.expectEqual(Result.success, grid_ref(t, .{
|
|
.tag = .active,
|
|
.value = .{ .active = .{ .x = 3, .y = 0 } },
|
|
}, &start_ref));
|
|
|
|
var end_ref: grid_ref_c.CGridRef = .{};
|
|
try testing.expectEqual(Result.success, grid_ref(t, .{
|
|
.tag = .active,
|
|
.value = .{ .active = .{ .x = 1, .y = 1 } },
|
|
}, &end_ref));
|
|
|
|
const linear: selection_c.CSelection = .{
|
|
.start = start_ref,
|
|
.end = end_ref,
|
|
};
|
|
|
|
var contains: bool = undefined;
|
|
try testing.expectEqual(Result.success, selection_c.contains(t, &linear, .{
|
|
.tag = .active,
|
|
.value = .{ .active = .{ .x = 4, .y = 0 } },
|
|
}, &contains));
|
|
try testing.expect(contains);
|
|
|
|
try testing.expectEqual(Result.success, selection_c.contains(t, &linear, .{
|
|
.tag = .active,
|
|
.value = .{ .active = .{ .x = 2, .y = 0 } },
|
|
}, &contains));
|
|
try testing.expect(!contains);
|
|
|
|
const rectangle: selection_c.CSelection = .{
|
|
.start = start_ref,
|
|
.end = end_ref,
|
|
.rectangle = true,
|
|
};
|
|
|
|
try testing.expectEqual(Result.success, selection_c.contains(t, &rectangle, .{
|
|
.tag = .active,
|
|
.value = .{ .active = .{ .x = 2, .y = 0 } },
|
|
}, &contains));
|
|
try testing.expect(contains);
|
|
}
|
|
|
|
test "selection_equal" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
var other_t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&other_t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(other_t);
|
|
|
|
vt_write(t, "Hello", 5);
|
|
vt_write(other_t, "Hello", 5);
|
|
|
|
var start_ref: grid_ref_c.CGridRef = .{};
|
|
try testing.expectEqual(Result.success, grid_ref(t, .{
|
|
.tag = .active,
|
|
.value = .{ .active = .{ .x = 0, .y = 0 } },
|
|
}, &start_ref));
|
|
|
|
var end_ref: grid_ref_c.CGridRef = .{};
|
|
try testing.expectEqual(Result.success, grid_ref(t, .{
|
|
.tag = .active,
|
|
.value = .{ .active = .{ .x = 1, .y = 0 } },
|
|
}, &end_ref));
|
|
|
|
var other_end_ref: grid_ref_c.CGridRef = .{};
|
|
try testing.expectEqual(Result.success, grid_ref(t, .{
|
|
.tag = .active,
|
|
.value = .{ .active = .{ .x = 2, .y = 0 } },
|
|
}, &other_end_ref));
|
|
|
|
var cross_terminal_ref: grid_ref_c.CGridRef = .{};
|
|
try testing.expectEqual(Result.success, grid_ref(other_t, .{
|
|
.tag = .active,
|
|
.value = .{ .active = .{ .x = 1, .y = 0 } },
|
|
}, &cross_terminal_ref));
|
|
|
|
const sel: selection_c.CSelection = .{
|
|
.start = start_ref,
|
|
.end = end_ref,
|
|
};
|
|
const equal_sel: selection_c.CSelection = .{
|
|
.start = start_ref,
|
|
.end = end_ref,
|
|
};
|
|
const different_endpoint: selection_c.CSelection = .{
|
|
.start = start_ref,
|
|
.end = other_end_ref,
|
|
};
|
|
const different_rectangle: selection_c.CSelection = .{
|
|
.start = start_ref,
|
|
.end = end_ref,
|
|
.rectangle = true,
|
|
};
|
|
const cross_terminal: selection_c.CSelection = .{
|
|
.start = start_ref,
|
|
.end = cross_terminal_ref,
|
|
};
|
|
|
|
var equal: bool = undefined;
|
|
try testing.expectEqual(Result.success, selection_c.equal(t, &sel, &equal_sel, &equal));
|
|
try testing.expect(equal);
|
|
|
|
try testing.expectEqual(Result.success, selection_c.equal(t, &sel, &different_endpoint, &equal));
|
|
try testing.expect(!equal);
|
|
|
|
try testing.expectEqual(Result.success, selection_c.equal(t, &sel, &different_rectangle, &equal));
|
|
try testing.expect(!equal);
|
|
|
|
try testing.expectEqual(Result.success, selection_c.equal(t, &sel, &cross_terminal, &equal));
|
|
try testing.expect(!equal);
|
|
try testing.expectEqual(Result.invalid_value, selection_c.equal(t, &sel, &equal_sel, null));
|
|
}
|
|
|
|
test "selection_order invalid values" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
var order: selection_c.Order = undefined;
|
|
try testing.expectEqual(Result.invalid_value, selection_c.order(null, null, &order));
|
|
try testing.expectEqual(Result.invalid_value, selection_c.order(t, null, &order));
|
|
}
|
|
|
|
test "grid_ref" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
vt_write(t, "Hello", 5);
|
|
|
|
var out_ref: grid_ref_c.CGridRef = .{};
|
|
try testing.expectEqual(Result.success, grid_ref(t, .{
|
|
.tag = .active,
|
|
.value = .{ .active = .{ .x = 0, .y = 0 } },
|
|
}, &out_ref));
|
|
|
|
// Extract cell from grid ref and verify it contains 'H'
|
|
var out_cell: cell_c.CCell = undefined;
|
|
try testing.expectEqual(Result.success, grid_ref_c.grid_ref_cell(&out_ref, &out_cell));
|
|
|
|
var cp: u32 = 0;
|
|
try testing.expectEqual(Result.success, cell_c.get(out_cell, .codepoint, @ptrCast(&cp)));
|
|
try testing.expectEqual(@as(u32, 'H'), cp);
|
|
}
|
|
|
|
test "grid_ref null terminal" {
|
|
var out_ref: grid_ref_c.CGridRef = .{};
|
|
try testing.expectEqual(Result.invalid_value, grid_ref(null, .{
|
|
.tag = .active,
|
|
.value = .{ .active = .{ .x = 0, .y = 0 } },
|
|
}, &out_ref));
|
|
}
|
|
|
|
test "point_from_grid_ref roundtrip active" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
vt_write(t, "Hello", 5);
|
|
|
|
// Get a grid ref at (2, 0) in active coords
|
|
var ref: grid_ref_c.CGridRef = .{};
|
|
try testing.expectEqual(Result.success, grid_ref(t, .{
|
|
.tag = .active,
|
|
.value = .{ .active = .{ .x = 2, .y = 0 } },
|
|
}, &ref));
|
|
|
|
// Convert back to active coords
|
|
var coord: point.Coordinate = undefined;
|
|
try testing.expectEqual(Result.success, point_from_grid_ref(t, &ref, .active, &coord));
|
|
try testing.expectEqual(@as(size.CellCountInt, 2), coord.x);
|
|
try testing.expectEqual(@as(u32, 0), coord.y);
|
|
}
|
|
|
|
test "point_from_grid_ref roundtrip viewport" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
vt_write(t, "Hello", 5);
|
|
|
|
var ref: grid_ref_c.CGridRef = .{};
|
|
try testing.expectEqual(Result.success, grid_ref(t, .{
|
|
.tag = .viewport,
|
|
.value = .{ .viewport = .{ .x = 0, .y = 0 } },
|
|
}, &ref));
|
|
|
|
var coord: point.Coordinate = undefined;
|
|
try testing.expectEqual(Result.success, point_from_grid_ref(t, &ref, .viewport, &coord));
|
|
try testing.expectEqual(@as(size.CellCountInt, 0), coord.x);
|
|
try testing.expectEqual(@as(u32, 0), coord.y);
|
|
}
|
|
|
|
test "point_from_grid_ref history ref to active returns no_value" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
4,
|
|
));
|
|
defer free(t);
|
|
|
|
// Write enough lines to push content into scrollback
|
|
for (0..10) |_| {
|
|
vt_write(t, "line\n", 5);
|
|
}
|
|
|
|
// Get a ref to the first line (now in scrollback)
|
|
var ref: grid_ref_c.CGridRef = .{};
|
|
try testing.expectEqual(Result.success, grid_ref(t, .{
|
|
.tag = .screen,
|
|
.value = .{ .screen = .{ .x = 0, .y = 0 } },
|
|
}, &ref));
|
|
|
|
// Should succeed for screen coords
|
|
var coord: point.Coordinate = undefined;
|
|
try testing.expectEqual(Result.success, point_from_grid_ref(t, &ref, .screen, &coord));
|
|
try testing.expectEqual(@as(u32, 0), coord.y);
|
|
|
|
// Should fail for active coords (it's in scrollback)
|
|
try testing.expectEqual(Result.no_value, point_from_grid_ref(t, &ref, .active, &coord));
|
|
}
|
|
|
|
test "point_from_grid_ref null terminal" {
|
|
var ref: grid_ref_c.CGridRef = .{};
|
|
try testing.expectEqual(Result.invalid_value, point_from_grid_ref(null, &ref, .active, null));
|
|
}
|
|
|
|
test "point_from_grid_ref null node" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
const ref: grid_ref_c.CGridRef = .{};
|
|
try testing.expectEqual(Result.invalid_value, point_from_grid_ref(t, &ref, .active, null));
|
|
}
|
|
|
|
test "set write_pty callback" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
const S = struct {
|
|
var last_data: ?[]u8 = null;
|
|
var last_userdata: ?*anyopaque = null;
|
|
|
|
fn deinit() void {
|
|
if (last_data) |d| testing.allocator.free(d);
|
|
last_data = null;
|
|
last_userdata = null;
|
|
}
|
|
|
|
fn writePty(_: Terminal, ud: ?*anyopaque, ptr: [*]const u8, len: usize) callconv(lib.calling_conv) void {
|
|
if (last_data) |d| testing.allocator.free(d);
|
|
last_data = testing.allocator.dupe(u8, ptr[0..len]) catch @panic("OOM");
|
|
last_userdata = ud;
|
|
}
|
|
};
|
|
defer S.deinit();
|
|
|
|
// Set userdata and write_pty callback
|
|
var sentinel: u8 = 42;
|
|
try testing.expectEqual(Result.success, set(t, .userdata, @ptrCast(&sentinel)));
|
|
try testing.expectEqual(Result.success, set(t, .write_pty, @ptrCast(&S.writePty)));
|
|
|
|
// DECRQM for wraparound mode (mode 7, set by default) should trigger write_pty
|
|
vt_write(t, "\x1B[?7$p", 6);
|
|
try testing.expect(S.last_data != null);
|
|
try testing.expectEqualStrings("\x1B[?7;1$y", S.last_data.?);
|
|
try testing.expectEqual(@as(?*anyopaque, @ptrCast(&sentinel)), S.last_userdata);
|
|
}
|
|
|
|
test "write_pty receives DECRQSS response" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
const S = struct {
|
|
var last_data: ?[]u8 = null;
|
|
|
|
fn deinit() void {
|
|
if (last_data) |data| testing.allocator.free(data);
|
|
last_data = null;
|
|
}
|
|
|
|
fn writePty(_: Terminal, _: ?*anyopaque, ptr: [*]const u8, len: usize) callconv(lib.calling_conv) void {
|
|
if (last_data) |data| testing.allocator.free(data);
|
|
last_data = testing.allocator.dupe(u8, ptr[0..len]) catch @panic("OOM");
|
|
}
|
|
};
|
|
defer S.deinit();
|
|
|
|
try testing.expectEqual(Result.success, set(t, .write_pty, @ptrCast(&S.writePty)));
|
|
|
|
const query = "\x1B[1m\x1BP$qm\x1B\\";
|
|
vt_write(t, query, query.len);
|
|
try testing.expectEqualStrings("\x1BP1$r0;1m\x1B\\", S.last_data.?);
|
|
}
|
|
|
|
test "write_pty receives OSC color query response" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
const S = struct {
|
|
var last_data: ?[]u8 = null;
|
|
|
|
fn deinit() void {
|
|
if (last_data) |d| testing.allocator.free(d);
|
|
last_data = null;
|
|
}
|
|
|
|
fn writePty(_: Terminal, _: ?*anyopaque, ptr: [*]const u8, len: usize) callconv(lib.calling_conv) void {
|
|
if (last_data) |d| testing.allocator.free(d);
|
|
last_data = testing.allocator.dupe(u8, ptr[0..len]) catch @panic("OOM");
|
|
}
|
|
};
|
|
defer S.deinit();
|
|
|
|
try testing.expectEqual(Result.success, set(t, .write_pty, @ptrCast(&S.writePty)));
|
|
|
|
const set_fg = "\x1B]10;rgb:01/02/03\x1B\\";
|
|
vt_write(t, set_fg, set_fg.len);
|
|
try testing.expect(S.last_data == null);
|
|
|
|
const query_fg = "\x1B]10;?\x1B\\";
|
|
vt_write(t, query_fg, query_fg.len);
|
|
try testing.expect(S.last_data != null);
|
|
try testing.expectEqualStrings("\x1B]10;rgb:0101/0202/0303\x1B\\", S.last_data.?);
|
|
}
|
|
|
|
test "set write_pty without callback ignores queries" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
// Without setting a callback, DECRQM should be silently ignored (no crash)
|
|
vt_write(t, "\x1B[?7$p", 6);
|
|
}
|
|
|
|
test "set write_pty null clears callback" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
const S = struct {
|
|
var called: bool = false;
|
|
fn writePty(_: Terminal, _: ?*anyopaque, _: [*]const u8, _: usize) callconv(lib.calling_conv) void {
|
|
called = true;
|
|
}
|
|
};
|
|
S.called = false;
|
|
|
|
// Set then clear the callback
|
|
try testing.expectEqual(Result.success, set(t, .write_pty, @ptrCast(&S.writePty)));
|
|
try testing.expectEqual(Result.success, set(t, .write_pty, null));
|
|
|
|
vt_write(t, "\x1B[?7$p", 6);
|
|
try testing.expect(!S.called);
|
|
}
|
|
|
|
test "set bell callback" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
const S = struct {
|
|
var bell_count: usize = 0;
|
|
var last_userdata: ?*anyopaque = null;
|
|
|
|
fn bell(_: Terminal, ud: ?*anyopaque) callconv(lib.calling_conv) void {
|
|
bell_count += 1;
|
|
last_userdata = ud;
|
|
}
|
|
};
|
|
S.bell_count = 0;
|
|
S.last_userdata = null;
|
|
|
|
// Set userdata and bell callback
|
|
var sentinel: u8 = 99;
|
|
try testing.expectEqual(Result.success, set(t, .userdata, @ptrCast(&sentinel)));
|
|
try testing.expectEqual(Result.success, set(t, .bell, @ptrCast(&S.bell)));
|
|
|
|
// Single BEL
|
|
vt_write(t, "\x07", 1);
|
|
try testing.expectEqual(@as(usize, 1), S.bell_count);
|
|
try testing.expectEqual(@as(?*anyopaque, @ptrCast(&sentinel)), S.last_userdata);
|
|
|
|
// Multiple BELs
|
|
vt_write(t, "\x07\x07", 2);
|
|
try testing.expectEqual(@as(usize, 3), S.bell_count);
|
|
}
|
|
|
|
test "bell without callback is silent" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
// BEL without a callback should not crash
|
|
vt_write(t, "\x07", 1);
|
|
}
|
|
|
|
test "set enquiry callback" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
const S = struct {
|
|
var last_data: ?[]u8 = null;
|
|
|
|
fn deinit() void {
|
|
if (last_data) |d| testing.allocator.free(d);
|
|
last_data = null;
|
|
}
|
|
|
|
fn writePty(_: Terminal, _: ?*anyopaque, ptr: [*]const u8, len: usize) callconv(lib.calling_conv) void {
|
|
if (last_data) |d| testing.allocator.free(d);
|
|
last_data = testing.allocator.dupe(u8, ptr[0..len]) catch @panic("OOM");
|
|
}
|
|
|
|
const response = "OK";
|
|
fn enquiry(_: Terminal, _: ?*anyopaque) callconv(lib.calling_conv) lib.String {
|
|
return .{ .ptr = response, .len = response.len };
|
|
}
|
|
};
|
|
defer S.deinit();
|
|
|
|
try testing.expectEqual(Result.success, set(t, .write_pty, @ptrCast(&S.writePty)));
|
|
try testing.expectEqual(Result.success, set(t, .enquiry, @ptrCast(&S.enquiry)));
|
|
|
|
// ENQ (0x05) should trigger the enquiry callback and write response via write_pty
|
|
vt_write(t, "\x05", 1);
|
|
try testing.expect(S.last_data != null);
|
|
try testing.expectEqualStrings("OK", S.last_data.?);
|
|
}
|
|
|
|
test "enquiry without callback is silent" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
// ENQ without a callback should not crash
|
|
vt_write(t, "\x05", 1);
|
|
}
|
|
|
|
test "set xtversion callback" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
const S = struct {
|
|
var last_data: ?[]u8 = null;
|
|
|
|
fn deinit() void {
|
|
if (last_data) |d| testing.allocator.free(d);
|
|
last_data = null;
|
|
}
|
|
|
|
fn writePty(_: Terminal, _: ?*anyopaque, ptr: [*]const u8, len: usize) callconv(lib.calling_conv) void {
|
|
if (last_data) |d| testing.allocator.free(d);
|
|
last_data = testing.allocator.dupe(u8, ptr[0..len]) catch @panic("OOM");
|
|
}
|
|
|
|
const version = "myterm 1.0";
|
|
fn xtversion(_: Terminal, _: ?*anyopaque) callconv(lib.calling_conv) lib.String {
|
|
return .{ .ptr = version, .len = version.len };
|
|
}
|
|
};
|
|
defer S.deinit();
|
|
|
|
try testing.expectEqual(Result.success, set(t, .write_pty, @ptrCast(&S.writePty)));
|
|
try testing.expectEqual(Result.success, set(t, .xtversion, @ptrCast(&S.xtversion)));
|
|
|
|
// XTVERSION: CSI > q
|
|
vt_write(t, "\x1B[>q", 4);
|
|
try testing.expect(S.last_data != null);
|
|
// Response should be DCS >| version ST
|
|
try testing.expectEqualStrings("\x1BP>|myterm 1.0\x1B\\", S.last_data.?);
|
|
}
|
|
|
|
test "xtversion without callback reports default" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
const S = struct {
|
|
var last_data: ?[]u8 = null;
|
|
|
|
fn deinit() void {
|
|
if (last_data) |d| testing.allocator.free(d);
|
|
last_data = null;
|
|
}
|
|
|
|
fn writePty(_: Terminal, _: ?*anyopaque, ptr: [*]const u8, len: usize) callconv(lib.calling_conv) void {
|
|
if (last_data) |d| testing.allocator.free(d);
|
|
last_data = testing.allocator.dupe(u8, ptr[0..len]) catch @panic("OOM");
|
|
}
|
|
};
|
|
defer S.deinit();
|
|
|
|
// Set write_pty but not xtversion — should get default "libghostty"
|
|
try testing.expectEqual(Result.success, set(t, .write_pty, @ptrCast(&S.writePty)));
|
|
|
|
vt_write(t, "\x1B[>q", 4);
|
|
try testing.expect(S.last_data != null);
|
|
try testing.expectEqualStrings("\x1BP>|libghostty\x1B\\", S.last_data.?);
|
|
}
|
|
|
|
test "set title_changed callback" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
const S = struct {
|
|
var title_count: usize = 0;
|
|
var last_userdata: ?*anyopaque = null;
|
|
|
|
fn titleChanged(_: Terminal, ud: ?*anyopaque) callconv(lib.calling_conv) void {
|
|
title_count += 1;
|
|
last_userdata = ud;
|
|
}
|
|
};
|
|
S.title_count = 0;
|
|
S.last_userdata = null;
|
|
|
|
var sentinel: u8 = 77;
|
|
try testing.expectEqual(Result.success, set(t, .userdata, @ptrCast(&sentinel)));
|
|
try testing.expectEqual(Result.success, set(t, .title_changed, @ptrCast(&S.titleChanged)));
|
|
|
|
// OSC 2 ; title ST — set window title
|
|
vt_write(t, "\x1B]2;Hello\x1B\\", 10);
|
|
try testing.expectEqual(@as(usize, 1), S.title_count);
|
|
try testing.expectEqual(@as(?*anyopaque, @ptrCast(&sentinel)), S.last_userdata);
|
|
|
|
// Another title change
|
|
vt_write(t, "\x1B]2;World\x1B\\", 10);
|
|
try testing.expectEqual(@as(usize, 2), S.title_count);
|
|
}
|
|
|
|
test "title_changed without callback is silent" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
// OSC 2 without a callback should not crash
|
|
vt_write(t, "\x1B]2;Hello\x1B\\", 10);
|
|
}
|
|
|
|
test "set desktop_notification callback" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
const S = struct {
|
|
var count: usize = 0;
|
|
var last_userdata: ?*anyopaque = null;
|
|
var last_size: usize = 0;
|
|
var title: [64]u8 = undefined;
|
|
var title_len: usize = 0;
|
|
var body: [64]u8 = undefined;
|
|
var body_len: usize = 0;
|
|
|
|
fn desktopNotification(
|
|
_: Terminal,
|
|
ud: ?*anyopaque,
|
|
notification: *const DesktopNotification,
|
|
) callconv(lib.calling_conv) void {
|
|
count += 1;
|
|
last_userdata = ud;
|
|
last_size = notification.size;
|
|
title_len = notification.title.len;
|
|
body_len = notification.body.len;
|
|
@memcpy(title[0..title_len], notification.title.ptr[0..title_len]);
|
|
@memcpy(body[0..body_len], notification.body.ptr[0..body_len]);
|
|
}
|
|
};
|
|
S.count = 0;
|
|
S.last_userdata = null;
|
|
S.last_size = 0;
|
|
S.title_len = 0;
|
|
S.body_len = 0;
|
|
|
|
var sentinel: u8 = 99;
|
|
try testing.expectEqual(Result.success, set(t, .userdata, @ptrCast(&sentinel)));
|
|
try testing.expectEqual(Result.success, set(
|
|
t,
|
|
.desktop_notification,
|
|
@ptrCast(&S.desktopNotification),
|
|
));
|
|
|
|
// Split OSC 777 across writes to exercise the persistent VT parser.
|
|
const seq_a = "\x1B]777;notify;Codex;";
|
|
const seq_b = "Needs attention\x1B\\";
|
|
vt_write(t, seq_a, seq_a.len);
|
|
try testing.expectEqual(@as(usize, 0), S.count);
|
|
vt_write(t, seq_b, seq_b.len);
|
|
try testing.expectEqual(@as(usize, 1), S.count);
|
|
try testing.expectEqual(@as(?*anyopaque, @ptrCast(&sentinel)), S.last_userdata);
|
|
try testing.expectEqual(@sizeOf(DesktopNotification), S.last_size);
|
|
try testing.expectEqualStrings("Codex", S.title[0..S.title_len]);
|
|
try testing.expectEqualStrings("Needs attention", S.body[0..S.body_len]);
|
|
|
|
// OSC 9 has no title and preserves its body.
|
|
const seq_c = "\x1B]9;Build complete\x07";
|
|
vt_write(t, seq_c, seq_c.len);
|
|
try testing.expectEqual(@as(usize, 2), S.count);
|
|
try testing.expectEqualStrings("", S.title[0..S.title_len]);
|
|
try testing.expectEqualStrings("Build complete", S.body[0..S.body_len]);
|
|
|
|
// Removing the callback takes effect immediately.
|
|
try testing.expectEqual(Result.success, set(t, .desktop_notification, null));
|
|
vt_write(t, seq_c, seq_c.len);
|
|
try testing.expectEqual(@as(usize, 2), S.count);
|
|
}
|
|
|
|
test "set progress_report callback" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
const S = struct {
|
|
var count: usize = 0;
|
|
var last_userdata: ?*anyopaque = null;
|
|
var last_size: usize = 0;
|
|
var last_state: ProgressState = .remove;
|
|
var last_progress: i8 = -1;
|
|
|
|
fn progressReport(
|
|
_: Terminal,
|
|
ud: ?*anyopaque,
|
|
report: *const ProgressReport,
|
|
) callconv(lib.calling_conv) void {
|
|
count += 1;
|
|
last_userdata = ud;
|
|
last_size = report.size;
|
|
last_state = report.state;
|
|
last_progress = report.progress;
|
|
}
|
|
};
|
|
S.count = 0;
|
|
S.last_userdata = null;
|
|
S.last_size = 0;
|
|
S.last_state = .remove;
|
|
S.last_progress = -1;
|
|
|
|
var sentinel: u8 = 100;
|
|
try testing.expectEqual(Result.success, set(t, .userdata, @ptrCast(&sentinel)));
|
|
try testing.expectEqual(Result.success, set(
|
|
t,
|
|
.progress_report,
|
|
@ptrCast(&S.progressReport),
|
|
));
|
|
|
|
const cases = [_]struct {
|
|
sequence: []const u8,
|
|
state: ProgressState,
|
|
progress: i8,
|
|
}{
|
|
.{ .sequence = "\x1B]9;4;0;\x1B\\", .state = .remove, .progress = -1 },
|
|
.{ .sequence = "\x1B]9;4;1;42\x07", .state = .set, .progress = 42 },
|
|
.{ .sequence = "\x1B]9;4;2;7\x1B\\", .state = .@"error", .progress = 7 },
|
|
.{ .sequence = "\x1B]9;4;3\x1B\\", .state = .indeterminate, .progress = -1 },
|
|
.{ .sequence = "\x1B]9;4;4;75\x1B\\", .state = .pause, .progress = 75 },
|
|
};
|
|
|
|
for (cases, 1..) |case, expected_count| {
|
|
const midpoint = case.sequence.len / 2;
|
|
vt_write(t, case.sequence.ptr, midpoint);
|
|
try testing.expectEqual(expected_count - 1, S.count);
|
|
vt_write(t, case.sequence.ptr + midpoint, case.sequence.len - midpoint);
|
|
try testing.expectEqual(expected_count, S.count);
|
|
try testing.expectEqual(@as(?*anyopaque, @ptrCast(&sentinel)), S.last_userdata);
|
|
try testing.expectEqual(@sizeOf(ProgressReport), S.last_size);
|
|
try testing.expectEqual(case.state, S.last_state);
|
|
try testing.expectEqual(case.progress, S.last_progress);
|
|
}
|
|
|
|
try testing.expectEqual(Result.success, set(t, .progress_report, null));
|
|
const ignored = "\x1B]9;4;1;90\x1B\\";
|
|
vt_write(t, ignored, ignored.len);
|
|
try testing.expectEqual(@as(usize, cases.len), S.count);
|
|
}
|
|
|
|
test "set pwd_changed callback" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
const S = struct {
|
|
var pwd_count: usize = 0;
|
|
var last_userdata: ?*anyopaque = null;
|
|
|
|
fn pwdChanged(_: Terminal, ud: ?*anyopaque) callconv(lib.calling_conv) void {
|
|
pwd_count += 1;
|
|
last_userdata = ud;
|
|
}
|
|
};
|
|
S.pwd_count = 0;
|
|
S.last_userdata = null;
|
|
|
|
var sentinel: u8 = 88;
|
|
try testing.expectEqual(Result.success, set(t, .userdata, @ptrCast(&sentinel)));
|
|
try testing.expectEqual(Result.success, set(t, .pwd_changed, @ptrCast(&S.pwdChanged)));
|
|
|
|
// OSC 7 ; file:///tmp ST — report pwd
|
|
const seq1 = "\x1B]7;file:///tmp\x1B\\";
|
|
vt_write(t, seq1, seq1.len);
|
|
try testing.expectEqual(@as(usize, 1), S.pwd_count);
|
|
try testing.expectEqual(@as(?*anyopaque, @ptrCast(&sentinel)), S.last_userdata);
|
|
try testing.expectEqualStrings("file:///tmp", zigTerminal(t).?.getPwd().?);
|
|
|
|
// Another pwd change
|
|
const seq2 = "\x1B]7;file:///home/user\x1B\\";
|
|
vt_write(t, seq2, seq2.len);
|
|
try testing.expectEqual(@as(usize, 2), S.pwd_count);
|
|
try testing.expectEqualStrings("file:///home/user", zigTerminal(t).?.getPwd().?);
|
|
}
|
|
|
|
test "set clipboard_write callback" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
const S = struct {
|
|
var count: usize = 0;
|
|
var last_terminal: Terminal = null;
|
|
var last_userdata: ?*anyopaque = null;
|
|
var last_size: usize = 0;
|
|
var last_location: clipboard.Location = .standard;
|
|
var last_contents_null: bool = false;
|
|
var last_contents_len: usize = 0;
|
|
var last_mimes: [8][64]u8 = undefined;
|
|
var last_mime_lens: [8]usize = @splat(0);
|
|
var last_data: [8][64]u8 = undefined;
|
|
var last_data_lens: [8]usize = @splat(0);
|
|
var next_result: clipboard.WriteResult = .success;
|
|
|
|
fn clipboardWrite(
|
|
terminal_: Terminal,
|
|
ud: ?*anyopaque,
|
|
request: *const ClipboardWrite,
|
|
) callconv(lib.calling_conv) clipboard.WriteResult {
|
|
count += 1;
|
|
last_terminal = terminal_;
|
|
last_userdata = ud;
|
|
last_size = request.size;
|
|
last_location = request.location;
|
|
last_contents_null = request.contents == null;
|
|
last_contents_len = request.contents_len;
|
|
|
|
if (request.contents) |ptr| {
|
|
for (ptr[0..@min(request.contents_len, last_mimes.len)], 0..) |content, i| {
|
|
last_mime_lens[i] = @min(content.mime.len, last_mimes[i].len);
|
|
@memcpy(
|
|
last_mimes[i][0..last_mime_lens[i]],
|
|
content.mime.ptr[0..last_mime_lens[i]],
|
|
);
|
|
|
|
last_data_lens[i] = @min(content.data.len, last_data[i].len);
|
|
@memcpy(
|
|
last_data[i][0..last_data_lens[i]],
|
|
content.data.ptr[0..last_data_lens[i]],
|
|
);
|
|
}
|
|
}
|
|
|
|
return next_result;
|
|
}
|
|
};
|
|
S.count = 0;
|
|
S.last_terminal = null;
|
|
S.last_userdata = null;
|
|
S.next_result = .denied;
|
|
|
|
var sentinel: u8 = 88;
|
|
try testing.expectEqual(Result.success, set(t, .userdata, @ptrCast(&sentinel)));
|
|
try testing.expectEqual(Result.success, set(t, .clipboard_write, @ptrCast(&S.clipboardWrite)));
|
|
|
|
// Split OSC 52 write whose decoded payload contains an embedded NUL.
|
|
const seq1_a = "\x1B]52;c;aGVs";
|
|
const seq1_b = "bG8Ad29ybGQ=\x1B\\";
|
|
vt_write(t, seq1_a, seq1_a.len);
|
|
vt_write(t, seq1_b, seq1_b.len);
|
|
try testing.expectEqual(@as(usize, 1), S.count);
|
|
try testing.expectEqual(t, S.last_terminal);
|
|
try testing.expectEqual(@as(?*anyopaque, @ptrCast(&sentinel)), S.last_userdata);
|
|
try testing.expectEqual(@sizeOf(ClipboardWrite), S.last_size);
|
|
try testing.expectEqual(clipboard.Location.standard, S.last_location);
|
|
try testing.expect(!S.last_contents_null);
|
|
try testing.expectEqual(@as(usize, 1), S.last_contents_len);
|
|
try testing.expectEqualStrings("text/plain", S.last_mimes[0][0..S.last_mime_lens[0]]);
|
|
try testing.expectEqualSlices(u8, "hello\x00world", S.last_data[0][0..S.last_data_lens[0]]);
|
|
|
|
// OSC 52 destinations are normalized rather than exposed as wire bytes.
|
|
const location_cases = [_]struct {
|
|
selector: u8,
|
|
expected: clipboard.Location,
|
|
}{
|
|
.{ .selector = 's', .expected = .selection },
|
|
.{ .selector = 'p', .expected = .primary },
|
|
.{ .selector = 'q', .expected = .standard },
|
|
};
|
|
for (location_cases) |case| {
|
|
const seq = [_]u8{ '\x1B', ']', '5', '2', ';', case.selector, ';', 'e', 'A', '=', '=', '\x1B', '\\' };
|
|
vt_write(t, &seq, seq.len);
|
|
try testing.expectEqual(case.expected, S.last_location);
|
|
}
|
|
try testing.expectEqual(@as(usize, 4), S.count);
|
|
|
|
// An empty content list is a clear and retains a null descriptor pointer.
|
|
const clear = "\x1B]52;s;\x1B\\";
|
|
vt_write(t, clear, clear.len);
|
|
try testing.expectEqual(@as(usize, 5), S.count);
|
|
try testing.expectEqual(clipboard.Location.selection, S.last_location);
|
|
try testing.expect(S.last_contents_null);
|
|
try testing.expectEqual(@as(usize, 0), S.last_contents_len);
|
|
|
|
// Read requests and malformed base64 must never reach the callback.
|
|
const read = "\x1B]52;c;?\x1B\\";
|
|
vt_write(t, read, read.len);
|
|
const malformed = "\x1B]52;c;%%%\x1B\\";
|
|
vt_write(t, malformed, malformed.len);
|
|
try testing.expectEqual(@as(usize, 5), S.count);
|
|
|
|
// iTerm2 Copy reaches the same normalized callback.
|
|
const iterm = "\x1B]1337;Copy=:aVRlcm0=\x1B\\";
|
|
vt_write(t, iterm, iterm.len);
|
|
try testing.expectEqual(@as(usize, 6), S.count);
|
|
try testing.expectEqual(clipboard.Location.standard, S.last_location);
|
|
try testing.expectEqualStrings("text/plain", S.last_mimes[0][0..S.last_mime_lens[0]]);
|
|
try testing.expectEqualStrings("iTerm", S.last_data[0][0..S.last_data_lens[0]]);
|
|
|
|
// Every representation is converted, and callback results propagate
|
|
// through the C trampoline for protocols that can acknowledge writes.
|
|
const internal_contents = [_]clipboard.Content{
|
|
.{ .mime = "text/plain", .data = "plain" },
|
|
.{ .mime = "application/octet-stream", .data = "a\x00b" },
|
|
.{ .mime = "text/html", .data = "<b>plain</b>" },
|
|
.{ .mime = "text/rtf", .data = "{\\rtf1 plain}" },
|
|
.{ .mime = "image/png", .data = "\x89PNG" },
|
|
};
|
|
S.next_result = .busy;
|
|
const handler = &t.?.stream.handler;
|
|
const write_result = handler.effects.clipboard_write.?(handler, .{
|
|
.location = .primary,
|
|
.contents = &internal_contents,
|
|
});
|
|
try testing.expectEqual(clipboard.WriteResult.busy, write_result);
|
|
try testing.expectEqual(@as(usize, 7), S.count);
|
|
try testing.expectEqual(@as(usize, 5), S.last_contents_len);
|
|
try testing.expectEqualStrings(
|
|
"application/octet-stream",
|
|
S.last_mimes[1][0..S.last_mime_lens[1]],
|
|
);
|
|
try testing.expectEqualSlices(u8, "a\x00b", S.last_data[1][0..S.last_data_lens[1]]);
|
|
try testing.expectEqualStrings("image/png", S.last_mimes[4][0..S.last_mime_lens[4]]);
|
|
try testing.expectEqualSlices(u8, "\x89PNG", S.last_data[4][0..S.last_data_lens[4]]);
|
|
|
|
// Removing the callback takes effect immediately.
|
|
try testing.expectEqual(Result.success, set(t, .clipboard_write, null));
|
|
const after_remove = "\x1B]52;c;eA==\x1B\\";
|
|
vt_write(t, after_remove, after_remove.len);
|
|
try testing.expectEqual(@as(usize, 7), S.count);
|
|
}
|
|
|
|
test "clipboard_write without callback is unsupported and silent" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
// OSC 52 without a callback should not crash
|
|
const seq = "\x1B]52;c;aGVsbG8=\x1B\\";
|
|
vt_write(t, seq, seq.len);
|
|
|
|
const handler = &t.?.stream.handler;
|
|
const result = handler.effects.clipboard_write.?(handler, .{
|
|
.location = .standard,
|
|
.contents = &.{.{ .mime = "text/plain", .data = "hello" }},
|
|
});
|
|
try testing.expectEqual(clipboard.WriteResult.unsupported, result);
|
|
}
|
|
|
|
test "pwd_changed without callback is silent" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
// OSC 7 without a callback should not crash, but should still set the pwd
|
|
const seq = "\x1B]7;file:///tmp\x1B\\";
|
|
vt_write(t, seq, seq.len);
|
|
try testing.expectEqualStrings("file:///tmp", zigTerminal(t).?.getPwd().?);
|
|
}
|
|
|
|
test "set size callback" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
const S = struct {
|
|
var last_data: ?[]u8 = null;
|
|
|
|
fn deinit() void {
|
|
if (last_data) |d| testing.allocator.free(d);
|
|
last_data = null;
|
|
}
|
|
|
|
fn writePty(_: Terminal, _: ?*anyopaque, ptr: [*]const u8, len: usize) callconv(lib.calling_conv) void {
|
|
if (last_data) |d| testing.allocator.free(d);
|
|
last_data = testing.allocator.dupe(u8, ptr[0..len]) catch @panic("OOM");
|
|
}
|
|
|
|
fn sizeCb(_: Terminal, _: ?*anyopaque, out_size: *size_report.Size) callconv(lib.calling_conv) bool {
|
|
out_size.* = .{
|
|
.rows = 24,
|
|
.columns = 80,
|
|
.cell_width = 8,
|
|
.cell_height = 16,
|
|
};
|
|
return true;
|
|
}
|
|
};
|
|
defer S.deinit();
|
|
|
|
try testing.expectEqual(Result.success, set(t, .write_pty, @ptrCast(&S.writePty)));
|
|
try testing.expectEqual(Result.success, set(t, .size_cb, @ptrCast(&S.sizeCb)));
|
|
|
|
// CSI 18 t — report text area size in characters
|
|
vt_write(t, "\x1B[18t", 5);
|
|
try testing.expect(S.last_data != null);
|
|
try testing.expectEqualStrings("\x1b[8;24;80t", S.last_data.?);
|
|
}
|
|
|
|
test "size without callback is silent" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
// CSI 18 t without a size callback should not crash
|
|
vt_write(t, "\x1B[18t", 5);
|
|
}
|
|
|
|
test "set device_attributes callback primary" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
const S = struct {
|
|
var last_data: ?[]u8 = null;
|
|
|
|
fn deinit() void {
|
|
if (last_data) |d| testing.allocator.free(d);
|
|
last_data = null;
|
|
}
|
|
|
|
fn writePty(_: Terminal, _: ?*anyopaque, ptr: [*]const u8, len: usize) callconv(lib.calling_conv) void {
|
|
if (last_data) |d| testing.allocator.free(d);
|
|
last_data = testing.allocator.dupe(u8, ptr[0..len]) catch @panic("OOM");
|
|
}
|
|
|
|
fn da(_: Terminal, _: ?*anyopaque, out: *Effects.CDeviceAttributes) callconv(lib.calling_conv) bool {
|
|
out.* = .{
|
|
.primary = .{
|
|
.conformance_level = 64,
|
|
.features = .{ 22, 52 } ++ .{0} ** 62,
|
|
.num_features = 2,
|
|
},
|
|
.secondary = .{
|
|
.device_type = 1,
|
|
.firmware_version = 10,
|
|
.rom_cartridge = 0,
|
|
},
|
|
.tertiary = .{ .unit_id = 0 },
|
|
};
|
|
return true;
|
|
}
|
|
};
|
|
defer S.deinit();
|
|
|
|
try testing.expectEqual(Result.success, set(t, .write_pty, @ptrCast(&S.writePty)));
|
|
try testing.expectEqual(Result.success, set(t, .device_attributes, @ptrCast(&S.da)));
|
|
|
|
// CSI c — primary DA
|
|
vt_write(t, "\x1B[c", 3);
|
|
try testing.expect(S.last_data != null);
|
|
try testing.expectEqualStrings("\x1b[?64;22;52c", S.last_data.?);
|
|
}
|
|
|
|
test "set device_attributes callback secondary" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
const S = struct {
|
|
var last_data: ?[]u8 = null;
|
|
|
|
fn deinit() void {
|
|
if (last_data) |d| testing.allocator.free(d);
|
|
last_data = null;
|
|
}
|
|
|
|
fn writePty(_: Terminal, _: ?*anyopaque, ptr: [*]const u8, len: usize) callconv(lib.calling_conv) void {
|
|
if (last_data) |d| testing.allocator.free(d);
|
|
last_data = testing.allocator.dupe(u8, ptr[0..len]) catch @panic("OOM");
|
|
}
|
|
|
|
fn da(_: Terminal, _: ?*anyopaque, out: *Effects.CDeviceAttributes) callconv(lib.calling_conv) bool {
|
|
out.* = .{
|
|
.primary = .{
|
|
.conformance_level = 62,
|
|
.features = .{22} ++ .{0} ** 63,
|
|
.num_features = 1,
|
|
},
|
|
.secondary = .{
|
|
.device_type = 1,
|
|
.firmware_version = 10,
|
|
.rom_cartridge = 0,
|
|
},
|
|
.tertiary = .{ .unit_id = 0 },
|
|
};
|
|
return true;
|
|
}
|
|
};
|
|
defer S.deinit();
|
|
|
|
try testing.expectEqual(Result.success, set(t, .write_pty, @ptrCast(&S.writePty)));
|
|
try testing.expectEqual(Result.success, set(t, .device_attributes, @ptrCast(&S.da)));
|
|
|
|
// CSI > c — secondary DA
|
|
vt_write(t, "\x1B[>c", 4);
|
|
try testing.expect(S.last_data != null);
|
|
try testing.expectEqualStrings("\x1b[>1;10;0c", S.last_data.?);
|
|
}
|
|
|
|
test "set device_attributes callback tertiary" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
const S = struct {
|
|
var last_data: ?[]u8 = null;
|
|
|
|
fn deinit() void {
|
|
if (last_data) |d| testing.allocator.free(d);
|
|
last_data = null;
|
|
}
|
|
|
|
fn writePty(_: Terminal, _: ?*anyopaque, ptr: [*]const u8, len: usize) callconv(lib.calling_conv) void {
|
|
if (last_data) |d| testing.allocator.free(d);
|
|
last_data = testing.allocator.dupe(u8, ptr[0..len]) catch @panic("OOM");
|
|
}
|
|
|
|
fn da(_: Terminal, _: ?*anyopaque, out: *Effects.CDeviceAttributes) callconv(lib.calling_conv) bool {
|
|
out.* = .{
|
|
.primary = .{
|
|
.conformance_level = 62,
|
|
.features = .{0} ** 64,
|
|
.num_features = 0,
|
|
},
|
|
.secondary = .{
|
|
.device_type = 1,
|
|
.firmware_version = 0,
|
|
.rom_cartridge = 0,
|
|
},
|
|
.tertiary = .{ .unit_id = 0xAABBCCDD },
|
|
};
|
|
return true;
|
|
}
|
|
};
|
|
defer S.deinit();
|
|
|
|
try testing.expectEqual(Result.success, set(t, .write_pty, @ptrCast(&S.writePty)));
|
|
try testing.expectEqual(Result.success, set(t, .device_attributes, @ptrCast(&S.da)));
|
|
|
|
// CSI = c — tertiary DA
|
|
vt_write(t, "\x1B[=c", 4);
|
|
try testing.expect(S.last_data != null);
|
|
try testing.expectEqualStrings("\x1bP!|AABBCCDD\x1b\\", S.last_data.?);
|
|
}
|
|
|
|
test "device_attributes without callback uses default" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
const S = struct {
|
|
var last_data: ?[]u8 = null;
|
|
|
|
fn deinit() void {
|
|
if (last_data) |d| testing.allocator.free(d);
|
|
last_data = null;
|
|
}
|
|
|
|
fn writePty(_: Terminal, _: ?*anyopaque, ptr: [*]const u8, len: usize) callconv(lib.calling_conv) void {
|
|
if (last_data) |d| testing.allocator.free(d);
|
|
last_data = testing.allocator.dupe(u8, ptr[0..len]) catch @panic("OOM");
|
|
}
|
|
};
|
|
defer S.deinit();
|
|
|
|
try testing.expectEqual(Result.success, set(t, .write_pty, @ptrCast(&S.writePty)));
|
|
|
|
// Without setting a device_attributes callback, DA1 should return the default
|
|
vt_write(t, "\x1B[c", 3);
|
|
try testing.expect(S.last_data != null);
|
|
try testing.expectEqualStrings("\x1b[?62;22c", S.last_data.?);
|
|
}
|
|
|
|
test "device_attributes callback returns false uses default" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
const S = struct {
|
|
var last_data: ?[]u8 = null;
|
|
|
|
fn deinit() void {
|
|
if (last_data) |d| testing.allocator.free(d);
|
|
last_data = null;
|
|
}
|
|
|
|
fn writePty(_: Terminal, _: ?*anyopaque, ptr: [*]const u8, len: usize) callconv(lib.calling_conv) void {
|
|
if (last_data) |d| testing.allocator.free(d);
|
|
last_data = testing.allocator.dupe(u8, ptr[0..len]) catch @panic("OOM");
|
|
}
|
|
|
|
fn da(_: Terminal, _: ?*anyopaque, _: *Effects.CDeviceAttributes) callconv(lib.calling_conv) bool {
|
|
return false;
|
|
}
|
|
};
|
|
defer S.deinit();
|
|
|
|
try testing.expectEqual(Result.success, set(t, .write_pty, @ptrCast(&S.writePty)));
|
|
try testing.expectEqual(Result.success, set(t, .device_attributes, @ptrCast(&S.da)));
|
|
|
|
// Callback returns false, should use default response
|
|
vt_write(t, "\x1B[c", 3);
|
|
try testing.expect(S.last_data != null);
|
|
try testing.expectEqualStrings("\x1b[?62;22c", S.last_data.?);
|
|
}
|
|
|
|
test "set and get title" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
// No title set yet — should return empty string
|
|
var title: lib.String = undefined;
|
|
try testing.expectEqual(Result.success, get(t, .title, @ptrCast(&title)));
|
|
try testing.expectEqual(@as(usize, 0), title.len);
|
|
|
|
// Set title via option
|
|
const hello: lib.String = .{ .ptr = "Hello", .len = 5 };
|
|
try testing.expectEqual(Result.success, set(t, .title, @ptrCast(&hello)));
|
|
|
|
try testing.expectEqual(Result.success, get(t, .title, @ptrCast(&title)));
|
|
try testing.expectEqualStrings("Hello", title.ptr[0..title.len]);
|
|
|
|
// Overwrite title
|
|
const world: lib.String = .{ .ptr = "World", .len = 5 };
|
|
try testing.expectEqual(Result.success, set(t, .title, @ptrCast(&world)));
|
|
|
|
try testing.expectEqual(Result.success, get(t, .title, @ptrCast(&title)));
|
|
try testing.expectEqualStrings("World", title.ptr[0..title.len]);
|
|
|
|
// Clear title with NULL
|
|
try testing.expectEqual(Result.success, set(t, .title, null));
|
|
|
|
try testing.expectEqual(Result.success, get(t, .title, @ptrCast(&title)));
|
|
try testing.expectEqual(@as(usize, 0), title.len);
|
|
}
|
|
|
|
test "set and get pwd" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
// No pwd set yet — should return empty string
|
|
var pwd: lib.String = undefined;
|
|
try testing.expectEqual(Result.success, get(t, .pwd, @ptrCast(&pwd)));
|
|
try testing.expectEqual(@as(usize, 0), pwd.len);
|
|
|
|
// Set pwd via option
|
|
const home: lib.String = .{ .ptr = "/home/user", .len = 10 };
|
|
try testing.expectEqual(Result.success, set(t, .pwd, @ptrCast(&home)));
|
|
|
|
try testing.expectEqual(Result.success, get(t, .pwd, @ptrCast(&pwd)));
|
|
try testing.expectEqualStrings("/home/user", pwd.ptr[0..pwd.len]);
|
|
|
|
// Clear pwd with NULL
|
|
try testing.expectEqual(Result.success, set(t, .pwd, null));
|
|
|
|
try testing.expectEqual(Result.success, get(t, .pwd, @ptrCast(&pwd)));
|
|
try testing.expectEqual(@as(usize, 0), pwd.len);
|
|
}
|
|
|
|
test "get title set via vt_write" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
// Set title via OSC 2
|
|
vt_write(t, "\x1B]2;VT Title\x1B\\", 14);
|
|
|
|
var title: lib.String = undefined;
|
|
try testing.expectEqual(Result.success, get(t, .title, @ptrCast(&title)));
|
|
try testing.expectEqualStrings("VT Title", title.ptr[0..title.len]);
|
|
}
|
|
|
|
test "title report requires explicit opt in" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
const S = struct {
|
|
var last_data: ?[]u8 = null;
|
|
|
|
fn deinit() void {
|
|
if (last_data) |data| testing.allocator.free(data);
|
|
last_data = null;
|
|
}
|
|
|
|
fn writePty(
|
|
_: Terminal,
|
|
_: ?*anyopaque,
|
|
ptr: [*]const u8,
|
|
len: usize,
|
|
) callconv(lib.calling_conv) void {
|
|
if (last_data) |data| testing.allocator.free(data);
|
|
last_data = testing.allocator.dupe(u8, ptr[0..len]) catch @panic("OOM");
|
|
}
|
|
};
|
|
S.last_data = null;
|
|
defer S.deinit();
|
|
|
|
try testing.expectEqual(
|
|
Result.success,
|
|
set(t, .write_pty, @ptrCast(&S.writePty)),
|
|
);
|
|
|
|
const set_title = "\x1B]2;echo vulnerable\x1B\\";
|
|
const query_title = "\x1B[21t";
|
|
vt_write(t, set_title, set_title.len);
|
|
|
|
// WRITE_PTY alone must not enable the security-sensitive response.
|
|
vt_write(t, query_title, query_title.len);
|
|
try testing.expect(S.last_data == null);
|
|
|
|
const enabled = true;
|
|
try testing.expectEqual(Result.success, set(t, .title_report, &enabled));
|
|
vt_write(t, query_title, query_title.len);
|
|
try testing.expectEqualStrings(
|
|
"\x1b]lecho vulnerable\x1b\\",
|
|
S.last_data.?,
|
|
);
|
|
|
|
// NULL restores the secure default.
|
|
S.deinit();
|
|
try testing.expectEqual(Result.success, set(t, .title_report, null));
|
|
vt_write(t, query_title, query_title.len);
|
|
try testing.expect(S.last_data == null);
|
|
}
|
|
|
|
test "resize updates pixel dimensions" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
// Pixel geometry must still be applied when the cell dimensions match.
|
|
try testing.expectEqual(Result.success, resize(t, 80, 24, 9, 18));
|
|
|
|
const zt = t.?.terminal;
|
|
try testing.expectEqual(@as(u32, 80 * 9), zt.width_px);
|
|
try testing.expectEqual(@as(u32, 24 * 18), zt.height_px);
|
|
}
|
|
|
|
test "resize pixel overflow saturates" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
try testing.expectEqual(Result.success, resize(t, 100, 40, std.math.maxInt(u32), std.math.maxInt(u32)));
|
|
|
|
const zt = t.?.terminal;
|
|
try testing.expectEqual(std.math.maxInt(u32), zt.width_px);
|
|
try testing.expectEqual(std.math.maxInt(u32), zt.height_px);
|
|
}
|
|
|
|
test "resize disables synchronized output" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
const zt = t.?.terminal;
|
|
zt.modes.set(.synchronized_output, true);
|
|
|
|
// The terminal-level reset must run even if grid work is unnecessary.
|
|
try testing.expectEqual(Result.success, resize(t, 80, 24, 9, 18));
|
|
try testing.expect(!zt.modes.get(.synchronized_output));
|
|
}
|
|
|
|
test "resize sends in-band size report" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
const S = struct {
|
|
var last_data: ?[]u8 = null;
|
|
|
|
fn deinit() void {
|
|
if (last_data) |d| testing.allocator.free(d);
|
|
last_data = null;
|
|
}
|
|
|
|
fn writePty(_: Terminal, _: ?*anyopaque, ptr: [*]const u8, len: usize) callconv(lib.calling_conv) void {
|
|
if (last_data) |d| testing.allocator.free(d);
|
|
last_data = testing.allocator.dupe(u8, ptr[0..len]) catch @panic("OOM");
|
|
}
|
|
};
|
|
defer S.deinit();
|
|
|
|
try testing.expectEqual(Result.success, set(t, .write_pty, @ptrCast(&S.writePty)));
|
|
|
|
// Enable in-band size reports (mode 2048)
|
|
t.?.terminal.modes.set(.in_band_size_reports, true);
|
|
|
|
try testing.expectEqual(Result.success, resize(t, 100, 40, 9, 18));
|
|
|
|
// Expected: \x1B[48;rows;cols;height_px;width_pxt
|
|
// height_px = 40*18 = 720, width_px = 100*9 = 900
|
|
try testing.expect(S.last_data != null);
|
|
try testing.expectEqualStrings("\x1B[48;40;100;720;900t", S.last_data.?);
|
|
}
|
|
|
|
test "resize no size report without mode 2048" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
const S = struct {
|
|
var called: bool = false;
|
|
fn writePty(_: Terminal, _: ?*anyopaque, _: [*]const u8, _: usize) callconv(lib.calling_conv) void {
|
|
called = true;
|
|
}
|
|
};
|
|
S.called = false;
|
|
|
|
try testing.expectEqual(Result.success, set(t, .write_pty, @ptrCast(&S.writePty)));
|
|
|
|
// in_band_size_reports is off by default
|
|
try testing.expectEqual(Result.success, resize(t, 100, 40, 9, 18));
|
|
try testing.expect(!S.called);
|
|
}
|
|
|
|
test "resize in-band report without write_pty callback" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
// Enable mode 2048 but don't set a write_pty callback — should not crash
|
|
t.?.terminal.modes.set(.in_band_size_reports, true);
|
|
try testing.expectEqual(Result.success, resize(t, 100, 40, 9, 18));
|
|
}
|
|
|
|
test "resize null terminal" {
|
|
try testing.expectEqual(Result.invalid_value, resize(null, 100, 40, 9, 18));
|
|
}
|
|
|
|
test "resize zero cols" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
try testing.expectEqual(Result.invalid_value, resize(t, 0, 40, 9, 18));
|
|
}
|
|
|
|
test "resize zero rows" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
try testing.expectEqual(Result.invalid_value, resize(t, 100, 0, 9, 18));
|
|
}
|
|
|
|
test "grid_ref out of bounds" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
var out_ref: grid_ref_c.CGridRef = .{};
|
|
try testing.expectEqual(Result.invalid_value, grid_ref(t, .{
|
|
.tag = .active,
|
|
.value = .{ .active = .{ .x = 100, .y = 0 } },
|
|
}, &out_ref));
|
|
}
|
|
|
|
test "set and get color_foreground" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
// Initially unset
|
|
var rgb: color.RGB.C = undefined;
|
|
try testing.expectEqual(Result.no_value, get(t, .color_foreground, @ptrCast(&rgb)));
|
|
|
|
// Set a value
|
|
const fg: color.RGB.C = .{ .r = 0xAA, .g = 0xBB, .b = 0xCC };
|
|
try testing.expectEqual(Result.success, set(t, .color_foreground, @ptrCast(&fg)));
|
|
try testing.expectEqual(Result.success, get(t, .color_foreground, @ptrCast(&rgb)));
|
|
try testing.expectEqual(fg, rgb);
|
|
|
|
// Clear with null
|
|
try testing.expectEqual(Result.success, set(t, .color_foreground, null));
|
|
try testing.expectEqual(Result.no_value, get(t, .color_foreground, @ptrCast(&rgb)));
|
|
}
|
|
|
|
test "set and get color_background" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
var rgb: color.RGB.C = undefined;
|
|
try testing.expectEqual(Result.no_value, get(t, .color_background, @ptrCast(&rgb)));
|
|
|
|
const bg: color.RGB.C = .{ .r = 0x11, .g = 0x22, .b = 0x33 };
|
|
try testing.expectEqual(Result.success, set(t, .color_background, @ptrCast(&bg)));
|
|
try testing.expectEqual(Result.success, get(t, .color_background, @ptrCast(&rgb)));
|
|
try testing.expectEqual(bg, rgb);
|
|
|
|
try testing.expectEqual(Result.success, set(t, .color_background, null));
|
|
try testing.expectEqual(Result.no_value, get(t, .color_background, @ptrCast(&rgb)));
|
|
}
|
|
|
|
test "set and get color_cursor" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
var rgb: color.RGB.C = undefined;
|
|
try testing.expectEqual(Result.no_value, get(t, .color_cursor, @ptrCast(&rgb)));
|
|
|
|
const cur: color.RGB.C = .{ .r = 0xFF, .g = 0x00, .b = 0x88 };
|
|
try testing.expectEqual(Result.success, set(t, .color_cursor, @ptrCast(&cur)));
|
|
try testing.expectEqual(Result.success, get(t, .color_cursor, @ptrCast(&rgb)));
|
|
try testing.expectEqual(cur, rgb);
|
|
|
|
try testing.expectEqual(Result.success, set(t, .color_cursor, null));
|
|
try testing.expectEqual(Result.no_value, get(t, .color_cursor, @ptrCast(&rgb)));
|
|
}
|
|
|
|
test "set and get color_palette" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
// Get default palette
|
|
var palette: color.PaletteC = undefined;
|
|
try testing.expectEqual(Result.success, get(t, .color_palette, @ptrCast(&palette)));
|
|
try testing.expectEqual(color.default[0].cval(), palette[0]);
|
|
|
|
// Set custom palette
|
|
var custom: color.PaletteC = color.paletteCval(&color.default);
|
|
custom[0] = .{ .r = 0x12, .g = 0x34, .b = 0x56 };
|
|
try testing.expectEqual(Result.success, set(t, .color_palette, @ptrCast(&custom)));
|
|
try testing.expectEqual(Result.success, get(t, .color_palette, @ptrCast(&palette)));
|
|
try testing.expectEqual(custom[0], palette[0]);
|
|
|
|
// Reset with null restores default
|
|
try testing.expectEqual(Result.success, set(t, .color_palette, null));
|
|
try testing.expectEqual(Result.success, get(t, .color_palette, @ptrCast(&palette)));
|
|
try testing.expectEqual(color.default[0].cval(), palette[0]);
|
|
}
|
|
|
|
test "get color default vs effective with override" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
const zt = t.?.terminal;
|
|
var rgb: color.RGB.C = undefined;
|
|
|
|
// Set defaults
|
|
const fg: color.RGB.C = .{ .r = 0xAA, .g = 0xBB, .b = 0xCC };
|
|
const bg: color.RGB.C = .{ .r = 0x11, .g = 0x22, .b = 0x33 };
|
|
const cur: color.RGB.C = .{ .r = 0xFF, .g = 0x00, .b = 0x88 };
|
|
try testing.expectEqual(Result.success, set(t, .color_foreground, @ptrCast(&fg)));
|
|
try testing.expectEqual(Result.success, set(t, .color_background, @ptrCast(&bg)));
|
|
try testing.expectEqual(Result.success, set(t, .color_cursor, @ptrCast(&cur)));
|
|
|
|
// Simulate OSC overrides
|
|
const override: color.RGB = .{ .r = 0x00, .g = 0x00, .b = 0x00 };
|
|
zt.colors.foreground.override = override;
|
|
zt.colors.background.override = override;
|
|
zt.colors.cursor.override = override;
|
|
|
|
// Effective returns override
|
|
try testing.expectEqual(Result.success, get(t, .color_foreground, @ptrCast(&rgb)));
|
|
try testing.expectEqual(override.cval(), rgb);
|
|
try testing.expectEqual(Result.success, get(t, .color_background, @ptrCast(&rgb)));
|
|
try testing.expectEqual(override.cval(), rgb);
|
|
try testing.expectEqual(Result.success, get(t, .color_cursor, @ptrCast(&rgb)));
|
|
try testing.expectEqual(override.cval(), rgb);
|
|
|
|
// Default returns original
|
|
try testing.expectEqual(Result.success, get(t, .color_foreground_default, @ptrCast(&rgb)));
|
|
try testing.expectEqual(fg, rgb);
|
|
try testing.expectEqual(Result.success, get(t, .color_background_default, @ptrCast(&rgb)));
|
|
try testing.expectEqual(bg, rgb);
|
|
try testing.expectEqual(Result.success, get(t, .color_cursor_default, @ptrCast(&rgb)));
|
|
try testing.expectEqual(cur, rgb);
|
|
}
|
|
|
|
test "get color default returns no_value when unset" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
var rgb: color.RGB.C = undefined;
|
|
try testing.expectEqual(Result.no_value, get(t, .color_foreground_default, @ptrCast(&rgb)));
|
|
try testing.expectEqual(Result.no_value, get(t, .color_background_default, @ptrCast(&rgb)));
|
|
try testing.expectEqual(Result.no_value, get(t, .color_cursor_default, @ptrCast(&rgb)));
|
|
}
|
|
|
|
test "get color_palette_default vs current" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
const zt = t.?.terminal;
|
|
|
|
// Set a custom default palette
|
|
var custom: color.PaletteC = color.paletteCval(&color.default);
|
|
custom[0] = .{ .r = 0x12, .g = 0x34, .b = 0x56 };
|
|
try testing.expectEqual(Result.success, set(t, .color_palette, @ptrCast(&custom)));
|
|
|
|
// Simulate OSC override on index 0
|
|
zt.colors.palette.set(0, .{ .r = 0xFF, .g = 0xFF, .b = 0xFF });
|
|
|
|
// Current palette returns the override
|
|
var palette: color.PaletteC = undefined;
|
|
try testing.expectEqual(Result.success, get(t, .color_palette, @ptrCast(&palette)));
|
|
try testing.expectEqual(color.RGB.C{ .r = 0xFF, .g = 0xFF, .b = 0xFF }, palette[0]);
|
|
|
|
// Default palette returns the original
|
|
try testing.expectEqual(Result.success, get(t, .color_palette_default, @ptrCast(&palette)));
|
|
try testing.expectEqual(custom[0], palette[0]);
|
|
}
|
|
|
|
test "set color sets dirty flag" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
const zt = t.?.terminal;
|
|
zt.flags.dirty.palette = false;
|
|
|
|
const fg: color.RGB.C = .{ .r = 0xFF, .g = 0xFF, .b = 0xFF };
|
|
try testing.expectEqual(Result.success, set(t, .color_foreground, @ptrCast(&fg)));
|
|
try testing.expect(zt.flags.dirty.palette);
|
|
}
|
|
|
|
test "set glyph protocol disables APC handling and clears glossary" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
const register = "\x1B_25a1;r;cp=e0a0;AAAAAAAAAAAAAA==\x1B\\";
|
|
vt_write(t, register, register.len);
|
|
try testing.expect(t.?.terminal.glyph_glossary.contains(0xE0A0));
|
|
|
|
const disabled = false;
|
|
try testing.expectEqual(Result.success, set(t, .glyph_protocol, @ptrCast(&disabled)));
|
|
try testing.expect(!t.?.stream.handler.apc_handler.enabled.contains(.glyph));
|
|
try testing.expect(!t.?.terminal.glyph_glossary.contains(0xE0A0));
|
|
|
|
vt_write(t, register, register.len);
|
|
try testing.expect(!t.?.terminal.glyph_glossary.contains(0xE0A0));
|
|
|
|
const enabled = true;
|
|
try testing.expectEqual(Result.success, set(t, .glyph_protocol, @ptrCast(&enabled)));
|
|
vt_write(t, register, register.len);
|
|
try testing.expect(t.?.terminal.glyph_glossary.contains(0xE0A0));
|
|
}
|
|
|
|
test "get_multi success" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
var cols: u16 = 0;
|
|
var rows: u16 = 0;
|
|
var written: usize = 0;
|
|
|
|
const keys = [_]TerminalData{ .cols, .rows };
|
|
var values = [_]?*anyopaque{ @ptrCast(&cols), @ptrCast(&rows) };
|
|
try testing.expectEqual(Result.success, get_multi(t, keys.len, &keys, &values, &written));
|
|
try testing.expectEqual(keys.len, written);
|
|
try testing.expectEqual(80, cols);
|
|
try testing.expectEqual(24, rows);
|
|
}
|
|
|
|
test "get_multi error sets out_written" {
|
|
var t: Terminal = null;
|
|
try testing.expectEqual(Result.success, new(
|
|
&lib.alloc.test_allocator,
|
|
&t,
|
|
80,
|
|
24,
|
|
));
|
|
defer free(t);
|
|
|
|
var cols: u16 = 0;
|
|
var written: usize = 99;
|
|
|
|
const keys = [_]TerminalData{ .cols, .invalid };
|
|
var values = [_]?*anyopaque{ @ptrCast(&cols), @ptrCast(&cols) };
|
|
try testing.expectEqual(Result.invalid_value, get_multi(t, keys.len, &keys, &values, &written));
|
|
try testing.expectEqual(1, written);
|
|
try testing.expectEqual(80, cols);
|
|
}
|
|
|
|
test "get_multi null keys returns invalid_value" {
|
|
var cols: u16 = 0;
|
|
var values = [_]?*anyopaque{@ptrCast(&cols)};
|
|
try testing.expectEqual(Result.invalid_value, get_multi(null, 1, null, &values, null));
|
|
}
|