libghostty: expose paste encode to C API

Add ghostty_paste_encode() which encodes paste data for writing to
the terminal pty. It strips unsafe control bytes, wraps in bracketed
paste sequences when requested, and replaces newlines with carriage
returns for unbracketed mode. The input buffer is modified in place
and the encoded result is written to a caller-provided output buffer,
following the same buffer/out_written pattern as the other encode
functions like ghostty_size_report_encode.

Update the c-vt-paste example with an encode_example() demonstrating
the new function and add corresponding @snippet references in the
header documentation.
This commit is contained in:
Mitchell Hashimoto
2026-03-26 11:26:56 -07:00
parent 6ebbd4785b
commit 11574c35a2
6 changed files with 165 additions and 6 deletions

View File

@@ -1,7 +1,7 @@
# Example: `ghostty-vt` Paste Safety Check
# Example: `ghostty-vt` Paste Utilities
This contains a simple example of how to use the `ghostty-vt` paste
utilities to check if paste data is safe.
utilities to check if paste data is safe and encode it for terminal input.
This uses a `build.zig` and `Zig` to build the C program so that we
can reuse a lot of our build logic and depend directly on our source

View File

@@ -3,7 +3,7 @@
#include <ghostty/vt.h>
//! [paste-safety]
void basic_example() {
void safety_example() {
const char* safe_data = "hello world";
const char* unsafe_data = "rm -rf /\n";
@@ -17,8 +17,26 @@ void basic_example() {
}
//! [paste-safety]
//! [paste-encode]
void encode_example() {
// The input buffer is modified in place (unsafe bytes are stripped).
char data[] = "hello\nworld";
char buf[64];
size_t written = 0;
GhosttyResult result = ghostty_paste_encode(
data, strlen(data), true, buf, sizeof(buf), &written);
if (result == GHOSTTY_SUCCESS) {
printf("Encoded %zu bytes: ", written);
fwrite(buf, 1, written, stdout);
printf("\n");
}
}
//! [paste-encode]
int main() {
basic_example();
safety_example();
// Test unsafe paste data with bracketed paste end sequence
const char *unsafe_escape = "evil\x1b[201~code";
@@ -32,5 +50,7 @@ int main() {
printf("Empty data is safe\n");
}
encode_example();
return 0;
}

View File

@@ -9,22 +9,32 @@
/** @defgroup paste Paste Utilities
*
* Utilities for validating paste data safety.
* Utilities for validating and encoding paste data for terminal input.
*
* ## Basic Usage
*
* Use ghostty_paste_is_safe() to check if paste data contains potentially
* dangerous sequences before sending it to the terminal.
*
* ## Example
* Use ghostty_paste_encode() to encode paste data for writing to the pty,
* including bracketed paste wrapping and unsafe byte stripping.
*
* ## Examples
*
* ### Safety Check
*
* @snippet c-vt-paste/src/main.c paste-safety
*
* ### Encoding
*
* @snippet c-vt-paste/src/main.c paste-encode
*
* @{
*/
#include <stdbool.h>
#include <stddef.h>
#include <ghostty/vt/types.h>
#ifdef __cplusplus
extern "C" {
@@ -47,6 +57,41 @@ extern "C" {
*/
bool ghostty_paste_is_safe(const char* data, size_t len);
/**
* Encode paste data for writing to the terminal pty.
*
* This function prepares paste data for terminal input by:
* - Stripping unsafe control bytes (NUL, ESC, DEL, etc.) by replacing
* them with spaces
* - Wrapping the data in bracketed paste sequences if @p bracketed is true
* - Replacing newlines with carriage returns if @p bracketed is false
*
* The input @p data buffer is modified in place during encoding. The
* encoded result (potentially with bracketed paste prefix/suffix) is
* written to the output buffer.
*
* If the output buffer is too small, the function returns
* GHOSTTY_OUT_OF_SPACE and sets the required size in @p out_written.
* The caller can then retry with a sufficiently sized buffer.
*
* @param data The paste data to encode (modified in place, may be NULL)
* @param data_len The length of the input data in bytes
* @param bracketed Whether bracketed paste mode is active
* @param buf Output buffer to write the encoded result 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_paste_encode(
char* data,
size_t data_len,
bool bracketed,
char* buf,
size_t buf_len,
size_t* out_written);
#ifdef __cplusplus
}
#endif

View File

@@ -167,6 +167,7 @@ comptime {
@export(&c.focus_encode, .{ .name = "ghostty_focus_encode" });
@export(&c.mode_report_encode, .{ .name = "ghostty_mode_report_encode" });
@export(&c.paste_is_safe, .{ .name = "ghostty_paste_is_safe" });
@export(&c.paste_encode, .{ .name = "ghostty_paste_encode" });
@export(&c.size_report_encode, .{ .name = "ghostty_size_report_encode" });
@export(&c.style_default, .{ .name = "ghostty_style_default" });
@export(&c.style_is_default, .{ .name = "ghostty_style_is_default" });

View File

@@ -115,6 +115,7 @@ pub const mouse_encoder_reset = mouse_encode.reset;
pub const mouse_encoder_encode = mouse_encode.encode;
pub const paste_is_safe = paste.is_safe;
pub const paste_encode = paste.encode;
pub const alloc_alloc = allocator.alloc;
pub const alloc_free = allocator.free;

View File

@@ -1,12 +1,104 @@
const std = @import("std");
const lib = @import("../lib.zig");
const paste = @import("../../input/paste.zig");
const Result = @import("result.zig").Result;
pub fn is_safe(data: ?[*]const u8, len: usize) callconv(lib.calling_conv) bool {
const slice: []const u8 = if (data) |v| v[0..len] else &.{};
return paste.isSafe(slice);
}
pub fn encode(
data: ?[*]u8,
data_len: usize,
bracketed: bool,
out_: ?[*]u8,
out_len: usize,
out_written: *usize,
) callconv(lib.calling_conv) Result {
const slice: []u8 = if (data) |v| v[0..data_len] else &.{};
const result = paste.encode(slice, .{ .bracketed = bracketed });
const total = result[0].len + result[1].len + result[2].len;
out_written.* = total;
const out: []u8 = if (out_) |o| o[0..out_len] else &.{};
if (out.len < total) return .out_of_space;
var offset: usize = 0;
for (result) |segment| {
@memcpy(out[offset..][0..segment.len], segment);
offset += segment.len;
}
return .success;
}
test "encode bracketed" {
const testing = std.testing;
const input = try testing.allocator.dupe(u8, "hello");
defer testing.allocator.free(input);
var buf: [64]u8 = undefined;
var written: usize = 0;
const result = encode(input.ptr, input.len, true, &buf, buf.len, &written);
try testing.expectEqual(.success, result);
try testing.expectEqualStrings("\x1b[200~hello\x1b[201~", buf[0..written]);
}
test "encode unbracketed no newlines" {
const testing = std.testing;
const input = try testing.allocator.dupe(u8, "hello");
defer testing.allocator.free(input);
var buf: [64]u8 = undefined;
var written: usize = 0;
const result = encode(input.ptr, input.len, false, &buf, buf.len, &written);
try testing.expectEqual(.success, result);
try testing.expectEqualStrings("hello", buf[0..written]);
}
test "encode unbracketed newlines" {
const testing = std.testing;
const input = try testing.allocator.dupe(u8, "hello\nworld");
defer testing.allocator.free(input);
var buf: [64]u8 = undefined;
var written: usize = 0;
const result = encode(input.ptr, input.len, false, &buf, buf.len, &written);
try testing.expectEqual(.success, result);
try testing.expectEqualStrings("hello\rworld", buf[0..written]);
}
test "encode strip unsafe bytes" {
const testing = std.testing;
const input = try testing.allocator.dupe(u8, "hel\x1blo\x00world");
defer testing.allocator.free(input);
var buf: [64]u8 = undefined;
var written: usize = 0;
const result = encode(input.ptr, input.len, true, &buf, buf.len, &written);
try testing.expectEqual(.success, result);
try testing.expectEqualStrings("\x1b[200~hel lo world\x1b[201~", buf[0..written]);
}
test "encode with insufficient buffer" {
const testing = std.testing;
const input = try testing.allocator.dupe(u8, "hello");
defer testing.allocator.free(input);
var buf: [1]u8 = undefined;
var written: usize = 0;
const result = encode(input.ptr, input.len, true, &buf, buf.len, &written);
try testing.expectEqual(.out_of_space, result);
try testing.expectEqual(17, written);
}
test "encode with null buffer" {
const testing = std.testing;
const input = try testing.allocator.dupe(u8, "hello");
defer testing.allocator.free(input);
var written: usize = 0;
const result = encode(input.ptr, input.len, true, null, 0, &written);
try testing.expectEqual(.out_of_space, result);
try testing.expectEqual(17, written);
}
test "is_safe with safe data" {
const testing = std.testing;
const safe = "hello world";