mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-06 07:38:21 +00:00
vt: expose title and pwd in C API
Add title and pwd as both gettable data keys (GHOSTTY_TERMINAL_DATA_TITLE/PWD) and settable options (GHOSTTY_TERMINAL_OPT_TITLE/PWD) in the C terminal API. Getting returns a borrowed GhosttyString; setting copies the data into the terminal via setTitle/setPwd. The underlying Terminal.setTitle/setPwd now append a null sentinel so that getTitle/getPwd can return sentinel-terminated slices ([:0]const u8), which is useful for downstream consumers that need C strings. Change ghostty_terminal_set to return GhosttyResult instead of void so that the new title/pwd options can report allocation failures. Existing option-setting calls cannot fail so the return value is backwards-compatible for callers that discard it.
This commit is contained in:
@@ -414,6 +414,26 @@ typedef enum {
|
||||
* Input type: GhosttyTerminalDeviceAttributesFn*
|
||||
*/
|
||||
GHOSTTY_TERMINAL_OPT_DEVICE_ATTRIBUTES = 8,
|
||||
|
||||
/**
|
||||
* Set the terminal title manually.
|
||||
*
|
||||
* The string data is copied into the terminal. A NULL value pointer
|
||||
* clears the title (equivalent to setting an empty string).
|
||||
*
|
||||
* Input type: GhosttyString*
|
||||
*/
|
||||
GHOSTTY_TERMINAL_OPT_TITLE = 9,
|
||||
|
||||
/**
|
||||
* Set the terminal working directory manually.
|
||||
*
|
||||
* The string data is copied into the terminal. A NULL value pointer
|
||||
* clears the pwd (equivalent to setting an empty string).
|
||||
*
|
||||
* Input type: GhosttyString*
|
||||
*/
|
||||
GHOSTTY_TERMINAL_OPT_PWD = 10,
|
||||
} GhosttyTerminalOption;
|
||||
|
||||
/**
|
||||
@@ -513,6 +533,29 @@ typedef enum {
|
||||
* Output type: bool *
|
||||
*/
|
||||
GHOSTTY_TERMINAL_DATA_MOUSE_TRACKING = 11,
|
||||
|
||||
/**
|
||||
* The terminal title as set by escape sequences (e.g. OSC 0/2).
|
||||
*
|
||||
* Returns a borrowed string. The pointer is valid until the next call
|
||||
* to ghostty_terminal_vt_write() or ghostty_terminal_reset(). An empty
|
||||
* string (len=0) is returned when no title has been set.
|
||||
*
|
||||
* Output type: GhosttyString *
|
||||
*/
|
||||
GHOSTTY_TERMINAL_DATA_TITLE = 12,
|
||||
|
||||
/**
|
||||
* The terminal's current working directory as set by escape sequences
|
||||
* (e.g. OSC 7).
|
||||
*
|
||||
* Returns a borrowed string. The pointer is valid until the next call
|
||||
* to ghostty_terminal_vt_write() or ghostty_terminal_reset(). An empty
|
||||
* string (len=0) is returned when no pwd has been set.
|
||||
*
|
||||
* Output type: GhosttyString *
|
||||
*/
|
||||
GHOSTTY_TERMINAL_DATA_PWD = 13,
|
||||
} GhosttyTerminalData;
|
||||
|
||||
/**
|
||||
@@ -590,9 +633,9 @@ GhosttyResult ghostty_terminal_resize(GhosttyTerminal terminal,
|
||||
*
|
||||
* @ingroup terminal
|
||||
*/
|
||||
void ghostty_terminal_set(GhosttyTerminal terminal,
|
||||
GhosttyTerminalOption option,
|
||||
const void* value);
|
||||
GhosttyResult ghostty_terminal_set(GhosttyTerminal terminal,
|
||||
GhosttyTerminalOption option,
|
||||
const void* value);
|
||||
|
||||
/**
|
||||
* Write VT-encoded data to the terminal for processing.
|
||||
|
||||
@@ -2876,27 +2876,33 @@ pub fn resize(
|
||||
/// Set the pwd for the terminal.
|
||||
pub fn setPwd(self: *Terminal, pwd: []const u8) !void {
|
||||
self.pwd.clearRetainingCapacity();
|
||||
try self.pwd.appendSlice(self.gpa(), pwd);
|
||||
if (pwd.len > 0) {
|
||||
try self.pwd.appendSlice(self.gpa(), pwd);
|
||||
try self.pwd.append(self.gpa(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the pwd for the terminal, if any. The memory is owned by the
|
||||
/// Terminal and is not copied. It is safe until a reset or setPwd.
|
||||
pub fn getPwd(self: *const Terminal) ?[]const u8 {
|
||||
pub fn getPwd(self: *const Terminal) ?[:0]const u8 {
|
||||
if (self.pwd.items.len == 0) return null;
|
||||
return self.pwd.items;
|
||||
return self.pwd.items[0 .. self.pwd.items.len - 1 :0];
|
||||
}
|
||||
|
||||
/// Set the title for the terminal, as set by escape sequences (e.g. OSC 0/2).
|
||||
pub fn setTitle(self: *Terminal, t: []const u8) !void {
|
||||
self.title.clearRetainingCapacity();
|
||||
try self.title.appendSlice(self.gpa(), t);
|
||||
if (t.len > 0) {
|
||||
try self.title.appendSlice(self.gpa(), t);
|
||||
try self.title.append(self.gpa(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the title for the terminal, if any. The memory is owned by the
|
||||
/// Terminal and is not copied. It is safe until a reset or setTitle.
|
||||
pub fn getTitle(self: *const Terminal) ?[]const u8 {
|
||||
pub fn getTitle(self: *const Terminal) ?[:0]const u8 {
|
||||
if (self.title.items.len == 0) return null;
|
||||
return self.title.items;
|
||||
return self.title.items[0 .. self.title.items.len - 1 :0];
|
||||
}
|
||||
|
||||
/// Switch to the given screen type (alternate or primary).
|
||||
|
||||
@@ -298,6 +298,8 @@ pub const Option = enum(c_int) {
|
||||
size_cb = 6,
|
||||
color_scheme = 7,
|
||||
device_attributes = 8,
|
||||
title = 9,
|
||||
pwd = 10,
|
||||
|
||||
/// Input type expected for setting the option.
|
||||
pub fn InType(comptime self: Option) type {
|
||||
@@ -311,6 +313,7 @@ pub const Option = enum(c_int) {
|
||||
.xtversion => ?Effects.XtversionFn,
|
||||
.title_changed => ?Effects.TitleChangedFn,
|
||||
.size_cb => ?Effects.SizeFn,
|
||||
.title, .pwd => lib.String,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -319,11 +322,11 @@ pub fn set(
|
||||
terminal_: Terminal,
|
||||
option: Option,
|
||||
value: ?*const anyopaque,
|
||||
) callconv(.c) void {
|
||||
) callconv(.c) Result {
|
||||
if (comptime std.debug.runtime_safety) {
|
||||
_ = std.meta.intToEnum(Option, @intFromEnum(option)) catch {
|
||||
log.warn("terminal_set invalid option value={d}", .{@intFromEnum(option)});
|
||||
return;
|
||||
return .invalid_value;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -340,8 +343,8 @@ fn setTyped(
|
||||
terminal_: Terminal,
|
||||
comptime option: Option,
|
||||
value: ?*const option.InType(),
|
||||
) void {
|
||||
const wrapper = terminal_ orelse return;
|
||||
) Result {
|
||||
const wrapper = terminal_ orelse return .invalid_value;
|
||||
switch (option) {
|
||||
.userdata => wrapper.effects.userdata = if (value) |v| v.* else null,
|
||||
.write_pty => wrapper.effects.write_pty = if (value) |v| v.* else null,
|
||||
@@ -352,7 +355,16 @@ fn setTyped(
|
||||
.xtversion => wrapper.effects.xtversion = if (value) |v| v.* else null,
|
||||
.title_changed => wrapper.effects.title_changed = if (value) |v| v.* else null,
|
||||
.size_cb => wrapper.effects.size_cb = if (value) |v| v.* else null,
|
||||
.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;
|
||||
},
|
||||
}
|
||||
return .success;
|
||||
}
|
||||
|
||||
/// C: GhosttyTerminalScrollViewport
|
||||
@@ -430,6 +442,8 @@ pub const TerminalData = enum(c_int) {
|
||||
scrollbar = 9,
|
||||
cursor_style = 10,
|
||||
mouse_tracking = 11,
|
||||
title = 12,
|
||||
pwd = 13,
|
||||
|
||||
/// Output type expected for querying the data of the given kind.
|
||||
pub fn OutType(comptime self: TerminalData) type {
|
||||
@@ -441,6 +455,7 @@ pub const TerminalData = enum(c_int) {
|
||||
.kitty_keyboard_flags => u8,
|
||||
.scrollbar => TerminalScrollbar,
|
||||
.cursor_style => style_c.Style,
|
||||
.title, .pwd => lib.String,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -489,6 +504,14 @@ fn getTyped(
|
||||
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 };
|
||||
},
|
||||
}
|
||||
|
||||
return .success;
|
||||
@@ -1068,9 +1091,9 @@ test "set write_pty callback" {
|
||||
// Set userdata and write_pty callback
|
||||
var sentinel: u8 = 42;
|
||||
const ud: ?*anyopaque = @ptrCast(&sentinel);
|
||||
set(t, .userdata, @ptrCast(&ud));
|
||||
try testing.expectEqual(Result.success, set(t, .userdata, @ptrCast(&ud)));
|
||||
const cb: ?Effects.WritePtyFn = &S.writePty;
|
||||
set(t, .write_pty, @ptrCast(&cb));
|
||||
try testing.expectEqual(Result.success, set(t, .write_pty, @ptrCast(&cb)));
|
||||
|
||||
// DECRQM for wraparound mode (mode 7, set by default) should trigger write_pty
|
||||
vt_write(t, "\x1B[?7$p", 6);
|
||||
@@ -1119,8 +1142,8 @@ test "set write_pty null clears callback" {
|
||||
|
||||
// Set then clear the callback
|
||||
const cb: ?Effects.WritePtyFn = &S.writePty;
|
||||
set(t, .write_pty, @ptrCast(&cb));
|
||||
set(t, .write_pty, null);
|
||||
try testing.expectEqual(Result.success, set(t, .write_pty, @ptrCast(&cb)));
|
||||
try testing.expectEqual(Result.success, set(t, .write_pty, null));
|
||||
|
||||
vt_write(t, "\x1B[?7$p", 6);
|
||||
try testing.expect(!S.called);
|
||||
@@ -1154,9 +1177,9 @@ test "set bell callback" {
|
||||
// Set userdata and bell callback
|
||||
var sentinel: u8 = 99;
|
||||
const ud: ?*anyopaque = @ptrCast(&sentinel);
|
||||
set(t, .userdata, @ptrCast(&ud));
|
||||
try testing.expectEqual(Result.success, set(t, .userdata, @ptrCast(&ud)));
|
||||
const cb: ?Effects.BellFn = &S.bell;
|
||||
set(t, .bell, @ptrCast(&cb));
|
||||
try testing.expectEqual(Result.success, set(t, .bell, @ptrCast(&cb)));
|
||||
|
||||
// Single BEL
|
||||
vt_write(t, "\x07", 1);
|
||||
@@ -1219,9 +1242,9 @@ test "set enquiry callback" {
|
||||
defer S.deinit();
|
||||
|
||||
const write_cb: ?Effects.WritePtyFn = &S.writePty;
|
||||
set(t, .write_pty, @ptrCast(&write_cb));
|
||||
try testing.expectEqual(Result.success, set(t, .write_pty, @ptrCast(&write_cb)));
|
||||
const enq_cb: ?Effects.EnquiryFn = &S.enquiry;
|
||||
set(t, .enquiry, @ptrCast(&enq_cb));
|
||||
try testing.expectEqual(Result.success, set(t, .enquiry, @ptrCast(&enq_cb)));
|
||||
|
||||
// ENQ (0x05) should trigger the enquiry callback and write response via write_pty
|
||||
vt_write(t, "\x05", 1);
|
||||
@@ -1280,9 +1303,9 @@ test "set xtversion callback" {
|
||||
defer S.deinit();
|
||||
|
||||
const write_cb: ?Effects.WritePtyFn = &S.writePty;
|
||||
set(t, .write_pty, @ptrCast(&write_cb));
|
||||
try testing.expectEqual(Result.success, set(t, .write_pty, @ptrCast(&write_cb)));
|
||||
const xtv_cb: ?Effects.XtversionFn = &S.xtversion;
|
||||
set(t, .xtversion, @ptrCast(&xtv_cb));
|
||||
try testing.expectEqual(Result.success, set(t, .xtversion, @ptrCast(&xtv_cb)));
|
||||
|
||||
// XTVERSION: CSI > q
|
||||
vt_write(t, "\x1B[>q", 4);
|
||||
@@ -1321,7 +1344,7 @@ test "xtversion without callback reports default" {
|
||||
|
||||
// Set write_pty but not xtversion — should get default "libghostty"
|
||||
const write_cb: ?Effects.WritePtyFn = &S.writePty;
|
||||
set(t, .write_pty, @ptrCast(&write_cb));
|
||||
try testing.expectEqual(Result.success, set(t, .write_pty, @ptrCast(&write_cb)));
|
||||
|
||||
vt_write(t, "\x1B[>q", 4);
|
||||
try testing.expect(S.last_data != null);
|
||||
@@ -1355,9 +1378,9 @@ test "set title_changed callback" {
|
||||
|
||||
var sentinel: u8 = 77;
|
||||
const ud: ?*anyopaque = @ptrCast(&sentinel);
|
||||
set(t, .userdata, @ptrCast(&ud));
|
||||
try testing.expectEqual(Result.success, set(t, .userdata, @ptrCast(&ud)));
|
||||
const cb: ?Effects.TitleChangedFn = &S.titleChanged;
|
||||
set(t, .title_changed, @ptrCast(&cb));
|
||||
try testing.expectEqual(Result.success, set(t, .title_changed, @ptrCast(&cb)));
|
||||
|
||||
// OSC 2 ; title ST — set window title
|
||||
vt_write(t, "\x1B]2;Hello\x1B\\", 10);
|
||||
@@ -1425,9 +1448,9 @@ test "set size callback" {
|
||||
defer S.deinit();
|
||||
|
||||
const write_cb: ?Effects.WritePtyFn = &S.writePty;
|
||||
set(t, .write_pty, @ptrCast(&write_cb));
|
||||
try testing.expectEqual(Result.success, set(t, .write_pty, @ptrCast(&write_cb)));
|
||||
const size_cb_fn: ?Effects.SizeFn = &S.sizeCb;
|
||||
set(t, .size_cb, @ptrCast(&size_cb_fn));
|
||||
try testing.expectEqual(Result.success, set(t, .size_cb, @ptrCast(&size_cb_fn)));
|
||||
|
||||
// CSI 18 t — report text area size in characters
|
||||
vt_write(t, "\x1B[18t", 5);
|
||||
@@ -1498,9 +1521,9 @@ test "set device_attributes callback primary" {
|
||||
defer S.deinit();
|
||||
|
||||
const write_cb: ?Effects.WritePtyFn = &S.writePty;
|
||||
set(t, .write_pty, @ptrCast(&write_cb));
|
||||
try testing.expectEqual(Result.success, set(t, .write_pty, @ptrCast(&write_cb)));
|
||||
const da_cb: ?Effects.DeviceAttributesFn = &S.da;
|
||||
set(t, .device_attributes, @ptrCast(&da_cb));
|
||||
try testing.expectEqual(Result.success, set(t, .device_attributes, @ptrCast(&da_cb)));
|
||||
|
||||
// CSI c — primary DA
|
||||
vt_write(t, "\x1B[c", 3);
|
||||
@@ -1554,9 +1577,9 @@ test "set device_attributes callback secondary" {
|
||||
defer S.deinit();
|
||||
|
||||
const write_cb: ?Effects.WritePtyFn = &S.writePty;
|
||||
set(t, .write_pty, @ptrCast(&write_cb));
|
||||
try testing.expectEqual(Result.success, set(t, .write_pty, @ptrCast(&write_cb)));
|
||||
const da_cb: ?Effects.DeviceAttributesFn = &S.da;
|
||||
set(t, .device_attributes, @ptrCast(&da_cb));
|
||||
try testing.expectEqual(Result.success, set(t, .device_attributes, @ptrCast(&da_cb)));
|
||||
|
||||
// CSI > c — secondary DA
|
||||
vt_write(t, "\x1B[>c", 4);
|
||||
@@ -1610,9 +1633,9 @@ test "set device_attributes callback tertiary" {
|
||||
defer S.deinit();
|
||||
|
||||
const write_cb: ?Effects.WritePtyFn = &S.writePty;
|
||||
set(t, .write_pty, @ptrCast(&write_cb));
|
||||
try testing.expectEqual(Result.success, set(t, .write_pty, @ptrCast(&write_cb)));
|
||||
const da_cb: ?Effects.DeviceAttributesFn = &S.da;
|
||||
set(t, .device_attributes, @ptrCast(&da_cb));
|
||||
try testing.expectEqual(Result.success, set(t, .device_attributes, @ptrCast(&da_cb)));
|
||||
|
||||
// CSI = c — tertiary DA
|
||||
vt_write(t, "\x1B[=c", 4);
|
||||
@@ -1649,7 +1672,7 @@ test "device_attributes without callback uses default" {
|
||||
defer S.deinit();
|
||||
|
||||
const write_cb: ?Effects.WritePtyFn = &S.writePty;
|
||||
set(t, .write_pty, @ptrCast(&write_cb));
|
||||
try testing.expectEqual(Result.success, set(t, .write_pty, @ptrCast(&write_cb)));
|
||||
|
||||
// Without setting a device_attributes callback, DA1 should return the default
|
||||
vt_write(t, "\x1B[c", 3);
|
||||
@@ -1690,9 +1713,9 @@ test "device_attributes callback returns false uses default" {
|
||||
defer S.deinit();
|
||||
|
||||
const write_cb: ?Effects.WritePtyFn = &S.writePty;
|
||||
set(t, .write_pty, @ptrCast(&write_cb));
|
||||
try testing.expectEqual(Result.success, set(t, .write_pty, @ptrCast(&write_cb)));
|
||||
const da_cb: ?Effects.DeviceAttributesFn = &S.da;
|
||||
set(t, .device_attributes, @ptrCast(&da_cb));
|
||||
try testing.expectEqual(Result.success, set(t, .device_attributes, @ptrCast(&da_cb)));
|
||||
|
||||
// Callback returns false, should use default response
|
||||
vt_write(t, "\x1B[c", 3);
|
||||
@@ -1700,6 +1723,98 @@ test "device_attributes callback returns false uses default" {
|
||||
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,
|
||||
.{
|
||||
.cols = 80,
|
||||
.rows = 24,
|
||||
.max_scrollback = 0,
|
||||
},
|
||||
));
|
||||
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,
|
||||
.{
|
||||
.cols = 80,
|
||||
.rows = 24,
|
||||
.max_scrollback = 0,
|
||||
},
|
||||
));
|
||||
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,
|
||||
.{
|
||||
.cols = 80,
|
||||
.rows = 24,
|
||||
.max_scrollback = 0,
|
||||
},
|
||||
));
|
||||
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 "grid_ref out of bounds" {
|
||||
var t: Terminal = null;
|
||||
try testing.expectEqual(Result.success, new(
|
||||
|
||||
Reference in New Issue
Block a user