mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-13 19:15:48 +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:
@@ -30,14 +30,17 @@
|
||||
* The API is organized into the following groups:
|
||||
* - @ref terminal "Terminal" - Complete terminal emulator state and rendering
|
||||
* - @ref formatter "Formatter" - Format terminal content as plain text, VT sequences, or HTML
|
||||
* - @ref key "Key Encoding" - Encode key events into terminal sequences
|
||||
* - @ref mouse "Mouse Encoding" - Encode mouse events into terminal sequences
|
||||
* - @ref osc "OSC Parser" - Parse OSC (Operating System Command) sequences
|
||||
* - @ref sgr "SGR Parser" - Parse SGR (Select Graphic Rendition) sequences
|
||||
* - @ref paste "Paste Utilities" - Validate paste data safety
|
||||
* - @ref allocator "Memory Management" - Memory management and custom allocators
|
||||
* - @ref wasm "WebAssembly Utilities" - WebAssembly convenience functions
|
||||
*
|
||||
* Encoding related APIs:
|
||||
* - @ref focus "Focus Encoding" - Encode focus in/out events into terminal sequences
|
||||
* - @ref key "Key Encoding" - Encode key events into terminal sequences
|
||||
* - @ref mouse "Mouse Encoding" - Encode mouse events into terminal sequences
|
||||
*
|
||||
* @section examples_sec Examples
|
||||
*
|
||||
* Complete working examples:
|
||||
@@ -90,6 +93,7 @@ extern "C" {
|
||||
|
||||
#include <ghostty/vt/types.h>
|
||||
#include <ghostty/vt/allocator.h>
|
||||
#include <ghostty/vt/focus.h>
|
||||
#include <ghostty/vt/formatter.h>
|
||||
#include <ghostty/vt/terminal.h>
|
||||
#include <ghostty/vt/osc.h>
|
||||
|
||||
94
include/ghostty/vt/focus.h
Normal file
94
include/ghostty/vt/focus.h
Normal file
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* @file focus.h
|
||||
*
|
||||
* Focus encoding - encode focus in/out events into terminal escape sequences.
|
||||
*/
|
||||
|
||||
#ifndef GHOSTTY_VT_FOCUS_H
|
||||
#define GHOSTTY_VT_FOCUS_H
|
||||
|
||||
/** @defgroup focus Focus Encoding
|
||||
*
|
||||
* Utilities for encoding focus gained/lost events into terminal escape
|
||||
* sequences (CSI I / CSI O) for focus reporting mode (mode 1004).
|
||||
*
|
||||
* ## Basic Usage
|
||||
*
|
||||
* Use ghostty_focus_encode() to encode a focus event into a caller-provided
|
||||
* buffer. If the buffer is too small, the function returns
|
||||
* GHOSTTY_OUT_OF_SPACE and sets the required size in the output parameter.
|
||||
*
|
||||
* ## Example
|
||||
*
|
||||
* @code{.c}
|
||||
* #include <stdio.h>
|
||||
* #include <ghostty/vt.h>
|
||||
*
|
||||
* int main() {
|
||||
* char buf[8];
|
||||
* size_t written = 0;
|
||||
*
|
||||
* GhosttyResult result = ghostty_focus_encode(
|
||||
* GHOSTTY_FOCUS_GAINED, buf, sizeof(buf), &written);
|
||||
*
|
||||
* if (result == GHOSTTY_SUCCESS) {
|
||||
* printf("Encoded %zu bytes: ", written);
|
||||
* fwrite(buf, 1, written, stdout);
|
||||
* printf("\n");
|
||||
* }
|
||||
*
|
||||
* return 0;
|
||||
* }
|
||||
* @endcode
|
||||
*
|
||||
* @{
|
||||
*/
|
||||
|
||||
#include <stddef.h>
|
||||
#include <ghostty/vt/types.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Focus event types for focus reporting mode (mode 1004).
|
||||
*/
|
||||
typedef enum {
|
||||
/** Terminal window gained focus */
|
||||
GHOSTTY_FOCUS_GAINED = 0,
|
||||
/** Terminal window lost focus */
|
||||
GHOSTTY_FOCUS_LOST = 1,
|
||||
} GhosttyFocusEvent;
|
||||
|
||||
/**
|
||||
* Encode a focus event into a terminal escape sequence.
|
||||
*
|
||||
* Encodes a focus gained (CSI I) or focus lost (CSI O) report into the
|
||||
* provided buffer.
|
||||
*
|
||||
* If the buffer is too small, the function returns GHOSTTY_OUT_OF_SPACE
|
||||
* and writes the required buffer size to @p out_written. The caller can
|
||||
* then retry with a sufficiently sized buffer.
|
||||
*
|
||||
* @param event The focus event to encode
|
||||
* @param buf Output buffer to write the encoded sequence into (may be NULL)
|
||||
* @param buf_len Size of the output buffer in bytes
|
||||
* @param[out] out_written On success, the number of bytes written. On
|
||||
* GHOSTTY_OUT_OF_SPACE, the required buffer size.
|
||||
* @return GHOSTTY_SUCCESS on success, GHOSTTY_OUT_OF_SPACE if the buffer
|
||||
* is too small
|
||||
*/
|
||||
GhosttyResult ghostty_focus_encode(
|
||||
GhosttyFocusEvent event,
|
||||
char* buf,
|
||||
size_t buf_len,
|
||||
size_t* out_written);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
/** @} */
|
||||
|
||||
#endif /* GHOSTTY_VT_FOCUS_H */
|
||||
@@ -185,7 +185,7 @@ void ghostty_key_encoder_setopt_from_terminal(GhosttyKeyEncoder encoder, Ghostty
|
||||
* typically don't generate escape sequences. Check the out_len parameter to
|
||||
* determine if any data was written.
|
||||
*
|
||||
* If the output buffer is too small, this function returns GHOSTTY_OUT_OF_MEMORY
|
||||
* If the output buffer is too small, this function returns GHOSTTY_OUT_OF_SPACE
|
||||
* and out_len will contain the required buffer size. The caller can then
|
||||
* allocate a larger buffer and call the function again.
|
||||
*
|
||||
@@ -194,15 +194,15 @@ void ghostty_key_encoder_setopt_from_terminal(GhosttyKeyEncoder encoder, Ghostty
|
||||
* @param out_buf Buffer to write the encoded sequence to
|
||||
* @param out_buf_size Size of the output buffer in bytes
|
||||
* @param out_len Pointer to store the number of bytes written (may be NULL)
|
||||
* @return GHOSTTY_SUCCESS on success, GHOSTTY_OUT_OF_MEMORY if buffer too small, or other error code
|
||||
* @return GHOSTTY_SUCCESS on success, GHOSTTY_OUT_OF_SPACE if buffer too small, or other error code
|
||||
*
|
||||
* ## Example: Calculate required buffer size
|
||||
*
|
||||
* @code{.c}
|
||||
* // Query the required size with a NULL buffer (always returns OUT_OF_MEMORY)
|
||||
* // Query the required size with a NULL buffer (always returns OUT_OF_SPACE)
|
||||
* size_t required = 0;
|
||||
* GhosttyResult result = ghostty_key_encoder_encode(encoder, event, NULL, 0, &required);
|
||||
* assert(result == GHOSTTY_OUT_OF_MEMORY);
|
||||
* assert(result == GHOSTTY_OUT_OF_SPACE);
|
||||
*
|
||||
* // Allocate buffer of required size
|
||||
* char *buf = malloc(required);
|
||||
@@ -228,7 +228,7 @@ void ghostty_key_encoder_setopt_from_terminal(GhosttyKeyEncoder encoder, Ghostty
|
||||
* if (result == GHOSTTY_SUCCESS) {
|
||||
* // Write the encoded sequence to the terminal
|
||||
* write(pty_fd, buf, written);
|
||||
* } else if (result == GHOSTTY_OUT_OF_MEMORY) {
|
||||
* } else if (result == GHOSTTY_OUT_OF_SPACE) {
|
||||
* // Buffer too small, written contains required size
|
||||
* char *dynamic_buf = malloc(written);
|
||||
* result = ghostty_key_encoder_encode(encoder, event, dynamic_buf, written, &written);
|
||||
|
||||
@@ -189,7 +189,7 @@ void ghostty_mouse_encoder_reset(GhosttyMouseEncoder encoder);
|
||||
* Not all mouse events produce output. In such cases this returns
|
||||
* GHOSTTY_SUCCESS with out_len set to 0.
|
||||
*
|
||||
* If the output buffer is too small, this returns GHOSTTY_OUT_OF_MEMORY
|
||||
* If the output buffer is too small, this returns GHOSTTY_OUT_OF_SPACE
|
||||
* and out_len contains the required size.
|
||||
*
|
||||
* @param encoder The encoder handle, must not be NULL
|
||||
@@ -197,7 +197,7 @@ void ghostty_mouse_encoder_reset(GhosttyMouseEncoder encoder);
|
||||
* @param out_buf Buffer to write encoded bytes to, or NULL to query required size
|
||||
* @param out_buf_size Size of out_buf in bytes
|
||||
* @param out_len Pointer to store bytes written (or required bytes on failure)
|
||||
* @return GHOSTTY_SUCCESS on success, GHOSTTY_OUT_OF_MEMORY if buffer is too small,
|
||||
* @return GHOSTTY_SUCCESS on success, GHOSTTY_OUT_OF_SPACE if buffer is too small,
|
||||
* or another error code
|
||||
*
|
||||
* @ingroup mouse
|
||||
|
||||
@@ -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