mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-05 23:28:21 +00:00
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.
This commit is contained in:
@@ -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;
|
||||
|
||||
/**
|
||||
|
||||
53
include/ghostty/vt/selection.h
Normal file
53
include/ghostty/vt/selection.h
Normal 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 */
|
||||
@@ -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;
|
||||
|
||||
16
src/terminal/c/selection.zig
Normal file
16
src/terminal/c/selection.zig
Normal 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);
|
||||
}
|
||||
};
|
||||
@@ -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) },
|
||||
|
||||
Reference in New Issue
Block a user