libghostty: add GhosttySelection type and selection support to formatter (#12115)

Add a new GhosttySelection C API type (selection.h / c/selection.zig)
that pairs two GhosttyGridRef endpoints with a rectangle flag. This maps
directly to the internal Selection type using untracked pins.

The formatter terminal options gain an optional selection pointer. When
non-null the formatter restricts output to the specified range instead
of emitting the entire screen. When null the existing behavior of
formatting the full screen is preserved.
pull/12116/head
Mitchell Hashimoto 2026-04-04 20:48:39 -07:00 committed by GitHub
commit 10696b5ed1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 138 additions and 1 deletions

View File

@ -123,6 +123,7 @@ extern "C" {
#include <ghostty/vt/mouse.h>
#include <ghostty/vt/paste.h>
#include <ghostty/vt/screen.h>
#include <ghostty/vt/selection.h>
#include <ghostty/vt/size_report.h>
#include <ghostty/vt/wasm.h>

View File

@ -11,6 +11,7 @@
#include <stddef.h>
#include <stdint.h>
#include <ghostty/vt/allocator.h>
#include <ghostty/vt/selection.h>
#include <ghostty/vt/types.h>
#include <ghostty/vt/terminal.h>
@ -133,6 +134,10 @@ typedef struct {
/** Extra terminal state to include in styled output. */
GhosttyFormatterTerminalExtra extra;
/** Optional selection to restrict output to a range.
* If NULL, the entire screen is formatted. */
const GhosttySelection *selection;
} GhosttyFormatterTerminalOptions;
/**

View File

@ -0,0 +1,53 @@
/**
* @file selection.h
*
* Selection range type for specifying a region of terminal content.
*/
#ifndef GHOSTTY_VT_SELECTION_H
#define GHOSTTY_VT_SELECTION_H
#include <stdbool.h>
#include <stddef.h>
#include <ghostty/vt/grid_ref.h>
#ifdef __cplusplus
extern "C" {
#endif
/** @defgroup selection Selection
*
* A selection range defined by two grid references that identifies a
* contiguous or rectangular region of terminal content.
*
* @{
*/
/**
* A selection range defined by two grid references.
*
* This is a sized struct. Use GHOSTTY_INIT_SIZED() to initialize it.
*
* @ingroup selection
*/
typedef struct {
/** Size of this struct in bytes. Must be set to sizeof(GhosttySelection). */
size_t size;
/** Start of the selection range (inclusive). */
GhosttyGridRef start;
/** End of the selection range (inclusive). */
GhosttyGridRef end;
/** Whether the selection is rectangular (block) rather than linear. */
bool rectangle;
} GhosttySelection;
/** @} */
#ifdef __cplusplus
}
#endif
#endif /* GHOSTTY_VT_SELECTION_H */

View File

@ -3,6 +3,8 @@ const testing = std.testing;
const lib = @import("../lib.zig");
const CAllocator = lib.alloc.Allocator;
const terminal_c = @import("terminal.zig");
const grid_ref = @import("grid_ref.zig");
const selection_c = @import("selection.zig");
const ZigTerminal = @import("../Terminal.zig");
const formatterpkg = @import("../formatter.zig");
const Result = @import("result.zig").Result;
@ -23,6 +25,8 @@ pub const Formatter = ?*FormatterWrapper;
/// C: GhosttyFormatterFormat
pub const Format = formatterpkg.Format;
const CSelection = selection_c.CSelection;
/// C: GhosttyFormatterScreenOptions
pub const ScreenOptions = extern struct {
/// C: GhosttyFormatterScreenExtra
@ -63,6 +67,10 @@ pub const TerminalOptions = extern struct {
trim: bool,
extra: Extra,
/// Optional selection to restrict output to a range.
/// If null, the entire screen is formatted.
selection: ?*const CSelection = null,
/// C: GhosttyFormatterTerminalExtra
pub const Extra = extern struct {
size: usize = @sizeOf(Extra),
@ -138,6 +146,12 @@ fn terminal_new_(
});
formatter.extra = opts.extra.toZig();
// Setup the content that we're formatting
if (opts.selection) |sel| formatter.content = .{
.selection = sel.toZig() orelse
return error.InvalidValue,
};
ptr.* = .{
.kind = .{ .terminal = formatter },
.alloc = alloc,
@ -389,6 +403,50 @@ test "format vt" {
try testing.expect(std.mem.indexOf(u8, buf[0..written], "Test") != null);
}
test "format plain with selection" {
var t: terminal_c.Terminal = null;
try testing.expectEqual(Result.success, terminal_c.new(
&lib.alloc.test_allocator,
&t,
.{ .cols = 80, .rows = 24, .max_scrollback = 10_000 },
));
defer terminal_c.free(t);
terminal_c.vt_write(t, "Hello World", 11);
// Get grid refs for "World" (columns 6..10 on row 0)
var start_ref: grid_ref.CGridRef = .{};
try testing.expectEqual(Result.success, terminal_c.grid_ref(t, .{
.tag = .active,
.value = .{ .active = .{ .x = 6, .y = 0 } },
}, &start_ref));
var end_ref: grid_ref.CGridRef = .{};
try testing.expectEqual(Result.success, terminal_c.grid_ref(t, .{
.tag = .active,
.value = .{ .active = .{ .x = 10, .y = 0 } },
}, &end_ref));
const sel: selection_c.CSelection = .{
.start = start_ref,
.end = end_ref,
};
var f: Formatter = null;
try testing.expectEqual(Result.success, terminal_new(
&lib.alloc.test_allocator,
&f,
t,
.{ .emit = .plain, .unwrap = false, .trim = true, .selection = &sel, .extra = .{ .palette = false, .modes = false, .scrolling_region = false, .tabstops = false, .pwd = false, .keyboard = false, .screen = .{ .cursor = false, .style = false, .hyperlink = false, .protection = false, .kitty_keyboard = false, .charsets = false } } },
));
defer free(f);
var buf: [1024]u8 = undefined;
var written: usize = 0;
try testing.expectEqual(Result.success, format_buf(f, &buf, buf.len, &written));
try testing.expectEqualStrings("World", buf[0..written]);
}
test "format html" {
var t: terminal_c.Terminal = null;
try testing.expectEqual(Result.success, terminal_c.new(

View File

@ -31,7 +31,7 @@ pub const CGridRef = extern struct {
};
}
fn toPin(self: CGridRef) ?PageList.Pin {
pub fn toPin(self: CGridRef) ?PageList.Pin {
return .{
.node = self.node orelse return null,
.x = self.x,

View File

@ -12,6 +12,7 @@ pub const types = @import("types.zig");
pub const modes = @import("modes.zig");
pub const osc = @import("osc.zig");
pub const render = @import("render.zig");
pub const selection = @import("selection.zig");
pub const key_event = @import("key_event.zig");
pub const key_encode = @import("key_encode.zig");
pub const mouse_event = @import("mouse_event.zig");
@ -163,6 +164,7 @@ test {
_ = modes;
_ = osc;
_ = render;
_ = selection;
_ = key_event;
_ = key_encode;
_ = mouse_event;

View File

@ -0,0 +1,16 @@
const grid_ref = @import("grid_ref.zig");
const Selection = @import("../Selection.zig");
/// C: GhosttySelection
pub const CSelection = extern struct {
size: usize = @sizeOf(CSelection),
start: grid_ref.CGridRef,
end: grid_ref.CGridRef,
rectangle: bool = false,
pub fn toZig(self: CSelection) ?Selection {
const start_pin = self.start.toPin() orelse return null;
const end_pin = self.end.toPin() orelse return null;
return Selection.init(start_pin, end_pin, self.rectangle);
}
};

View File

@ -13,6 +13,7 @@ const size_report = @import("size_report.zig");
const terminal = @import("terminal.zig");
const formatter = @import("formatter.zig");
const selection = @import("selection.zig");
const render = @import("render.zig");
const style_c = @import("style.zig");
const mouse_encode = @import("mouse_encode.zig");
@ -26,6 +27,7 @@ pub const structs: std.StaticStringMap(StructInfo) = .initComptime(.{
.{ "GhosttyDeviceAttributesSecondary", StructInfo.init(terminal.DeviceAttributes.Secondary) },
.{ "GhosttyDeviceAttributesTertiary", StructInfo.init(terminal.DeviceAttributes.Tertiary) },
.{ "GhosttyFormatterTerminalOptions", StructInfo.init(formatter.TerminalOptions) },
.{ "GhosttySelection", StructInfo.init(selection.CSelection) },
.{ "GhosttyFormatterTerminalExtra", StructInfo.init(formatter.TerminalOptions.Extra) },
.{ "GhosttyFormatterScreenExtra", StructInfo.init(formatter.ScreenOptions.Extra) },
.{ "GhosttyGridRef", StructInfo.init(grid_ref.CGridRef) },