mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-07-31 04:39:01 +00:00
vt: expose focus encoding in C and Zig APIs
Add focus event encoding (CSI I / CSI O) to the libghostty-vt public API, following the same patterns as key and mouse encoding. The focus Event enum uses lib.Enum for C ABI compatibility. The C API provides ghostty_focus_encode() which writes into a caller-provided buffer and returns GHOSTTY_OUT_OF_SPACE with the required size when the buffer is too small. Also update key and mouse encoders to return GHOSTTY_OUT_OF_SPACE instead of GHOSTTY_OUT_OF_MEMORY for buffer-too-small errors, reserving OUT_OF_MEMORY for actual allocation failures. Update all corresponding header documentation.
This commit is contained in:
@@ -81,11 +81,17 @@ pub const input = struct {
|
||||
// We have to be careful to only import targeted files within
|
||||
// the input package because the full package brings in too many
|
||||
// other dependencies.
|
||||
const focus = terminal.focus;
|
||||
const paste = @import("input/paste.zig");
|
||||
const key = @import("input/key.zig");
|
||||
const key_encode = @import("input/key_encode.zig");
|
||||
const mouse_encode = @import("input/mouse_encode.zig");
|
||||
|
||||
// Focus-related APIs
|
||||
pub const max_focus_encode_size = focus.max_encode_size;
|
||||
pub const FocusEvent = focus.Event;
|
||||
pub const encodeFocus = focus.encode;
|
||||
|
||||
// Paste-related APIs
|
||||
pub const PasteError = paste.Error;
|
||||
pub const PasteOptions = paste.Options;
|
||||
@@ -158,6 +164,7 @@ comptime {
|
||||
@export(&c.osc_end, .{ .name = "ghostty_osc_end" });
|
||||
@export(&c.osc_command_type, .{ .name = "ghostty_osc_command_type" });
|
||||
@export(&c.osc_command_data, .{ .name = "ghostty_osc_command_data" });
|
||||
@export(&c.focus_encode, .{ .name = "ghostty_focus_encode" });
|
||||
@export(&c.paste_is_safe, .{ .name = "ghostty_paste_is_safe" });
|
||||
@export(&c.color_rgb_get, .{ .name = "ghostty_color_rgb_get" });
|
||||
@export(&c.sgr_new, .{ .name = "ghostty_sgr_new" });
|
||||
|
||||
54
src/terminal/c/focus.zig
Normal file
54
src/terminal/c/focus.zig
Normal file
@@ -0,0 +1,54 @@
|
||||
const std = @import("std");
|
||||
const terminal_focus = @import("../focus.zig");
|
||||
const Result = @import("result.zig").Result;
|
||||
|
||||
pub fn encode(
|
||||
event: terminal_focus.Event,
|
||||
out_: ?[*]u8,
|
||||
out_len: usize,
|
||||
out_written: *usize,
|
||||
) callconv(.c) Result {
|
||||
var writer: std.Io.Writer = .fixed(if (out_) |out| out[0..out_len] else &.{});
|
||||
terminal_focus.encode(&writer, event) catch |err| switch (err) {
|
||||
error.WriteFailed => {
|
||||
var discarding: std.Io.Writer.Discarding = .init(&.{});
|
||||
terminal_focus.encode(&discarding.writer, event) catch unreachable;
|
||||
out_written.* = @intCast(discarding.count);
|
||||
return .out_of_space;
|
||||
},
|
||||
};
|
||||
|
||||
out_written.* = writer.end;
|
||||
return .success;
|
||||
}
|
||||
|
||||
test "encode focus gained" {
|
||||
var buf: [terminal_focus.max_encode_size]u8 = undefined;
|
||||
var written: usize = 0;
|
||||
const result = encode(.gained, &buf, buf.len, &written);
|
||||
try std.testing.expectEqual(.success, result);
|
||||
try std.testing.expectEqualStrings("\x1B[I", buf[0..written]);
|
||||
}
|
||||
|
||||
test "encode focus lost" {
|
||||
var buf: [terminal_focus.max_encode_size]u8 = undefined;
|
||||
var written: usize = 0;
|
||||
const result = encode(.lost, &buf, buf.len, &written);
|
||||
try std.testing.expectEqual(.success, result);
|
||||
try std.testing.expectEqualStrings("\x1B[O", buf[0..written]);
|
||||
}
|
||||
|
||||
test "encode with insufficient buffer" {
|
||||
var buf: [1]u8 = undefined;
|
||||
var written: usize = 0;
|
||||
const result = encode(.gained, &buf, buf.len, &written);
|
||||
try std.testing.expectEqual(.out_of_space, result);
|
||||
try std.testing.expectEqual(terminal_focus.max_encode_size, written);
|
||||
}
|
||||
|
||||
test "encode with null buffer" {
|
||||
var written: usize = 0;
|
||||
const result = encode(.gained, null, 0, &written);
|
||||
try std.testing.expectEqual(.out_of_space, result);
|
||||
try std.testing.expectEqual(terminal_focus.max_encode_size, written);
|
||||
}
|
||||
@@ -152,7 +152,7 @@ pub fn encode(
|
||||
// Discarding always uses a u64. If we're on 32-bit systems
|
||||
// we cast down. We should make this safer in the future.
|
||||
out_written.* = @intCast(discarding.count);
|
||||
return .out_of_memory;
|
||||
return .out_of_space;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -329,7 +329,7 @@ test "encode: kitty ctrl release with ctrl mod set" {
|
||||
|
||||
// Encode null should give us the length required
|
||||
var required: usize = 0;
|
||||
try testing.expectEqual(Result.out_of_memory, encode(
|
||||
try testing.expectEqual(Result.out_of_space, encode(
|
||||
encoder,
|
||||
event,
|
||||
null,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
pub const color = @import("color.zig");
|
||||
pub const focus = @import("focus.zig");
|
||||
pub const formatter = @import("formatter.zig");
|
||||
pub const osc = @import("osc.zig");
|
||||
pub const key_event = @import("key_event.zig");
|
||||
@@ -20,6 +21,8 @@ pub const osc_command_data = osc.commandData;
|
||||
|
||||
pub const color_rgb_get = color.rgb_get;
|
||||
|
||||
pub const focus_encode = focus.encode;
|
||||
|
||||
pub const formatter_terminal_new = formatter.terminal_new;
|
||||
pub const formatter_format_buf = formatter.format_buf;
|
||||
pub const formatter_format_alloc = formatter.format_alloc;
|
||||
@@ -90,6 +93,7 @@ pub const terminal_scroll_viewport = terminal.scroll_viewport;
|
||||
|
||||
test {
|
||||
_ = color;
|
||||
_ = focus;
|
||||
_ = formatter;
|
||||
_ = osc;
|
||||
_ = key_event;
|
||||
|
||||
@@ -240,7 +240,7 @@ pub fn encode(
|
||||
// Discarding always uses a u64. If we're on 32-bit systems
|
||||
// we cast down. We should make this safer in the future.
|
||||
out_written.* = @intCast(discarding.count);
|
||||
return .out_of_memory;
|
||||
return .out_of_space;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -386,7 +386,7 @@ test "encode: sgr press left" {
|
||||
mouse_event.set_position(event, .{ .x = 0, .y = 0 });
|
||||
|
||||
var required: usize = 0;
|
||||
try testing.expectEqual(Result.out_of_memory, encode(
|
||||
try testing.expectEqual(Result.out_of_space, encode(
|
||||
encoder,
|
||||
event,
|
||||
null,
|
||||
@@ -507,7 +507,7 @@ test "encode: querying required size doesn't update dedupe state" {
|
||||
mouse_event.set_position(event, .{ .x = 5, .y = 6 });
|
||||
|
||||
var required: usize = 0;
|
||||
try testing.expectEqual(Result.out_of_memory, encode(
|
||||
try testing.expectEqual(Result.out_of_space, encode(
|
||||
encoder,
|
||||
event,
|
||||
null,
|
||||
|
||||
41
src/terminal/focus.zig
Normal file
41
src/terminal/focus.zig
Normal file
@@ -0,0 +1,41 @@
|
||||
const std = @import("std");
|
||||
const build_options = @import("terminal_options");
|
||||
const lib = @import("../lib/main.zig");
|
||||
const lib_target: lib.Target = if (build_options.c_abi) .c else .zig;
|
||||
|
||||
/// Maximum number of bytes that `encode` will write. Any users of this
|
||||
/// should be resilient to this changing, so this is always a specific
|
||||
/// value (e.g. we don't add unnecessary padding).
|
||||
pub const max_encode_size = 3;
|
||||
|
||||
/// A focus event that can be reported to the application running in the
|
||||
/// terminal when focus reporting mode (mode 1004) is enabled.
|
||||
pub const Event = lib.Enum(lib_target, &.{
|
||||
"gained",
|
||||
"lost",
|
||||
});
|
||||
|
||||
/// Encode a focus in/out report (CSI I / CSI O).
|
||||
pub fn encode(
|
||||
writer: *std.Io.Writer,
|
||||
event: Event,
|
||||
) std.Io.Writer.Error!void {
|
||||
try writer.writeAll(switch (event) {
|
||||
.gained => "\x1B[I",
|
||||
.lost => "\x1B[O",
|
||||
});
|
||||
}
|
||||
|
||||
test "encode focus gained" {
|
||||
var buf: [max_encode_size]u8 = undefined;
|
||||
var writer: std.Io.Writer = .fixed(&buf);
|
||||
try encode(&writer, .gained);
|
||||
try std.testing.expectEqualStrings("\x1B[I", writer.buffered());
|
||||
}
|
||||
|
||||
test "encode focus lost" {
|
||||
var buf: [max_encode_size]u8 = undefined;
|
||||
var writer: std.Io.Writer = .fixed(&buf);
|
||||
try encode(&writer, .lost);
|
||||
try std.testing.expectEqualStrings("\x1B[O", writer.buffered());
|
||||
}
|
||||
@@ -11,6 +11,7 @@ pub const osc = @import("osc.zig");
|
||||
pub const point = @import("point.zig");
|
||||
pub const color = @import("color.zig");
|
||||
pub const device_status = @import("device_status.zig");
|
||||
pub const focus = @import("focus.zig");
|
||||
pub const formatter = @import("formatter.zig");
|
||||
pub const highlight = @import("highlight.zig");
|
||||
pub const kitty = @import("kitty.zig");
|
||||
|
||||
@@ -664,8 +664,13 @@ pub fn focusGained(self: *Termio, td: *ThreadData, focused: bool) !void {
|
||||
|
||||
// If we have focus events enabled, we send the focus event.
|
||||
if (focus_event) {
|
||||
const seq = if (focused) "\x1b[I" else "\x1b[O";
|
||||
try self.queueWrite(td, seq, false);
|
||||
var buf: [terminalpkg.focus.max_encode_size]u8 = undefined;
|
||||
var writer: std.Io.Writer = .fixed(&buf);
|
||||
terminalpkg.focus.encode(&writer, if (focused) .gained else .lost) catch |err| {
|
||||
log.err("error encoding focus event err={}", .{err});
|
||||
return;
|
||||
};
|
||||
try self.queueWrite(td, writer.buffered(), false);
|
||||
}
|
||||
|
||||
// We always notify our backend of focus changes.
|
||||
|
||||
Reference in New Issue
Block a user