mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-08-24 08:01:45 +00:00
terminal: C API for unknown sequences
This commit is contained in:
@@ -70,6 +70,32 @@ GhosttyClipboardWriteResult on_clipboard_write(
|
||||
}
|
||||
//! [effects-clipboard-write]
|
||||
|
||||
//! [effects-unknown-sequence]
|
||||
void on_unknown_sequence(
|
||||
GhosttyTerminal terminal,
|
||||
void* userdata,
|
||||
const GhosttyTerminalUnknownSequence* sequence) {
|
||||
(void)terminal;
|
||||
(void)userdata;
|
||||
|
||||
switch (sequence->tag) {
|
||||
case GHOSTTY_TERMINAL_UNKNOWN_SEQUENCE_APC: {
|
||||
const GhosttyTerminalUnknownStringSequence* apc = &sequence->value.apc;
|
||||
printf(" unknown APC (truncated=%s, content=%zu bytes): ",
|
||||
apc->truncated ? "yes" : "no",
|
||||
apc->content.len);
|
||||
if (apc->content.len > 0) {
|
||||
fwrite(apc->content.ptr, 1, apc->content.len, stdout);
|
||||
}
|
||||
printf("\n");
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
//! [effects-unknown-sequence]
|
||||
|
||||
//! [effects-register]
|
||||
int main() {
|
||||
// Create a terminal
|
||||
@@ -92,6 +118,14 @@ int main() {
|
||||
(const void *)on_title_changed);
|
||||
ghostty_terminal_set(terminal, GHOSTTY_TERMINAL_OPT_CLIPBOARD_WRITE,
|
||||
(const void *)on_clipboard_write);
|
||||
ghostty_terminal_set(terminal, GHOSTTY_TERMINAL_OPT_UNKNOWN_SEQUENCE,
|
||||
(const void *)on_unknown_sequence);
|
||||
|
||||
// Unknown sequence capture is independently bounded and disabled by
|
||||
// default. This limit will apply to every supported unknown sequence type.
|
||||
size_t unknown_max_bytes = 256;
|
||||
ghostty_terminal_set(terminal, GHOSTTY_TERMINAL_OPT_UNKNOWN_MAX_BYTES,
|
||||
&unknown_max_bytes);
|
||||
|
||||
// Feed VT data that triggers effects:
|
||||
|
||||
@@ -120,7 +154,13 @@ int main() {
|
||||
ghostty_terminal_vt_write(terminal, (const uint8_t*)clipboard_seq,
|
||||
strlen(clipboard_seq));
|
||||
|
||||
// 5. Another bell to show the counter increments
|
||||
// 5. Unsupported APC sequence
|
||||
printf("Sending unknown APC:\n");
|
||||
const char* unknown_apc = "\x1B_private-command;payload\x1B\\";
|
||||
ghostty_terminal_vt_write(terminal, (const uint8_t*)unknown_apc,
|
||||
strlen(unknown_apc));
|
||||
|
||||
// 6. Another bell to show the counter increments
|
||||
printf("Sending another BEL:\n");
|
||||
ghostty_terminal_vt_write(terminal, &bel, 1);
|
||||
|
||||
|
||||
@@ -97,6 +97,7 @@ extern "C" {
|
||||
* | `GHOSTTY_TERMINAL_OPT_CLIPBOARD_WRITE` | `GhosttyTerminalClipboardWriteFn` | Clipboard write via OSC 52 / OSC 1337 |
|
||||
* | `GHOSTTY_TERMINAL_OPT_DESKTOP_NOTIFICATION`| `GhosttyTerminalDesktopNotificationFn` | Desktop notification via OSC 9 / OSC 777 |
|
||||
* | `GHOSTTY_TERMINAL_OPT_PROGRESS_REPORT` | `GhosttyTerminalProgressReportFn` | Progress report via OSC 9;4 |
|
||||
* | `GHOSTTY_TERMINAL_OPT_UNKNOWN_SEQUENCE` | `GhosttyTerminalUnknownSequenceFn` | Unsupported sequence identifier |
|
||||
*
|
||||
* ### Defining a write_pty callback
|
||||
* @snippet c-vt-effects/src/main.c effects-write-pty
|
||||
@@ -110,6 +111,9 @@ extern "C" {
|
||||
* ### Defining a clipboard_write callback
|
||||
* @snippet c-vt-effects/src/main.c effects-clipboard-write
|
||||
*
|
||||
* ### Defining an unknown_sequence callback
|
||||
* @snippet c-vt-effects/src/main.c effects-unknown-sequence
|
||||
*
|
||||
* ### Registering effects and processing VT data
|
||||
* @snippet c-vt-effects/src/main.c effects-register
|
||||
*
|
||||
@@ -331,6 +335,88 @@ typedef struct {
|
||||
typedef void (*GhosttyTerminalBellFn)(GhosttyTerminal terminal,
|
||||
void* userdata);
|
||||
|
||||
/**
|
||||
* Unsupported terminal sequence tags.
|
||||
*
|
||||
* Only APC sequences are currently reported. Additional sequence types may
|
||||
* be added without changing the callback shape.
|
||||
*
|
||||
* @ingroup terminal
|
||||
*/
|
||||
typedef enum GHOSTTY_ENUM_TYPED {
|
||||
/** Application Program Command (APC). */
|
||||
GHOSTTY_TERMINAL_UNKNOWN_SEQUENCE_APC = 0,
|
||||
GHOSTTY_TERMINAL_UNKNOWN_SEQUENCE_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
|
||||
} GhosttyTerminalUnknownSequenceTag;
|
||||
|
||||
/**
|
||||
* An unsupported string terminal sequence.
|
||||
*
|
||||
* The content is borrowed and valid only for the callback duration. It
|
||||
* contains the bytes between the sequence introducer and terminator, may
|
||||
* contain arbitrary binary data, and is not null-terminated.
|
||||
*
|
||||
* @ingroup terminal
|
||||
*/
|
||||
typedef struct {
|
||||
/** Whether content was shortened by the byte limit or allocation failure. */
|
||||
bool truncated;
|
||||
|
||||
/** Retained sequence content. */
|
||||
GhosttyString content;
|
||||
} GhosttyTerminalUnknownStringSequence;
|
||||
|
||||
/**
|
||||
* Unsupported terminal sequence value.
|
||||
*
|
||||
* @ingroup terminal
|
||||
*/
|
||||
typedef union {
|
||||
/** Application Program Command (APC). */
|
||||
GhosttyTerminalUnknownStringSequence apc;
|
||||
|
||||
/**
|
||||
* Padding for ABI compatibility. Do not use.
|
||||
*
|
||||
* 128 bytes leaves room for future structured sequence payloads, such as
|
||||
* CSI with borrowed parameter, separator, and intermediate arrays, without
|
||||
* changing the tagged union's ABI.
|
||||
*/
|
||||
uint64_t _padding[16];
|
||||
} GhosttyTerminalUnknownSequenceValue;
|
||||
|
||||
/**
|
||||
* An unsupported terminal sequence.
|
||||
*
|
||||
* @ingroup terminal
|
||||
*/
|
||||
typedef struct {
|
||||
GhosttyTerminalUnknownSequenceTag tag;
|
||||
GhosttyTerminalUnknownSequenceValue value;
|
||||
} GhosttyTerminalUnknownSequence;
|
||||
|
||||
/**
|
||||
* Callback function type for unsupported terminal sequences.
|
||||
*
|
||||
* Called synchronously for normally terminated sequences whose identifier is
|
||||
* not supported by the active terminal handler. Aborted sequences, malformed
|
||||
* recognized commands, and explicitly disabled known protocols are ignored.
|
||||
*
|
||||
* Capture must also be enabled with a nonzero
|
||||
* GHOSTTY_TERMINAL_OPT_UNKNOWN_MAX_BYTES value. Installing this callback alone
|
||||
* does not retain sequence content or allocate memory.
|
||||
*
|
||||
* @param terminal The terminal handle
|
||||
* @param userdata The userdata pointer set via GHOSTTY_TERMINAL_OPT_USERDATA
|
||||
* @param sequence Borrowed unsupported sequence
|
||||
*
|
||||
* @ingroup terminal
|
||||
*/
|
||||
typedef void (*GhosttyTerminalUnknownSequenceFn)(
|
||||
GhosttyTerminal terminal,
|
||||
void* userdata,
|
||||
const GhosttyTerminalUnknownSequence* sequence);
|
||||
|
||||
/**
|
||||
* Clipboard destination for a clipboard write.
|
||||
*
|
||||
@@ -1103,6 +1189,27 @@ typedef enum GHOSTTY_ENUM_TYPED {
|
||||
* Input type: GhosttyTerminalModeConfig*
|
||||
*/
|
||||
GHOSTTY_TERMINAL_OPT_MODE = 34,
|
||||
|
||||
/**
|
||||
* Callback invoked for unsupported terminal sequence identifiers. Set to
|
||||
* NULL to ignore unsupported sequences. Capture must also be enabled with
|
||||
* GHOSTTY_TERMINAL_OPT_UNKNOWN_MAX_BYTES.
|
||||
*
|
||||
* Input type: GhosttyTerminalUnknownSequenceFn
|
||||
*/
|
||||
GHOSTTY_TERMINAL_OPT_UNKNOWN_SEQUENCE = 35,
|
||||
|
||||
/**
|
||||
* Set the maximum content bytes retained for each unsupported terminal
|
||||
* sequence. A NULL value pointer or zero disables capture and prevents
|
||||
* unknown-sequence callbacks.
|
||||
*
|
||||
* When this limit is hit, the unknown sequence callback will still
|
||||
* be invoked but `truncated` will be set to true.
|
||||
*
|
||||
* Input type: size_t*
|
||||
*/
|
||||
GHOSTTY_TERMINAL_OPT_UNKNOWN_MAX_BYTES = 36,
|
||||
GHOSTTY_TERMINAL_OPT_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
|
||||
} GhosttyTerminalOption;
|
||||
|
||||
|
||||
@@ -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