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
commit
10696b5ed1
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
|
@ -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) },
|
||||
|
|
|
|||
Loading…
Reference in New Issue