diff --git a/include/ghostty/vt.h b/include/ghostty/vt.h index 2a52f4b08..6a943350c 100644 --- a/include/ghostty/vt.h +++ b/include/ghostty/vt.h @@ -123,6 +123,7 @@ extern "C" { #include #include #include +#include #include #include diff --git a/include/ghostty/vt/formatter.h b/include/ghostty/vt/formatter.h index 81efdb27c..19f6664c3 100644 --- a/include/ghostty/vt/formatter.h +++ b/include/ghostty/vt/formatter.h @@ -11,6 +11,7 @@ #include #include #include +#include #include #include @@ -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; /** diff --git a/include/ghostty/vt/selection.h b/include/ghostty/vt/selection.h new file mode 100644 index 000000000..9f878fadc --- /dev/null +++ b/include/ghostty/vt/selection.h @@ -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 +#include +#include + +#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 */ diff --git a/src/terminal/c/formatter.zig b/src/terminal/c/formatter.zig index 11717bc22..5b4504c8c 100644 --- a/src/terminal/c/formatter.zig +++ b/src/terminal/c/formatter.zig @@ -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( diff --git a/src/terminal/c/grid_ref.zig b/src/terminal/c/grid_ref.zig index c7e86c29f..db7ff2e81 100644 --- a/src/terminal/c/grid_ref.zig +++ b/src/terminal/c/grid_ref.zig @@ -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, diff --git a/src/terminal/c/main.zig b/src/terminal/c/main.zig index 699ae5ade..dc3b7e7ce 100644 --- a/src/terminal/c/main.zig +++ b/src/terminal/c/main.zig @@ -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; diff --git a/src/terminal/c/selection.zig b/src/terminal/c/selection.zig new file mode 100644 index 000000000..74e96598f --- /dev/null +++ b/src/terminal/c/selection.zig @@ -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); + } +}; diff --git a/src/terminal/c/types.zig b/src/terminal/c/types.zig index b808bf38b..500809d9c 100644 --- a/src/terminal/c/types.zig +++ b/src/terminal/c/types.zig @@ -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) },