vt: add enquiry and xtversion effect callbacks
Add GHOSTTY_TERMINAL_OPT_ENQUIRY and GHOSTTY_TERMINAL_OPT_XTVERSION so C consumers can respond to ENQ (0x05) and XTVERSION (CSI > q) queries. Both callbacks return a GhosttyString rather than using out-pointers. Introduce GhosttyString in types.h as a borrowed byte string (ptr + len) backed by lib.String on the Zig side. This will be reusable for future callbacks that need to return string data. Without an xtversion callback the trampoline returns an empty string, which causes the handler to report the default "libghostty" version. Without an enquiry callback no response is sent.pull/11814/head
parent
c13a9bb49c
commit
f9c34b40f0
|
|
@ -167,6 +167,40 @@ typedef void (*GhosttyTerminalWritePtyFn)(GhosttyTerminal terminal,
|
|||
typedef void (*GhosttyTerminalBellFn)(GhosttyTerminal terminal,
|
||||
void* userdata);
|
||||
|
||||
/**
|
||||
* Callback function type for enquiry (ENQ, 0x05).
|
||||
*
|
||||
* Called when the terminal receives an ENQ character. Return the
|
||||
* response bytes as a GhosttyString. The memory must remain valid
|
||||
* until the callback returns. Return a zero-length string to send
|
||||
* no response.
|
||||
*
|
||||
* @param terminal The terminal handle
|
||||
* @param userdata The userdata pointer set via GHOSTTY_TERMINAL_OPT_USERDATA
|
||||
* @return The response bytes to write back to the pty
|
||||
*
|
||||
* @ingroup terminal
|
||||
*/
|
||||
typedef GhosttyString (*GhosttyTerminalEnquiryFn)(GhosttyTerminal terminal,
|
||||
void* userdata);
|
||||
|
||||
/**
|
||||
* Callback function type for XTVERSION.
|
||||
*
|
||||
* Called when the terminal receives an XTVERSION query (CSI > q).
|
||||
* Return the version string (e.g. "myterm 1.0") as a GhosttyString.
|
||||
* The memory must remain valid until the callback returns. Return a
|
||||
* zero-length string to report the default "libghostty" version.
|
||||
*
|
||||
* @param terminal The terminal handle
|
||||
* @param userdata The userdata pointer set via GHOSTTY_TERMINAL_OPT_USERDATA
|
||||
* @return The version string to report
|
||||
*
|
||||
* @ingroup terminal
|
||||
*/
|
||||
typedef GhosttyString (*GhosttyTerminalXtversionFn)(GhosttyTerminal terminal,
|
||||
void* userdata);
|
||||
|
||||
/**
|
||||
* Terminal option identifiers.
|
||||
*
|
||||
|
|
@ -199,6 +233,22 @@ typedef enum {
|
|||
* Input type: GhosttyTerminalBellFn*
|
||||
*/
|
||||
GHOSTTY_TERMINAL_OPT_BELL = 2,
|
||||
|
||||
/**
|
||||
* Callback invoked when the terminal receives an ENQ character
|
||||
* (0x05). Set to NULL to send no response.
|
||||
*
|
||||
* Input type: GhosttyTerminalEnquiryFn*
|
||||
*/
|
||||
GHOSTTY_TERMINAL_OPT_ENQUIRY = 3,
|
||||
|
||||
/**
|
||||
* Callback invoked when the terminal receives an XTVERSION query
|
||||
* (CSI > q). Set to NULL to report the default "libghostty" string.
|
||||
*
|
||||
* Input type: GhosttyTerminalXtversionFn*
|
||||
*/
|
||||
GHOSTTY_TERMINAL_OPT_XTVERSION = 4,
|
||||
} GhosttyTerminalOption;
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@
|
|||
#ifndef GHOSTTY_VT_TYPES_H
|
||||
#define GHOSTTY_VT_TYPES_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/**
|
||||
* Result codes for libghostty-vt operations.
|
||||
*/
|
||||
|
|
@ -21,6 +24,20 @@ typedef enum {
|
|||
GHOSTTY_OUT_OF_SPACE = -3,
|
||||
} GhosttyResult;
|
||||
|
||||
/**
|
||||
* A borrowed byte string (pointer + length).
|
||||
*
|
||||
* The memory is not owned by this struct. The pointer is only valid
|
||||
* for the lifetime documented by the API that produces or consumes it.
|
||||
*/
|
||||
typedef struct {
|
||||
/** Pointer to the string bytes. */
|
||||
const uint8_t* ptr;
|
||||
|
||||
/** Length of the string in bytes. */
|
||||
size_t len;
|
||||
} GhosttyString;
|
||||
|
||||
/**
|
||||
* Initialize a sized struct to zero and set its size field.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
const std = @import("std");
|
||||
const testing = std.testing;
|
||||
const lib = @import("../../lib/main.zig");
|
||||
const lib_alloc = @import("../../lib/allocator.zig");
|
||||
const CAllocator = lib_alloc.Allocator;
|
||||
const ZigTerminal = @import("../Terminal.zig");
|
||||
|
|
@ -36,6 +37,8 @@ const Effects = struct {
|
|||
userdata: ?*anyopaque = null,
|
||||
write_pty: ?WritePtyFn = null,
|
||||
bell: ?BellFn = null,
|
||||
enquiry: ?EnquiryFn = null,
|
||||
xtversion: ?XtversionFn = null,
|
||||
|
||||
/// C function pointer type for the write_pty callback.
|
||||
pub const WritePtyFn = *const fn (Terminal, ?*anyopaque, [*]const u8, usize) callconv(.c) void;
|
||||
|
|
@ -43,6 +46,17 @@ const Effects = struct {
|
|||
/// C function pointer type for the bell callback.
|
||||
pub const BellFn = *const fn (Terminal, ?*anyopaque) callconv(.c) void;
|
||||
|
||||
/// 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(.c) 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(.c) lib.String;
|
||||
|
||||
fn writePtyTrampoline(handler: *Handler, data: [:0]const u8) void {
|
||||
const stream_ptr: *Stream = @fieldParentPtr("handler", handler);
|
||||
const wrapper: *TerminalWrapper = @fieldParentPtr("stream", stream_ptr);
|
||||
|
|
@ -56,6 +70,24 @@ const Effects = struct {
|
|||
const func = wrapper.effects.bell orelse return;
|
||||
func(@ptrCast(wrapper), wrapper.effects.userdata);
|
||||
}
|
||||
|
||||
fn enquiryTrampoline(handler: *Handler) []const u8 {
|
||||
const stream_ptr: *Stream = @fieldParentPtr("handler", handler);
|
||||
const wrapper: *TerminalWrapper = @fieldParentPtr("stream", stream_ptr);
|
||||
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 stream_ptr: *Stream = @fieldParentPtr("handler", handler);
|
||||
const wrapper: *TerminalWrapper = @fieldParentPtr("stream", stream_ptr);
|
||||
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];
|
||||
}
|
||||
};
|
||||
|
||||
/// C: GhosttyTerminal
|
||||
|
|
@ -117,6 +149,8 @@ fn new_(
|
|||
var handler: Stream.Handler = t.vtHandler();
|
||||
handler.effects.write_pty = &Effects.writePtyTrampoline;
|
||||
handler.effects.bell = &Effects.bellTrampoline;
|
||||
handler.effects.enquiry = &Effects.enquiryTrampoline;
|
||||
handler.effects.xtversion = &Effects.xtversionTrampoline;
|
||||
|
||||
wrapper.* = .{
|
||||
.terminal = t,
|
||||
|
|
@ -140,6 +174,8 @@ pub const Option = enum(c_int) {
|
|||
userdata = 0,
|
||||
write_pty = 1,
|
||||
bell = 2,
|
||||
enquiry = 3,
|
||||
xtversion = 4,
|
||||
|
||||
/// Input type expected for setting the option.
|
||||
pub fn InType(comptime self: Option) type {
|
||||
|
|
@ -147,6 +183,8 @@ pub const Option = enum(c_int) {
|
|||
.userdata => ?*anyopaque,
|
||||
.write_pty => ?Effects.WritePtyFn,
|
||||
.bell => ?Effects.BellFn,
|
||||
.enquiry => ?Effects.EnquiryFn,
|
||||
.xtversion => ?Effects.XtversionFn,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
|
@ -182,6 +220,8 @@ fn setTyped(
|
|||
.userdata => wrapper.effects.userdata = if (value) |v| v.* else null,
|
||||
.write_pty => wrapper.effects.write_pty = if (value) |v| v.* else null,
|
||||
.bell => wrapper.effects.bell = if (value) |v| v.* else null,
|
||||
.enquiry => wrapper.effects.enquiry = if (value) |v| v.* else null,
|
||||
.xtversion => wrapper.effects.xtversion = if (value) |v| v.* else null,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1015,6 +1055,149 @@ test "bell without callback is silent" {
|
|||
vt_write(t, "\x07", 1);
|
||||
}
|
||||
|
||||
test "set enquiry callback" {
|
||||
var t: Terminal = null;
|
||||
try testing.expectEqual(Result.success, new(
|
||||
&lib_alloc.test_allocator,
|
||||
&t,
|
||||
.{
|
||||
.cols = 80,
|
||||
.rows = 24,
|
||||
.max_scrollback = 0,
|
||||
},
|
||||
));
|
||||
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(.c) 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(.c) lib.String {
|
||||
return .{ .ptr = response, .len = response.len };
|
||||
}
|
||||
};
|
||||
defer S.deinit();
|
||||
|
||||
const write_cb: ?Effects.WritePtyFn = &S.writePty;
|
||||
set(t, .write_pty, @ptrCast(&write_cb));
|
||||
const enq_cb: ?Effects.EnquiryFn = &S.enquiry;
|
||||
set(t, .enquiry, @ptrCast(&enq_cb));
|
||||
|
||||
// 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,
|
||||
.{
|
||||
.cols = 80,
|
||||
.rows = 24,
|
||||
.max_scrollback = 0,
|
||||
},
|
||||
));
|
||||
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,
|
||||
.{
|
||||
.cols = 80,
|
||||
.rows = 24,
|
||||
.max_scrollback = 0,
|
||||
},
|
||||
));
|
||||
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(.c) 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(.c) lib.String {
|
||||
return .{ .ptr = version, .len = version.len };
|
||||
}
|
||||
};
|
||||
defer S.deinit();
|
||||
|
||||
const write_cb: ?Effects.WritePtyFn = &S.writePty;
|
||||
set(t, .write_pty, @ptrCast(&write_cb));
|
||||
const xtv_cb: ?Effects.XtversionFn = &S.xtversion;
|
||||
set(t, .xtversion, @ptrCast(&xtv_cb));
|
||||
|
||||
// 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,
|
||||
.{
|
||||
.cols = 80,
|
||||
.rows = 24,
|
||||
.max_scrollback = 0,
|
||||
},
|
||||
));
|
||||
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(.c) 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"
|
||||
const write_cb: ?Effects.WritePtyFn = &S.writePty;
|
||||
set(t, .write_pty, @ptrCast(&write_cb));
|
||||
|
||||
vt_write(t, "\x1B[>q", 4);
|
||||
try testing.expect(S.last_data != null);
|
||||
try testing.expectEqualStrings("\x1BP>|libghostty\x1B\\", S.last_data.?);
|
||||
}
|
||||
|
||||
test "grid_ref out of bounds" {
|
||||
var t: Terminal = null;
|
||||
try testing.expectEqual(Result.success, new(
|
||||
|
|
|
|||
Loading…
Reference in New Issue