mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-09-21 21:17:40 +00:00
terminal: C API for unknown sequences
This commit is contained in:
@@ -148,6 +148,36 @@ pub const ProgressReport = extern struct {
|
||||
progress: i8,
|
||||
};
|
||||
|
||||
/// A borrowed unsupported string sequence.
|
||||
///
|
||||
/// C: GhosttyTerminalUnknownStringSequence
|
||||
pub const UnknownStringSequence = extern struct {
|
||||
truncated: bool,
|
||||
content: lib.String,
|
||||
};
|
||||
|
||||
/// An unsupported terminal sequence reported to the C callback.
|
||||
///
|
||||
/// C: GhosttyTerminalUnknownSequence
|
||||
pub const UnknownSequence = union(Tag) {
|
||||
apc: UnknownStringSequence,
|
||||
|
||||
/// C: GhosttyTerminalUnknownSequenceTag
|
||||
pub const Tag = lib.Enum(lib.target, &.{"apc"});
|
||||
|
||||
const c_union = lib.TaggedUnion(
|
||||
lib.target,
|
||||
@This(),
|
||||
// A future borrowed CSI payload may need parameter, separator, and
|
||||
// intermediate arrays. Reserve 128 bytes so that representation and
|
||||
// other structured sequence types can be added without an ABI break.
|
||||
[16]u64,
|
||||
);
|
||||
pub const C = c_union.C;
|
||||
pub const CValue = c_union.CValue;
|
||||
pub const cval = c_union.cval;
|
||||
};
|
||||
|
||||
/// A terminal mode and boolean value used for mode configuration.
|
||||
///
|
||||
/// C: GhosttyTerminalModeConfig
|
||||
@@ -161,9 +191,10 @@ pub const ModeConfig = extern struct {
|
||||
}
|
||||
};
|
||||
|
||||
/// 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.
|
||||
/// C callback state for terminal effects. Most trampolines are always
|
||||
/// installed on the stream handler; they check these fields and no-op when
|
||||
/// the corresponding callback is null. The unknown-sequence trampoline is
|
||||
/// installed dynamically to preserve its null fast path.
|
||||
const Effects = struct {
|
||||
userdata: ?*anyopaque = null,
|
||||
write_pty: ?WritePtyFn = null,
|
||||
@@ -178,6 +209,7 @@ const Effects = struct {
|
||||
progress_report: ?ProgressReportFn = null,
|
||||
size_cb: ?SizeFn = null,
|
||||
clipboard_write: ?ClipboardWriteFn = null,
|
||||
unknown_sequence: ?UnknownSequenceFn = null,
|
||||
|
||||
/// Scratch buffer for DA1 feature codes. The device attributes
|
||||
/// trampoline converts C feature codes into this buffer and returns
|
||||
@@ -225,6 +257,10 @@ const Effects = struct {
|
||||
/// 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 unknown_sequence callback. The request
|
||||
/// and its content are borrowed for the callback duration.
|
||||
pub const UnknownSequenceFn = *const fn (Terminal, ?*anyopaque, *const UnknownSequence.C) 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.
|
||||
@@ -408,6 +444,26 @@ const Effects = struct {
|
||||
func(@ptrCast(wrapper), wrapper.effects.userdata, &c_report);
|
||||
}
|
||||
|
||||
fn unknownSequenceTrampoline(
|
||||
handler: *Handler,
|
||||
sequence: Handler.UnknownSequence,
|
||||
) void {
|
||||
const wrapper = TerminalWrapper.fromHandler(handler);
|
||||
const func = wrapper.effects.unknown_sequence orelse return;
|
||||
const value = UnknownSequence.cval(switch (sequence) {
|
||||
.apc => |apc_value| .{
|
||||
.apc = .{
|
||||
.truncated = apc_value.truncated,
|
||||
.content = .{
|
||||
.ptr = apc_value.content.ptr,
|
||||
.len = apc_value.content.len,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
func(@ptrCast(wrapper), wrapper.effects.userdata, &value);
|
||||
}
|
||||
|
||||
fn sizeTrampoline(handler: *Handler) ?size_report.Size {
|
||||
const wrapper = TerminalWrapper.fromHandler(handler);
|
||||
const func = wrapper.effects.size_cb orelse return null;
|
||||
@@ -905,6 +961,8 @@ pub const Option = enum(c_int) {
|
||||
title_report = 32,
|
||||
mode_default = 33,
|
||||
mode = 34,
|
||||
unknown_sequence = 35,
|
||||
unknown_max_bytes = 36,
|
||||
|
||||
/// Input type expected for setting the option.
|
||||
pub fn InType(comptime self: Option) type {
|
||||
@@ -922,6 +980,7 @@ pub const Option = enum(c_int) {
|
||||
.progress_report => ?Effects.ProgressReportFn,
|
||||
.size_cb => ?Effects.SizeFn,
|
||||
.clipboard_write => ?Effects.ClipboardWriteFn,
|
||||
.unknown_sequence => ?Effects.UnknownSequenceFn,
|
||||
.title, .pwd => ?*const lib.String,
|
||||
.color_foreground, .color_background, .color_cursor => ?*const color.RGB.C,
|
||||
.color_palette => ?*const color.PaletteC,
|
||||
@@ -937,6 +996,7 @@ pub const Option = enum(c_int) {
|
||||
.scrollback_max_bytes,
|
||||
.scrollback_max_lines,
|
||||
.continuation_max_bytes,
|
||||
.unknown_max_bytes,
|
||||
=> ?*const usize,
|
||||
.selection => ?*const selection_c.CSelection,
|
||||
.default_cursor_style => ?*const TerminalCursorStyle,
|
||||
@@ -988,6 +1048,13 @@ fn setTyped(
|
||||
.progress_report => wrapper.effects.progress_report = value,
|
||||
.size_cb => wrapper.effects.size_cb = value,
|
||||
.clipboard_write => wrapper.effects.clipboard_write = value,
|
||||
.unknown_sequence => {
|
||||
wrapper.effects.unknown_sequence = value;
|
||||
wrapper.stream.handler.unknown_sequence = if (value != null)
|
||||
&Effects.unknownSequenceTrampoline
|
||||
else
|
||||
null;
|
||||
},
|
||||
.title_report => wrapper.stream.handler.title_report = if (value) |ptr|
|
||||
ptr.*
|
||||
else
|
||||
@@ -1106,6 +1173,8 @@ fn setTyped(
|
||||
wrapper,
|
||||
if (value) |ptr| ptr.* else default_continuation_max_bytes,
|
||||
),
|
||||
.unknown_max_bytes => wrapper.stream.handler.apc_handler.unknown_max_bytes =
|
||||
if (value) |ptr| ptr.* else 0,
|
||||
.mode, .mode_default => {
|
||||
const config = (value orelse return .invalid_value).*;
|
||||
const mode = config.toMode() orelse return .invalid_value;
|
||||
@@ -3861,6 +3930,114 @@ test "set progress_report callback" {
|
||||
try testing.expectEqual(@as(usize, cases.len), S.count);
|
||||
}
|
||||
|
||||
test "set unknown_sequence 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_tag: UnknownSequence.Tag = .apc;
|
||||
var last_truncated: bool = false;
|
||||
var content: [64]u8 = undefined;
|
||||
var content_len: usize = 0;
|
||||
|
||||
fn unknownSequence(
|
||||
terminal_: Terminal,
|
||||
ud: ?*anyopaque,
|
||||
sequence: *const UnknownSequence.C,
|
||||
) callconv(lib.calling_conv) void {
|
||||
count += 1;
|
||||
last_terminal = terminal_;
|
||||
last_userdata = ud;
|
||||
last_tag = sequence.tag;
|
||||
const apc_value = sequence.value.apc;
|
||||
last_truncated = apc_value.truncated;
|
||||
content_len = @min(apc_value.content.len, content.len);
|
||||
@memcpy(content[0..content_len], apc_value.content.ptr[0..content_len]);
|
||||
}
|
||||
};
|
||||
S.count = 0;
|
||||
S.last_terminal = null;
|
||||
S.last_userdata = null;
|
||||
S.last_tag = .apc;
|
||||
S.last_truncated = false;
|
||||
S.content_len = 0;
|
||||
|
||||
var sentinel: u8 = 101;
|
||||
try testing.expectEqual(Result.success, set(t, .userdata, @ptrCast(&sentinel)));
|
||||
|
||||
const max_bytes: usize = 8;
|
||||
try testing.expectEqual(Result.success, set(
|
||||
t,
|
||||
.unknown_max_bytes,
|
||||
@ptrCast(&max_bytes),
|
||||
));
|
||||
try testing.expectEqual(max_bytes, t.?.stream.handler.apc_handler.unknown_max_bytes);
|
||||
|
||||
// A byte limit without a callback performs no external effect.
|
||||
const before_callback = "\x1B_abc;xy\x1B\\";
|
||||
vt_write(t, before_callback, before_callback.len);
|
||||
try testing.expectEqual(@as(usize, 0), S.count);
|
||||
try testing.expect(t.?.stream.handler.unknown_sequence == null);
|
||||
|
||||
try testing.expectEqual(Result.success, set(
|
||||
t,
|
||||
.unknown_sequence,
|
||||
@ptrCast(&S.unknownSequence),
|
||||
));
|
||||
try testing.expect(t.?.stream.handler.unknown_sequence != null);
|
||||
|
||||
// Split a complete APC across writes to exercise persistent parser state.
|
||||
const seq_a = "\x1B_abc;";
|
||||
const seq_b = "xy\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(t, S.last_terminal);
|
||||
try testing.expectEqual(@as(?*anyopaque, @ptrCast(&sentinel)), S.last_userdata);
|
||||
try testing.expectEqual(UnknownSequence.Tag.apc, S.last_tag);
|
||||
try testing.expect(!S.last_truncated);
|
||||
try testing.expectEqualStrings("abc;xy", S.content[0..S.content_len]);
|
||||
|
||||
// Content beyond the generic limit is omitted and marked truncated.
|
||||
const truncated = "\x1B_abcdefghijkl\x1B\\";
|
||||
vt_write(t, truncated, truncated.len);
|
||||
try testing.expectEqual(@as(usize, 2), S.count);
|
||||
try testing.expect(S.last_truncated);
|
||||
try testing.expectEqualStrings("abcdefgh", S.content[0..S.content_len]);
|
||||
|
||||
// CAN aborts the APC and must not invoke the callback.
|
||||
const aborted = "\x1B_abcdef\x18";
|
||||
vt_write(t, aborted, aborted.len);
|
||||
try testing.expectEqual(@as(usize, 2), S.count);
|
||||
|
||||
// Clearing the callback restores the null fast path immediately.
|
||||
try testing.expectEqual(Result.success, set(t, .unknown_sequence, null));
|
||||
try testing.expect(t.?.stream.handler.unknown_sequence == null);
|
||||
vt_write(t, before_callback, before_callback.len);
|
||||
try testing.expectEqual(@as(usize, 2), S.count);
|
||||
|
||||
// A NULL limit disables capture even after reinstalling the callback.
|
||||
try testing.expectEqual(Result.success, set(
|
||||
t,
|
||||
.unknown_sequence,
|
||||
@ptrCast(&S.unknownSequence),
|
||||
));
|
||||
try testing.expectEqual(Result.success, set(t, .unknown_max_bytes, null));
|
||||
try testing.expectEqual(@as(usize, 0), t.?.stream.handler.apc_handler.unknown_max_bytes);
|
||||
vt_write(t, before_callback, before_callback.len);
|
||||
try testing.expectEqual(@as(usize, 2), S.count);
|
||||
}
|
||||
|
||||
test "set pwd_changed callback" {
|
||||
var t: Terminal = null;
|
||||
try testing.expectEqual(Result.success, new(
|
||||
|
||||
@@ -75,6 +75,8 @@ pub const structs: std.StaticStringMap(StructInfo) = structs: {
|
||||
.{ "GhosttyTerminalProgressReport", StructInfo.init(terminal.ProgressReport) },
|
||||
.{ "GhosttyTerminalScrollbar", StructInfo.init(terminal.TerminalScrollbar) },
|
||||
.{ "GhosttyTerminalScrollViewport", StructInfo.init(terminal.ScrollViewport) },
|
||||
.{ "GhosttyTerminalUnknownSequence", StructInfo.init(terminal.UnknownSequence.C) },
|
||||
.{ "GhosttyTerminalUnknownStringSequence", StructInfo.init(terminal.UnknownStringSequence) },
|
||||
.{ "GhosttyWriter", StructInfo.init(io.Writer) },
|
||||
});
|
||||
};
|
||||
@@ -223,6 +225,8 @@ test "json parses" {
|
||||
try std.testing.expect(root.contains("GhosttyClipboardWrite"));
|
||||
try std.testing.expect(root.contains("GhosttyFormatterTerminalOptions"));
|
||||
try std.testing.expect(root.contains("GhosttyTerminalModeConfig"));
|
||||
try std.testing.expect(root.contains("GhosttyTerminalUnknownSequence"));
|
||||
try std.testing.expect(root.contains("GhosttyTerminalUnknownStringSequence"));
|
||||
try std.testing.expect(root.contains("GhosttyReader"));
|
||||
try std.testing.expect(root.contains("GhosttyWriter"));
|
||||
|
||||
@@ -238,6 +242,16 @@ test "json parses" {
|
||||
try std.testing.expect(clipboard_write_fields.contains("contents"));
|
||||
try std.testing.expect(clipboard_write_fields.contains("contents_len"));
|
||||
|
||||
const unknown_sequence = root.get("GhosttyTerminalUnknownSequence").?.object;
|
||||
const unknown_sequence_fields = unknown_sequence.get("fields").?.object;
|
||||
try std.testing.expect(unknown_sequence_fields.contains("tag"));
|
||||
try std.testing.expect(unknown_sequence_fields.contains("value"));
|
||||
|
||||
const unknown_string = root.get("GhosttyTerminalUnknownStringSequence").?.object;
|
||||
const unknown_string_fields = unknown_string.get("fields").?.object;
|
||||
try std.testing.expect(unknown_string_fields.contains("truncated"));
|
||||
try std.testing.expect(unknown_string_fields.contains("content"));
|
||||
|
||||
const reader_fields = root.get("GhosttyReader").?.object
|
||||
.get("fields").?.object;
|
||||
try std.testing.expect(reader_fields.contains("read"));
|
||||
|
||||
Reference in New Issue
Block a user