mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-05-25 14:28:32 +00:00
libghostty: selection APIs for C (#12794)
Adds libghostty-vt selection APIs read/write, formatting, inspecting, and rendering selection state from C. | Introduced type/function | Purpose | | --- | --- | | `GhosttyRenderStateRowSelection` | Row-local inclusive selection range returned by render row queries. | | `GhosttyTerminalSelectWordOptions` | Options for deriving a word selection from a grid ref. | | `GhosttyTerminalSelectWordBetweenOptions` | Options for finding the nearest selectable word between two refs. | | `GhosttyTerminalSelectLineOptions` | Options for deriving line selections, including semantic prompt boundaries. | | `GhosttyTerminalSelectionFormatOptions` | Options for formatting the active or caller-provided selection. | | `GhosttySelectionOrder` | Describes endpoint ordering, including rectangular mirrored orders. | | `GhosttySelectionAdjust` | Operations for moving a selection endpoint. | | `ghostty_terminal_select_word` | Derive a word selection snapshot. | | `ghostty_terminal_select_word_between` | Derive the nearest word selection between two refs. | | `ghostty_terminal_select_line` | Derive a line selection snapshot. | | `ghostty_terminal_select_all` | Derive a selection covering all selectable content. | | `ghostty_terminal_select_output` | Derive a semantic command-output selection. | | `ghostty_terminal_selection_format_buf` | Format a selection into a caller-provided buffer. | | `ghostty_terminal_selection_format_alloc` | Format a selection into an allocated buffer. | | `ghostty_terminal_selection_adjust` | Mutate a selection snapshot endpoint. | | `ghostty_terminal_selection_order` | Query selection endpoint order. | | `ghostty_terminal_selection_ordered` | Return a selection with normalized endpoint order. | | `ghostty_terminal_selection_contains` | Test whether a point is inside a selection. | | `ghostty_terminal_selection_equal` | Compare two selection snapshots using terminal semantics. |
This commit is contained in:
@@ -2,8 +2,8 @@
|
||||
|
||||
This contains an example of how to use the `ghostty-vt` render-state API
|
||||
to create a render state, update it from terminal content, iterate rows
|
||||
and cells, read styles and colors, inspect cursor state, and manage dirty
|
||||
tracking.
|
||||
and cells, read styles and colors, inspect cursor and row-local selection
|
||||
state, and manage dirty tracking.
|
||||
|
||||
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
|
||||
|
||||
@@ -46,6 +46,32 @@ int main(void) {
|
||||
ghostty_terminal_vt_write(
|
||||
terminal, (const uint8_t*)content, strlen(content));
|
||||
|
||||
// Select "underlined" on the second row. Render state exposes this
|
||||
// later as a row-local selected cell range.
|
||||
GhosttyGridRef selection_start = GHOSTTY_INIT_SIZED(GhosttyGridRef);
|
||||
GhosttyPoint selection_start_pt = {
|
||||
.tag = GHOSTTY_POINT_TAG_ACTIVE,
|
||||
.value = { .coordinate = { .x = 0, .y = 1 } },
|
||||
};
|
||||
result = ghostty_terminal_grid_ref(
|
||||
terminal, selection_start_pt, &selection_start);
|
||||
assert(result == GHOSTTY_SUCCESS);
|
||||
|
||||
GhosttyGridRef selection_end = GHOSTTY_INIT_SIZED(GhosttyGridRef);
|
||||
GhosttyPoint selection_end_pt = {
|
||||
.tag = GHOSTTY_POINT_TAG_ACTIVE,
|
||||
.value = { .coordinate = { .x = 9, .y = 1 } },
|
||||
};
|
||||
result = ghostty_terminal_grid_ref(terminal, selection_end_pt, &selection_end);
|
||||
assert(result == GHOSTTY_SUCCESS);
|
||||
|
||||
GhosttySelection selection = GHOSTTY_INIT_SIZED(GhosttySelection);
|
||||
selection.start = selection_start;
|
||||
selection.end = selection_end;
|
||||
result = ghostty_terminal_set(
|
||||
terminal, GHOSTTY_TERMINAL_OPT_SELECTION, &selection);
|
||||
assert(result == GHOSTTY_SUCCESS);
|
||||
|
||||
result = ghostty_render_state_update(render_state, terminal);
|
||||
assert(result == GHOSTTY_SUCCESS);
|
||||
//! [render-state-update]
|
||||
@@ -154,6 +180,18 @@ int main(void) {
|
||||
printf("Row %2d [%s]: ", row_index,
|
||||
row_dirty ? "dirty" : "clean");
|
||||
|
||||
// Query the row-local selection range. Rows without a selection return
|
||||
// GHOSTTY_NO_VALUE; selected rows return inclusive start/end columns.
|
||||
GhosttyRenderStateRowSelection row_selection =
|
||||
GHOSTTY_INIT_SIZED(GhosttyRenderStateRowSelection);
|
||||
result = ghostty_render_state_row_get(
|
||||
row_iter, GHOSTTY_RENDER_STATE_ROW_DATA_SELECTION, &row_selection);
|
||||
assert(result == GHOSTTY_SUCCESS || result == GHOSTTY_NO_VALUE);
|
||||
if (result == GHOSTTY_SUCCESS) {
|
||||
printf("selection=%u..%u ",
|
||||
row_selection.start_x, row_selection.end_x);
|
||||
}
|
||||
|
||||
// Get cells for this row (reuses the same cells handle).
|
||||
result = ghostty_render_state_row_get(
|
||||
row_iter, GHOSTTY_RENDER_STATE_ROW_DATA_CELLS, &cells);
|
||||
|
||||
18
example/c-vt-selection/README.md
Normal file
18
example/c-vt-selection/README.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# Example: `ghostty-vt` Selection
|
||||
|
||||
This contains a simple example of how to use the `ghostty-vt` terminal,
|
||||
grid reference, selection, and formatter APIs to derive selections such as a
|
||||
word, semantic command line, command output, and all visible content.
|
||||
|
||||
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
|
||||
tree, but Ghostty emits a standard C library that can be used with any
|
||||
C tooling.
|
||||
|
||||
## Usage
|
||||
|
||||
Run the program:
|
||||
|
||||
```shell-session
|
||||
zig build run
|
||||
```
|
||||
42
example/c-vt-selection/build.zig
Normal file
42
example/c-vt-selection/build.zig
Normal file
@@ -0,0 +1,42 @@
|
||||
const std = @import("std");
|
||||
|
||||
pub fn build(b: *std.Build) void {
|
||||
const target = b.standardTargetOptions(.{});
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
|
||||
const run_step = b.step("run", "Run the app");
|
||||
|
||||
const exe_mod = b.createModule(.{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
exe_mod.addCSourceFiles(.{
|
||||
.root = b.path("src"),
|
||||
.files = &.{"main.c"},
|
||||
});
|
||||
|
||||
// You'll want to use a lazy dependency here so that ghostty is only
|
||||
// downloaded if you actually need it.
|
||||
if (b.lazyDependency("ghostty", .{
|
||||
// Setting simd to false will force a pure static build that
|
||||
// doesn't even require libc, but it has a significant performance
|
||||
// penalty. If your embedding app requires libc anyway, you should
|
||||
// always keep simd enabled.
|
||||
// .simd = false,
|
||||
})) |dep| {
|
||||
exe_mod.linkLibrary(dep.artifact("ghostty-vt"));
|
||||
}
|
||||
|
||||
// Exe
|
||||
const exe = b.addExecutable(.{
|
||||
.name = "c_vt_selection",
|
||||
.root_module = exe_mod,
|
||||
});
|
||||
b.installArtifact(exe);
|
||||
|
||||
// Run
|
||||
const run_cmd = b.addRunArtifact(exe);
|
||||
run_cmd.step.dependOn(b.getInstallStep());
|
||||
if (b.args) |args| run_cmd.addArgs(args);
|
||||
run_step.dependOn(&run_cmd.step);
|
||||
}
|
||||
24
example/c-vt-selection/build.zig.zon
Normal file
24
example/c-vt-selection/build.zig.zon
Normal file
@@ -0,0 +1,24 @@
|
||||
.{
|
||||
.name = .c_vt_selection,
|
||||
.version = "0.0.0",
|
||||
.fingerprint = 0xb2c2f1a828086fef,
|
||||
.minimum_zig_version = "0.15.1",
|
||||
.dependencies = .{
|
||||
// Ghostty dependency. In reality, you'd probably use a URL-based
|
||||
// dependency like the one showed (and commented out) below this one.
|
||||
// We use a path dependency here for simplicity and to ensure our
|
||||
// examples always test against the source they're bundled with.
|
||||
.ghostty = .{ .path = "../../" },
|
||||
|
||||
// Example of what a URL-based dependency looks like:
|
||||
// .ghostty = .{
|
||||
// .url = "https://github.com/ghostty-org/ghostty/archive/COMMIT.tar.gz",
|
||||
// .hash = "N-V-__8AAMVLTABmYkLqhZPLXnMl-KyN38R8UVYqGrxqO36s",
|
||||
// },
|
||||
},
|
||||
.paths = .{
|
||||
"build.zig",
|
||||
"build.zig.zon",
|
||||
"src",
|
||||
},
|
||||
}
|
||||
136
example/c-vt-selection/src/main.c
Normal file
136
example/c-vt-selection/src/main.c
Normal file
@@ -0,0 +1,136 @@
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <ghostty/vt.h>
|
||||
|
||||
//! [selection-main]
|
||||
static void vt_write(GhosttyTerminal terminal, const char *s) {
|
||||
ghostty_terminal_vt_write(terminal, (const uint8_t *)s, strlen(s));
|
||||
}
|
||||
|
||||
static GhosttyGridRef ref_at(GhosttyTerminal terminal, uint16_t x, uint16_t y) {
|
||||
GhosttyGridRef ref = GHOSTTY_INIT_SIZED(GhosttyGridRef);
|
||||
GhosttyPoint point = {
|
||||
.tag = GHOSTTY_POINT_TAG_ACTIVE,
|
||||
.value = { .coordinate = { .x = x, .y = y } },
|
||||
};
|
||||
|
||||
GhosttyResult result = ghostty_terminal_grid_ref(terminal, point, &ref);
|
||||
assert(result == GHOSTTY_SUCCESS);
|
||||
return ref;
|
||||
}
|
||||
|
||||
static void print_selection(
|
||||
GhosttyTerminal terminal,
|
||||
const char *label,
|
||||
const GhosttySelection *selection) {
|
||||
GhosttyFormatterTerminalOptions opts = GHOSTTY_INIT_SIZED(GhosttyFormatterTerminalOptions);
|
||||
opts.emit = GHOSTTY_FORMATTER_FORMAT_PLAIN;
|
||||
opts.trim = true;
|
||||
opts.selection = selection;
|
||||
|
||||
GhosttyFormatter formatter;
|
||||
GhosttyResult result = ghostty_formatter_terminal_new(
|
||||
NULL, &formatter, terminal, opts);
|
||||
assert(result == GHOSTTY_SUCCESS);
|
||||
|
||||
uint8_t *buf = NULL;
|
||||
size_t len = 0;
|
||||
result = ghostty_formatter_format_alloc(formatter, NULL, &buf, &len);
|
||||
assert(result == GHOSTTY_SUCCESS);
|
||||
|
||||
printf("%s: ", label);
|
||||
fwrite(buf, 1, len, stdout);
|
||||
printf("\n");
|
||||
|
||||
ghostty_free(NULL, buf, len);
|
||||
ghostty_formatter_free(formatter);
|
||||
}
|
||||
|
||||
int main() {
|
||||
GhosttyTerminal terminal;
|
||||
GhosttyTerminalOptions opts = {
|
||||
.cols = 80,
|
||||
.rows = 8,
|
||||
.max_scrollback = 0,
|
||||
};
|
||||
GhosttyResult result = ghostty_terminal_new(NULL, &terminal, opts);
|
||||
assert(result == GHOSTTY_SUCCESS);
|
||||
|
||||
// A realistic shell transcript with OSC 133 semantic prompt markers.
|
||||
// Ghostty uses these markers to distinguish prompt/input from command
|
||||
// output for semantic line and output selections.
|
||||
vt_write(terminal,
|
||||
"\033]133;A\007$ " // Prompt starts: "$ "
|
||||
"\033]133;B\007git status" // Input starts: "git status"
|
||||
"\033]133;C\007\r\n" // Output starts after Enter
|
||||
"On branch main\r\n"
|
||||
"nothing to commit, working tree clean");
|
||||
|
||||
GhosttySelection selection = GHOSTTY_INIT_SIZED(GhosttySelection);
|
||||
|
||||
// Double-click style word selection under the cursor.
|
||||
GhosttyTerminalSelectWordOptions word = GHOSTTY_INIT_SIZED(GhosttyTerminalSelectWordOptions);
|
||||
word.ref = ref_at(terminal, 6, 0); // the "status" in "git status"
|
||||
result = ghostty_terminal_select_word(terminal, &word, &selection);
|
||||
assert(result == GHOSTTY_SUCCESS);
|
||||
print_selection(terminal, "word", &selection);
|
||||
|
||||
//! [selection-word-between]
|
||||
// Double-click-and-drag style selection. Suppose the user double-clicks
|
||||
// "git" and drags to "status". The pointer may pass over whitespace, so
|
||||
// select the nearest word between the original click and current drag point
|
||||
// in both directions, then combine the outer word bounds.
|
||||
GhosttyGridRef click_ref = ref_at(terminal, 2, 0); // the "git" in "git status"
|
||||
GhosttyGridRef drag_ref = ref_at(terminal, 6, 0); // the "status" in "git status"
|
||||
|
||||
GhosttyTerminalSelectWordBetweenOptions start_word_opts =
|
||||
GHOSTTY_INIT_SIZED(GhosttyTerminalSelectWordBetweenOptions);
|
||||
start_word_opts.start = click_ref;
|
||||
start_word_opts.end = drag_ref;
|
||||
|
||||
GhosttySelection start_word = GHOSTTY_INIT_SIZED(GhosttySelection);
|
||||
result = ghostty_terminal_select_word_between(
|
||||
terminal, &start_word_opts, &start_word);
|
||||
assert(result == GHOSTTY_SUCCESS);
|
||||
|
||||
GhosttyTerminalSelectWordBetweenOptions end_word_opts =
|
||||
GHOSTTY_INIT_SIZED(GhosttyTerminalSelectWordBetweenOptions);
|
||||
end_word_opts.start = drag_ref;
|
||||
end_word_opts.end = click_ref;
|
||||
|
||||
GhosttySelection end_word = GHOSTTY_INIT_SIZED(GhosttySelection);
|
||||
result = ghostty_terminal_select_word_between(
|
||||
terminal, &end_word_opts, &end_word);
|
||||
assert(result == GHOSTTY_SUCCESS);
|
||||
|
||||
GhosttySelection drag_selection = GHOSTTY_INIT_SIZED(GhosttySelection);
|
||||
drag_selection.start = start_word.start;
|
||||
drag_selection.end = end_word.end;
|
||||
print_selection(terminal, "double-click drag", &drag_selection);
|
||||
//! [selection-word-between]
|
||||
|
||||
// Triple-click style line selection. With semantic prompt boundaries enabled,
|
||||
// this selects only the input area rather than the leading "$ " prompt.
|
||||
GhosttyTerminalSelectLineOptions line = GHOSTTY_INIT_SIZED(GhosttyTerminalSelectLineOptions);
|
||||
line.ref = ref_at(terminal, 2, 0); // the "git status" input area
|
||||
line.semantic_prompt_boundary = true;
|
||||
result = ghostty_terminal_select_line(terminal, &line, &selection);
|
||||
assert(result == GHOSTTY_SUCCESS);
|
||||
print_selection(terminal, "line", &selection);
|
||||
|
||||
// Select exactly the command output for the command under the cursor.
|
||||
result = ghostty_terminal_select_output(
|
||||
terminal, ref_at(terminal, 0, 1), &selection);
|
||||
assert(result == GHOSTTY_SUCCESS);
|
||||
print_selection(terminal, "output", &selection);
|
||||
|
||||
// Select all visible content.
|
||||
result = ghostty_terminal_select_all(terminal, &selection);
|
||||
assert(result == GHOSTTY_SUCCESS);
|
||||
print_selection(terminal, "all", &selection);
|
||||
|
||||
ghostty_terminal_free(terminal);
|
||||
return 0;
|
||||
}
|
||||
//! [selection-main]
|
||||
@@ -136,6 +136,7 @@ extern "C" {
|
||||
#include <ghostty/vt/modes.h>
|
||||
#include <ghostty/vt/mouse.h>
|
||||
#include <ghostty/vt/paste.h>
|
||||
#include <ghostty/vt/point.h>
|
||||
#include <ghostty/vt/screen.h>
|
||||
#include <ghostty/vt/selection.h>
|
||||
#include <ghostty/vt/size_report.h>
|
||||
|
||||
@@ -32,23 +32,6 @@ extern "C" {
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* Output format.
|
||||
*
|
||||
* @ingroup formatter
|
||||
*/
|
||||
typedef enum GHOSTTY_ENUM_TYPED {
|
||||
/** Plain text (no escape sequences). */
|
||||
GHOSTTY_FORMATTER_FORMAT_PLAIN,
|
||||
|
||||
/** VT sequences preserving colors, styles, URLs, etc. */
|
||||
GHOSTTY_FORMATTER_FORMAT_VT,
|
||||
|
||||
/** HTML with inline styles. */
|
||||
GHOSTTY_FORMATTER_FORMAT_HTML,
|
||||
GHOSTTY_FORMATTER_FORMAT_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
|
||||
} GhosttyFormatterFormat;
|
||||
|
||||
/**
|
||||
* Extra screen state to include in styled output.
|
||||
*
|
||||
|
||||
@@ -221,6 +221,9 @@ typedef enum GHOSTTY_ENUM_TYPED {
|
||||
* valid as long as the underlying render state is not updated.
|
||||
* It is unsafe to use cell data after updating the render state. */
|
||||
GHOSTTY_RENDER_STATE_ROW_DATA_CELLS = 3,
|
||||
|
||||
/** Row-local selected cell range (GhosttyRenderStateRowSelection). */
|
||||
GHOSTTY_RENDER_STATE_ROW_DATA_SELECTION = 4,
|
||||
GHOSTTY_RENDER_STATE_ROW_DATA_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
|
||||
} GhosttyRenderStateRowData;
|
||||
|
||||
@@ -235,6 +238,29 @@ typedef enum GHOSTTY_ENUM_TYPED {
|
||||
GHOSTTY_RENDER_STATE_ROW_OPTION_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
|
||||
} GhosttyRenderStateRowOption;
|
||||
|
||||
/**
|
||||
* Row-local selection range.
|
||||
*
|
||||
* This struct uses the sized-struct ABI pattern. Initialize with
|
||||
* GHOSTTY_INIT_SIZED(GhosttyRenderStateRowSelection) before querying
|
||||
* GHOSTTY_RENDER_STATE_ROW_DATA_SELECTION.
|
||||
*
|
||||
* Querying GHOSTTY_RENDER_STATE_ROW_DATA_SELECTION returns GHOSTTY_NO_VALUE
|
||||
* if the current row does not intersect the current selection.
|
||||
*
|
||||
* @ingroup render
|
||||
*/
|
||||
typedef struct {
|
||||
/** Size of this struct in bytes. Must be set to sizeof(GhosttyRenderStateRowSelection). */
|
||||
size_t size;
|
||||
|
||||
/** Start column of the row-local selection range, inclusive. */
|
||||
uint16_t start_x;
|
||||
|
||||
/** End column of the row-local selection range, inclusive. */
|
||||
uint16_t end_x;
|
||||
} GhosttyRenderStateRowSelection;
|
||||
|
||||
/**
|
||||
* Render-state color information.
|
||||
*
|
||||
|
||||
@@ -9,7 +9,11 @@
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <ghostty/vt/allocator.h>
|
||||
#include <ghostty/vt/grid_ref.h>
|
||||
#include <ghostty/vt/point.h>
|
||||
#include <ghostty/vt/types.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
@@ -17,14 +21,38 @@ extern "C" {
|
||||
|
||||
/** @defgroup selection Selection
|
||||
*
|
||||
* A selection range defined by two grid references that identifies a
|
||||
* contiguous or rectangular region of terminal content.
|
||||
* A snapshot selection range defined by two grid references that identifies
|
||||
* a contiguous or rectangular region of terminal content.
|
||||
*
|
||||
* The start and end values are GhosttyGridRef values. They are therefore
|
||||
* untracked grid references and inherit the same lifetime rules: they are
|
||||
* only safe to use until the next mutating operation on the terminal that
|
||||
* produced them, including freeing the terminal. To keep a selection valid
|
||||
* across terminal mutations, callers must maintain tracked grid references
|
||||
* for the endpoints and reconstruct a GhosttySelection from fresh snapshots
|
||||
* when needed.
|
||||
*
|
||||
* ## Examples
|
||||
*
|
||||
* @snippet c-vt-selection/src/main.c selection-main
|
||||
*
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* A selection range defined by two grid references.
|
||||
* A snapshot selection range defined by two grid references.
|
||||
*
|
||||
* Both endpoints are inclusive. The endpoints preserve selection direction
|
||||
* and may be reversed; callers must not assume that start is the top-left
|
||||
* endpoint or that end is the bottom-right endpoint.
|
||||
*
|
||||
* When rectangle is false, the endpoints describe a linear selection. When
|
||||
* rectangle is true, the same endpoints are interpreted as opposite corners
|
||||
* of a rectangular/block selection.
|
||||
*
|
||||
* The start and end values are untracked GhosttyGridRef snapshots and are
|
||||
* only valid until the next mutating operation on the terminal that produced
|
||||
* them unless the selection is reconstructed from tracked references.
|
||||
*
|
||||
* This is a sized struct. Use GHOSTTY_INIT_SIZED() to initialize it.
|
||||
*
|
||||
@@ -34,16 +62,554 @@ typedef struct {
|
||||
/** Size of this struct in bytes. Must be set to sizeof(GhosttySelection). */
|
||||
size_t size;
|
||||
|
||||
/** Start of the selection range (inclusive). */
|
||||
/**
|
||||
* Start of the selection range (inclusive).
|
||||
*
|
||||
* This may be after end in terminal order. It is an untracked
|
||||
* GhosttyGridRef snapshot and follows untracked grid-ref lifetime rules.
|
||||
*/
|
||||
GhosttyGridRef start;
|
||||
|
||||
/** End of the selection range (inclusive). */
|
||||
/**
|
||||
* End of the selection range (inclusive).
|
||||
*
|
||||
* This may be before start in terminal order. It is an untracked
|
||||
* GhosttyGridRef snapshot and follows untracked grid-ref lifetime rules.
|
||||
*/
|
||||
GhosttyGridRef end;
|
||||
|
||||
/** Whether the selection is rectangular (block) rather than linear. */
|
||||
/**
|
||||
* Whether the endpoints are interpreted as a rectangular/block selection
|
||||
* rather than a linear selection.
|
||||
*/
|
||||
bool rectangle;
|
||||
} GhosttySelection;
|
||||
|
||||
/**
|
||||
* Options for deriving a word selection from a terminal grid reference.
|
||||
*
|
||||
* This is a sized struct. Use GHOSTTY_INIT_SIZED() to initialize it.
|
||||
* If boundary_codepoints is NULL and boundary_codepoints_len is 0, Ghostty's
|
||||
* default word-boundary codepoints are used. If boundary_codepoints_len is
|
||||
* non-zero, boundary_codepoints must not be NULL.
|
||||
*
|
||||
* @ingroup selection
|
||||
*/
|
||||
typedef struct {
|
||||
/** Size of this struct in bytes. Must be set to sizeof(GhosttyTerminalSelectWordOptions). */
|
||||
size_t size;
|
||||
|
||||
/** Grid reference under which to derive the word selection. */
|
||||
GhosttyGridRef ref;
|
||||
|
||||
/** Optional word-boundary codepoints as uint32_t scalar values. */
|
||||
const uint32_t* boundary_codepoints;
|
||||
|
||||
/** Number of entries in boundary_codepoints. */
|
||||
size_t boundary_codepoints_len;
|
||||
} GhosttyTerminalSelectWordOptions;
|
||||
|
||||
/**
|
||||
* Options for deriving the nearest word selection between two grid references.
|
||||
*
|
||||
* This is a sized struct. Use GHOSTTY_INIT_SIZED() to initialize it.
|
||||
* If boundary_codepoints is NULL and boundary_codepoints_len is 0, Ghostty's
|
||||
* default word-boundary codepoints are used. If boundary_codepoints_len is
|
||||
* non-zero, boundary_codepoints must not be NULL.
|
||||
*
|
||||
* @ingroup selection
|
||||
*/
|
||||
typedef struct {
|
||||
/** Size of this struct in bytes. Must be set to sizeof(GhosttyTerminalSelectWordBetweenOptions). */
|
||||
size_t size;
|
||||
|
||||
/** Starting grid reference for the inclusive search range. */
|
||||
GhosttyGridRef start;
|
||||
|
||||
/** Ending grid reference for the inclusive search range. */
|
||||
GhosttyGridRef end;
|
||||
|
||||
/** Optional word-boundary codepoints as uint32_t scalar values. */
|
||||
const uint32_t* boundary_codepoints;
|
||||
|
||||
/** Number of entries in boundary_codepoints. */
|
||||
size_t boundary_codepoints_len;
|
||||
} GhosttyTerminalSelectWordBetweenOptions;
|
||||
|
||||
/**
|
||||
* Options for deriving a line selection from a terminal grid reference.
|
||||
*
|
||||
* This is a sized struct. Use GHOSTTY_INIT_SIZED() to initialize it.
|
||||
* If whitespace is NULL and whitespace_len is 0, Ghostty's default line-trim
|
||||
* whitespace codepoints are used. If whitespace_len is non-zero, whitespace
|
||||
* must not be NULL.
|
||||
*
|
||||
* @ingroup selection
|
||||
*/
|
||||
typedef struct {
|
||||
/** Size of this struct in bytes. Must be set to sizeof(GhosttyTerminalSelectLineOptions). */
|
||||
size_t size;
|
||||
|
||||
/** Grid reference under which to derive the line selection. */
|
||||
GhosttyGridRef ref;
|
||||
|
||||
/** Optional codepoints to trim from the start and end of the line. */
|
||||
const uint32_t* whitespace;
|
||||
|
||||
/** Number of entries in whitespace. */
|
||||
size_t whitespace_len;
|
||||
|
||||
/** Whether semantic prompt state changes should bound the line selection. */
|
||||
bool semantic_prompt_boundary;
|
||||
} GhosttyTerminalSelectLineOptions;
|
||||
|
||||
/**
|
||||
* Options for one-shot formatting of a terminal selection.
|
||||
*
|
||||
* This is a sized struct. Use GHOSTTY_INIT_SIZED() to initialize it.
|
||||
*
|
||||
* If selection is NULL, the terminal's current active selection is used.
|
||||
* If selection is non-NULL, that caller-provided snapshot selection is used.
|
||||
*
|
||||
* The selection is formatted from the terminal's active screen using the same
|
||||
* formatting semantics as GhosttyFormatter. For copy/clipboard behavior
|
||||
* matching Ghostty's Screen.selectionString(), use plain output with unwrap
|
||||
* and trim both set to true.
|
||||
*
|
||||
* @ingroup selection
|
||||
*/
|
||||
typedef struct {
|
||||
/** Size of this struct in bytes. Must be set to sizeof(GhosttyTerminalSelectionFormatOptions). */
|
||||
size_t size;
|
||||
|
||||
/** Output format to emit. */
|
||||
GhosttyFormatterFormat emit;
|
||||
|
||||
/** Whether to unwrap soft-wrapped lines. */
|
||||
bool unwrap;
|
||||
|
||||
/** Whether to trim trailing whitespace on non-blank lines. */
|
||||
bool trim;
|
||||
|
||||
/**
|
||||
* Optional selection to format.
|
||||
*
|
||||
* If NULL, the terminal's current active selection is used. If the terminal
|
||||
* has no active selection, formatting returns GHOSTTY_NO_VALUE.
|
||||
*
|
||||
* If non-NULL, the pointed-to selection must be a valid snapshot selection
|
||||
* for this terminal and must obey GhosttySelection lifetime rules.
|
||||
*/
|
||||
const GhosttySelection *selection;
|
||||
} GhosttyTerminalSelectionFormatOptions;
|
||||
|
||||
/**
|
||||
* Ordering of a selection's endpoints in terminal coordinates.
|
||||
*
|
||||
* Mirrored orders are only produced by rectangular selections whose start
|
||||
* and end endpoints are on opposite diagonal corners that are not simple
|
||||
* top-left-to-bottom-right or bottom-right-to-top-left orderings.
|
||||
*
|
||||
* @ingroup selection
|
||||
*/
|
||||
typedef enum GHOSTTY_ENUM_TYPED {
|
||||
/** Start is before end in top-left to bottom-right order. */
|
||||
GHOSTTY_SELECTION_ORDER_FORWARD = 0,
|
||||
|
||||
/** End is before start in top-left to bottom-right order. */
|
||||
GHOSTTY_SELECTION_ORDER_REVERSE = 1,
|
||||
|
||||
/** Rectangular selection from top-right to bottom-left. */
|
||||
GHOSTTY_SELECTION_ORDER_MIRRORED_FORWARD = 2,
|
||||
|
||||
/** Rectangular selection from bottom-left to top-right. */
|
||||
GHOSTTY_SELECTION_ORDER_MIRRORED_REVERSE = 3,
|
||||
|
||||
GHOSTTY_SELECTION_ORDER_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
|
||||
} GhosttySelectionOrder;
|
||||
|
||||
/**
|
||||
* Operation used to adjust a selection endpoint.
|
||||
*
|
||||
* Adjustment mutates the selection's logical end endpoint, not whichever
|
||||
* endpoint is visually bottom/right. This preserves keyboard and drag
|
||||
* behavior for both forward and reversed selections.
|
||||
*
|
||||
* @ingroup selection
|
||||
*/
|
||||
typedef enum GHOSTTY_ENUM_TYPED {
|
||||
/** Move left to the previous non-empty cell, wrapping upward. */
|
||||
GHOSTTY_SELECTION_ADJUST_LEFT = 0,
|
||||
|
||||
/** Move right to the next non-empty cell, wrapping downward. */
|
||||
GHOSTTY_SELECTION_ADJUST_RIGHT = 1,
|
||||
|
||||
/**
|
||||
* Move up one row at the current column, or to the beginning of the
|
||||
* line if already at the top.
|
||||
*/
|
||||
GHOSTTY_SELECTION_ADJUST_UP = 2,
|
||||
|
||||
/**
|
||||
* Move down to the next non-blank row at the current column, or to the
|
||||
* end of the line if none exists.
|
||||
*/
|
||||
GHOSTTY_SELECTION_ADJUST_DOWN = 3,
|
||||
|
||||
/** Move to the top-left cell of the screen. */
|
||||
GHOSTTY_SELECTION_ADJUST_HOME = 4,
|
||||
|
||||
/** Move to the right edge of the last non-blank row on the screen. */
|
||||
GHOSTTY_SELECTION_ADJUST_END = 5,
|
||||
|
||||
/**
|
||||
* Move up by one terminal page height, or to home if that would move
|
||||
* past the top.
|
||||
*/
|
||||
GHOSTTY_SELECTION_ADJUST_PAGE_UP = 6,
|
||||
|
||||
/**
|
||||
* Move down by one terminal page height, or to end if that would move
|
||||
* past the bottom.
|
||||
*/
|
||||
GHOSTTY_SELECTION_ADJUST_PAGE_DOWN = 7,
|
||||
|
||||
/** Move to the left edge of the current line. */
|
||||
GHOSTTY_SELECTION_ADJUST_BEGINNING_OF_LINE = 8,
|
||||
|
||||
/** Move to the right edge of the current line. */
|
||||
GHOSTTY_SELECTION_ADJUST_END_OF_LINE = 9,
|
||||
|
||||
GHOSTTY_SELECTION_ADJUST_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
|
||||
} GhosttySelectionAdjust;
|
||||
|
||||
/**
|
||||
* Derive a word selection snapshot from a terminal grid reference.
|
||||
*
|
||||
* The returned selection is not installed as the terminal's current
|
||||
* selection. It is a snapshot with the same lifetime rules as GhosttySelection.
|
||||
*
|
||||
* @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE)
|
||||
* @param options Word-selection options
|
||||
* @param[out] out_selection On success, receives the derived selection
|
||||
* @return GHOSTTY_SUCCESS on success, GHOSTTY_NO_VALUE if the valid ref has
|
||||
* no selectable word content, or GHOSTTY_INVALID_VALUE if the
|
||||
* terminal, options, ref, codepoint pointer, or output pointer are
|
||||
* invalid.
|
||||
*
|
||||
* @ingroup selection
|
||||
*/
|
||||
GHOSTTY_API GhosttyResult ghostty_terminal_select_word(
|
||||
GhosttyTerminal terminal,
|
||||
const GhosttyTerminalSelectWordOptions* options,
|
||||
GhosttySelection* out_selection);
|
||||
|
||||
/**
|
||||
* Derive the nearest word selection snapshot between two terminal grid refs.
|
||||
*
|
||||
* Starting at options->start, this searches toward options->end (inclusive)
|
||||
* and returns the first selectable word found using Ghostty's word-selection
|
||||
* rules.
|
||||
*
|
||||
* This is useful for implementing double-click-and-drag selection in a UI. If
|
||||
* a user double-clicks one word and drags across spaces or punctuation toward
|
||||
* another word, selecting only the word directly under the current pointer can
|
||||
* flicker or collapse when the pointer is between words. Instead, ask for the
|
||||
* nearest word between the original click and the drag point, ask again in the
|
||||
* reverse direction, and combine the two word bounds into the drag selection.
|
||||
*
|
||||
* @snippet c-vt-selection/src/main.c selection-word-between
|
||||
*
|
||||
* The returned selection is not installed as the terminal's current
|
||||
* selection. It is a snapshot with the same lifetime rules as GhosttySelection.
|
||||
*
|
||||
* @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE)
|
||||
* @param options Word-between-selection options
|
||||
* @param[out] out_selection On success, receives the derived selection
|
||||
* @return GHOSTTY_SUCCESS on success, GHOSTTY_NO_VALUE if there is no
|
||||
* selectable word content between the valid refs, or
|
||||
* GHOSTTY_INVALID_VALUE if the terminal, options, refs, codepoint
|
||||
* pointer, or output pointer are invalid.
|
||||
*
|
||||
* @ingroup selection
|
||||
*/
|
||||
GHOSTTY_API GhosttyResult ghostty_terminal_select_word_between(
|
||||
GhosttyTerminal terminal,
|
||||
const GhosttyTerminalSelectWordBetweenOptions* options,
|
||||
GhosttySelection* out_selection);
|
||||
|
||||
/**
|
||||
* Derive a line selection snapshot from a terminal grid reference.
|
||||
*
|
||||
* The returned selection is not installed as the terminal's current
|
||||
* selection. It is a snapshot with the same lifetime rules as GhosttySelection.
|
||||
*
|
||||
* @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE)
|
||||
* @param options Line-selection options
|
||||
* @param[out] out_selection On success, receives the derived selection
|
||||
* @return GHOSTTY_SUCCESS on success, GHOSTTY_NO_VALUE if the valid ref has
|
||||
* no selectable line content, or GHOSTTY_INVALID_VALUE if the
|
||||
* terminal, options, ref, codepoint pointer, or output pointer are
|
||||
* invalid.
|
||||
*
|
||||
* @ingroup selection
|
||||
*/
|
||||
GHOSTTY_API GhosttyResult ghostty_terminal_select_line(
|
||||
GhosttyTerminal terminal,
|
||||
const GhosttyTerminalSelectLineOptions* options,
|
||||
GhosttySelection* out_selection);
|
||||
|
||||
/**
|
||||
* Derive a selection snapshot covering all selectable terminal content.
|
||||
*
|
||||
* The returned selection is not installed as the terminal's current
|
||||
* selection. It is a snapshot with the same lifetime rules as GhosttySelection.
|
||||
*
|
||||
* @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE)
|
||||
* @param[out] out_selection On success, receives the derived selection
|
||||
* @return GHOSTTY_SUCCESS on success, GHOSTTY_NO_VALUE if there is no
|
||||
* selectable content, or GHOSTTY_INVALID_VALUE if the terminal or
|
||||
* output pointer is invalid.
|
||||
*
|
||||
* @ingroup selection
|
||||
*/
|
||||
GHOSTTY_API GhosttyResult ghostty_terminal_select_all(
|
||||
GhosttyTerminal terminal,
|
||||
GhosttySelection* out_selection);
|
||||
|
||||
/**
|
||||
* Derive a command-output selection snapshot from a terminal grid reference.
|
||||
*
|
||||
* The returned selection is not installed as the terminal's current
|
||||
* selection. It is a snapshot with the same lifetime rules as GhosttySelection.
|
||||
*
|
||||
* @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE)
|
||||
* @param ref Grid reference within command output to select
|
||||
* @param[out] out_selection On success, receives the derived selection
|
||||
* @return GHOSTTY_SUCCESS on success, GHOSTTY_NO_VALUE if the valid ref is
|
||||
* not selectable command output, or GHOSTTY_INVALID_VALUE if the
|
||||
* terminal, ref, or output pointer is invalid.
|
||||
*
|
||||
* @ingroup selection
|
||||
*/
|
||||
GHOSTTY_API GhosttyResult ghostty_terminal_select_output(
|
||||
GhosttyTerminal terminal,
|
||||
GhosttyGridRef ref,
|
||||
GhosttySelection* out_selection);
|
||||
|
||||
/**
|
||||
* Format a terminal selection into a caller-provided buffer.
|
||||
*
|
||||
* This is a one-shot convenience API for formatting either the terminal's
|
||||
* active selection or a caller-provided GhosttySelection without explicitly
|
||||
* creating a GhosttyFormatter.
|
||||
*
|
||||
* Pass NULL for buf to query the required output size. In that case,
|
||||
* out_written receives the required size and the function returns
|
||||
* GHOSTTY_OUT_OF_SPACE.
|
||||
*
|
||||
* If buf is too small, the function returns GHOSTTY_OUT_OF_SPACE and writes
|
||||
* the required size to out_written. The caller can then retry with a larger
|
||||
* buffer.
|
||||
*
|
||||
* If options.selection is NULL and the terminal has no active selection, the
|
||||
* function returns GHOSTTY_NO_VALUE.
|
||||
*
|
||||
* @param terminal The terminal to read from (must not be NULL)
|
||||
* @param options Selection formatting options
|
||||
* @param buf Output buffer, or NULL to query required size
|
||||
* @param buf_len Length of buf in bytes
|
||||
* @param out_written Number of bytes written, or required size on
|
||||
* GHOSTTY_OUT_OF_SPACE (must not be NULL)
|
||||
* @return GHOSTTY_SUCCESS on success, or an error code on failure
|
||||
*
|
||||
* @ingroup selection
|
||||
*/
|
||||
GHOSTTY_API GhosttyResult ghostty_terminal_selection_format_buf(
|
||||
GhosttyTerminal terminal,
|
||||
GhosttyTerminalSelectionFormatOptions options,
|
||||
uint8_t* buf,
|
||||
size_t buf_len,
|
||||
size_t* out_written);
|
||||
|
||||
/**
|
||||
* Format a terminal selection into an allocated buffer.
|
||||
*
|
||||
* This is a one-shot convenience API for formatting either the terminal's
|
||||
* active selection or a caller-provided GhosttySelection without explicitly
|
||||
* creating a GhosttyFormatter.
|
||||
*
|
||||
* The returned buffer is allocated using allocator, or the default allocator
|
||||
* if NULL is passed. The caller owns the returned buffer and must free it with
|
||||
* ghostty_free(), passing the same allocator and returned length.
|
||||
*
|
||||
* The returned bytes are not NUL-terminated. This supports plain text, VT, and
|
||||
* HTML uniformly as byte output.
|
||||
*
|
||||
* If options.selection is NULL and the terminal has no active selection, the
|
||||
* function returns GHOSTTY_NO_VALUE and leaves out_ptr as NULL and out_len as 0.
|
||||
*
|
||||
* @param terminal The terminal to read from (must not be NULL)
|
||||
* @param allocator Allocator used for the returned buffer, or NULL for the default allocator
|
||||
* @param options Selection formatting options
|
||||
* @param out_ptr Receives the allocated output buffer (must not be NULL)
|
||||
* @param out_len Receives the output length in bytes (must not be NULL)
|
||||
* @return GHOSTTY_SUCCESS on success, or an error code on failure
|
||||
*
|
||||
* @ingroup selection
|
||||
*/
|
||||
GHOSTTY_API GhosttyResult ghostty_terminal_selection_format_alloc(
|
||||
GhosttyTerminal terminal,
|
||||
const GhosttyAllocator* allocator,
|
||||
GhosttyTerminalSelectionFormatOptions options,
|
||||
uint8_t** out_ptr,
|
||||
size_t* out_len);
|
||||
|
||||
/**
|
||||
* Adjust a selection snapshot using terminal selection semantics.
|
||||
*
|
||||
* This mutates the caller-provided GhosttySelection in place. The logical end
|
||||
* endpoint is always moved, regardless of whether the selection is forward or
|
||||
* reversed visually. The input selection remains a snapshot: after adjustment,
|
||||
* call ghostty_terminal_set() with GHOSTTY_TERMINAL_OPT_SELECTION to install it
|
||||
* as the terminal-owned selection if desired.
|
||||
*
|
||||
* The selection's start and end grid refs must both be valid untracked
|
||||
* snapshots for the given terminal's currently active screen. In practice,
|
||||
* they must come from that terminal and screen, and no mutating terminal call
|
||||
* may have occurred since the refs were produced or reconstructed from
|
||||
* tracked refs. Passing refs from another terminal, another screen, or stale
|
||||
* refs violates this precondition.
|
||||
*
|
||||
* @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE)
|
||||
* @param selection Selection snapshot to adjust in place
|
||||
* @param adjustment The adjustment operation to apply
|
||||
* @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the terminal,
|
||||
* selection, or adjustment are invalid. Selection reference validity
|
||||
* is a precondition and is not checked.
|
||||
*
|
||||
* @ingroup selection
|
||||
*/
|
||||
GHOSTTY_API GhosttyResult ghostty_terminal_selection_adjust(
|
||||
GhosttyTerminal terminal,
|
||||
GhosttySelection* selection,
|
||||
GhosttySelectionAdjust adjustment);
|
||||
|
||||
/**
|
||||
* Get the current endpoint ordering of a selection snapshot.
|
||||
*
|
||||
* The selection's start and end grid refs must both be valid untracked
|
||||
* snapshots for the given terminal's currently active screen. In practice,
|
||||
* they must come from that terminal and screen, and no mutating terminal call
|
||||
* may have occurred since the refs were produced or reconstructed from
|
||||
* tracked refs. Passing refs from another terminal, another screen, or stale
|
||||
* refs violates this precondition.
|
||||
*
|
||||
* @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE)
|
||||
* @param selection Selection snapshot to inspect
|
||||
* @param[out] out_order On success, receives the selection order
|
||||
* @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the terminal,
|
||||
* selection, or output pointer are invalid. Selection reference
|
||||
* validity is a precondition and is not checked.
|
||||
*
|
||||
* @ingroup selection
|
||||
*/
|
||||
GHOSTTY_API GhosttyResult ghostty_terminal_selection_order(
|
||||
GhosttyTerminal terminal,
|
||||
const GhosttySelection* selection,
|
||||
GhosttySelectionOrder* out_order);
|
||||
|
||||
/**
|
||||
* Return a selection snapshot with endpoints ordered as requested.
|
||||
*
|
||||
* Use GHOSTTY_SELECTION_ORDER_FORWARD to get top-left to bottom-right bounds,
|
||||
* and GHOSTTY_SELECTION_ORDER_REVERSE to get bottom-right to top-left bounds.
|
||||
* Mirrored desired orders are accepted but normalized the same as forward.
|
||||
* The output selection is a fresh untracked snapshot and is not installed as
|
||||
* the terminal's current selection.
|
||||
*
|
||||
* The selection's start and end grid refs must both be valid untracked
|
||||
* snapshots for the given terminal's currently active screen. In practice,
|
||||
* they must come from that terminal and screen, and no mutating terminal call
|
||||
* may have occurred since the refs were produced or reconstructed from
|
||||
* tracked refs. Passing refs from another terminal, another screen, or stale
|
||||
* refs violates this precondition.
|
||||
*
|
||||
* @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE)
|
||||
* @param selection Selection snapshot to order
|
||||
* @param desired Desired endpoint order
|
||||
* @param[out] out_selection On success, receives the ordered selection
|
||||
* @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the terminal,
|
||||
* selection, desired order, or output pointer are invalid. Selection
|
||||
* reference validity is a precondition and is not checked.
|
||||
*
|
||||
* @ingroup selection
|
||||
*/
|
||||
GHOSTTY_API GhosttyResult ghostty_terminal_selection_ordered(
|
||||
GhosttyTerminal terminal,
|
||||
const GhosttySelection* selection,
|
||||
GhosttySelectionOrder desired,
|
||||
GhosttySelection* out_selection);
|
||||
|
||||
/**
|
||||
* Test whether a terminal point is inside a selection snapshot.
|
||||
*
|
||||
* This uses the same selection semantics as the terminal, including
|
||||
* rectangular/block selections and linear selections spanning multiple rows.
|
||||
*
|
||||
* The selection's start and end grid refs must both be valid untracked
|
||||
* snapshots for the given terminal's currently active screen. In practice,
|
||||
* they must come from that terminal and screen, and no mutating terminal call
|
||||
* may have occurred since the refs were produced or reconstructed from
|
||||
* tracked refs. Passing refs from another terminal, another screen, or stale
|
||||
* refs violates this precondition.
|
||||
*
|
||||
* @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE)
|
||||
* @param selection Selection snapshot to inspect
|
||||
* @param point Point to test for containment
|
||||
* @param[out] out_contains On success, receives whether point is inside selection
|
||||
* @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the terminal,
|
||||
* selection, point, or output pointer are invalid. Selection reference
|
||||
* validity is a precondition and is not checked.
|
||||
*
|
||||
* @ingroup selection
|
||||
*/
|
||||
GHOSTTY_API GhosttyResult ghostty_terminal_selection_contains(
|
||||
GhosttyTerminal terminal,
|
||||
const GhosttySelection* selection,
|
||||
GhosttyPoint point,
|
||||
bool* out_contains);
|
||||
|
||||
/**
|
||||
* Test whether two selection snapshots are equal.
|
||||
*
|
||||
* Equality uses the terminal's internal selection semantics: both endpoint
|
||||
* pins must match and both selections must have the same rectangular/block
|
||||
* state. This avoids requiring callers to compare raw GhosttyGridRef internals.
|
||||
*
|
||||
* Both selections' start and end grid refs must be valid untracked snapshots
|
||||
* for the given terminal's currently active screen. In practice, they must
|
||||
* come from that terminal and screen, and no mutating terminal call may have
|
||||
* occurred since the refs were produced or reconstructed from tracked refs.
|
||||
* Passing refs from another terminal, another screen, or stale refs violates
|
||||
* this precondition.
|
||||
*
|
||||
* @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE)
|
||||
* @param a First selection snapshot to compare
|
||||
* @param b Second selection snapshot to compare
|
||||
* @param[out] out_equal On success, receives whether the selections are equal
|
||||
* @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the terminal,
|
||||
* selections, or output pointer are invalid. Selection reference
|
||||
* validity is a precondition and is not checked.
|
||||
*
|
||||
* @ingroup selection
|
||||
*/
|
||||
GHOSTTY_API GhosttyResult ghostty_terminal_selection_equal(
|
||||
GhosttyTerminal terminal,
|
||||
const GhosttySelection* a,
|
||||
const GhosttySelection* b,
|
||||
bool* out_equal);
|
||||
|
||||
/** @} */
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#include <ghostty/vt/kitty_graphics.h>
|
||||
#include <ghostty/vt/screen.h>
|
||||
#include <ghostty/vt/point.h>
|
||||
#include <ghostty/vt/selection.h>
|
||||
#include <ghostty/vt/style.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
@@ -592,6 +593,21 @@ typedef enum GHOSTTY_ENUM_TYPED {
|
||||
* Input type: size_t*
|
||||
*/
|
||||
GHOSTTY_TERMINAL_OPT_APC_MAX_BYTES_KITTY = 20,
|
||||
|
||||
/**
|
||||
* Set the active screen selection.
|
||||
*
|
||||
* The value must point to a GhosttySelection whose grid references are
|
||||
* valid for this terminal's active screen at the time of the call. The
|
||||
* terminal copies the selection immediately and converts it to
|
||||
* terminal-owned tracked state, so the GhosttySelection struct and its
|
||||
* untracked grid references do not need to outlive this call.
|
||||
*
|
||||
* Passing NULL clears the active screen selection.
|
||||
*
|
||||
* Input type: GhosttySelection*
|
||||
*/
|
||||
GHOSTTY_TERMINAL_OPT_SELECTION = 21,
|
||||
GHOSTTY_TERMINAL_OPT_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
|
||||
} GhosttyTerminalOption;
|
||||
|
||||
@@ -868,6 +884,23 @@ typedef enum GHOSTTY_ENUM_TYPED {
|
||||
* Output type: GhosttyKittyGraphics *
|
||||
*/
|
||||
GHOSTTY_TERMINAL_DATA_KITTY_GRAPHICS = 30,
|
||||
|
||||
/**
|
||||
* The active screen's current selection.
|
||||
*
|
||||
* On success, writes an untracked snapshot of the terminal-owned selection
|
||||
* to the caller-provided GhosttySelection. The GhosttySelection struct is
|
||||
* caller-owned and may be kept, but the grid references inside it are
|
||||
* untracked borrowed references into the active screen. They are only valid
|
||||
* until the next mutating terminal call, such as ghostty_terminal_set(),
|
||||
* ghostty_terminal_vt_write(), ghostty_terminal_resize(), or
|
||||
* ghostty_terminal_reset().
|
||||
*
|
||||
* Returns GHOSTTY_NO_VALUE when there is no active selection.
|
||||
*
|
||||
* Output type: GhosttySelection *
|
||||
*/
|
||||
GHOSTTY_TERMINAL_DATA_SELECTION = 31,
|
||||
GHOSTTY_TERMINAL_DATA_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
|
||||
} GhosttyTerminalData;
|
||||
|
||||
|
||||
@@ -194,6 +194,23 @@ typedef struct GhosttyOscCommandImpl* GhosttyOscCommand;
|
||||
|
||||
/* ---- Common value types ---- */
|
||||
|
||||
/**
|
||||
* Terminal content output format.
|
||||
*
|
||||
* @ingroup formatter
|
||||
*/
|
||||
typedef enum GHOSTTY_ENUM_TYPED {
|
||||
/** Plain text (no escape sequences). */
|
||||
GHOSTTY_FORMATTER_FORMAT_PLAIN,
|
||||
|
||||
/** VT sequences preserving colors, styles, URLs, etc. */
|
||||
GHOSTTY_FORMATTER_FORMAT_VT,
|
||||
|
||||
/** HTML with inline styles. */
|
||||
GHOSTTY_FORMATTER_FORMAT_HTML,
|
||||
GHOSTTY_FORMATTER_FORMAT_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
|
||||
} GhosttyFormatterFormat;
|
||||
|
||||
/**
|
||||
* A borrowed byte string (pointer + length).
|
||||
*
|
||||
|
||||
@@ -49,6 +49,7 @@ const string = @import("string.zig");
|
||||
const terminal = struct {
|
||||
const CursorStyle = @import("../terminal/cursor.zig").Style;
|
||||
const color = @import("../terminal/color.zig");
|
||||
const selection_codepoints = @import("../terminal/selection_codepoints.zig");
|
||||
const style = @import("../terminal/style.zig");
|
||||
const x11_color = @import("../terminal/x11_color.zig");
|
||||
};
|
||||
@@ -6149,32 +6150,8 @@ pub const RepeatableString = struct {
|
||||
pub const SelectionWordChars = struct {
|
||||
const Self = @This();
|
||||
|
||||
/// Default boundary characters: ` \t'"│`|:;,()[]{}<>$`
|
||||
const default_codepoints = [_]u21{
|
||||
0, // null
|
||||
' ', // space
|
||||
'\t', // tab
|
||||
'\'', // single quote
|
||||
'"', // double quote
|
||||
'│', // U+2502 box drawing
|
||||
'`', // backtick
|
||||
'|', // pipe
|
||||
':', // colon
|
||||
';', // semicolon
|
||||
',', // comma
|
||||
'(', // left paren
|
||||
')', // right paren
|
||||
'[', // left bracket
|
||||
']', // right bracket
|
||||
'{', // left brace
|
||||
'}', // right brace
|
||||
'<', // less than
|
||||
'>', // greater than
|
||||
'$', // dollar
|
||||
};
|
||||
|
||||
/// The parsed codepoints. Always includes null (U+0000) at index 0.
|
||||
codepoints: []const u21 = &default_codepoints,
|
||||
codepoints: []const u21 = &terminal.selection_codepoints.default_word_boundaries,
|
||||
|
||||
pub fn parseCLI(self: *Self, alloc: Allocator, input: ?[]const u8) !void {
|
||||
const value = input orelse return error.ValueRequired;
|
||||
|
||||
@@ -209,6 +209,8 @@ comptime {
|
||||
@export(&c.formatter_format_buf, .{ .name = "ghostty_formatter_format_buf" });
|
||||
@export(&c.formatter_format_alloc, .{ .name = "ghostty_formatter_format_alloc" });
|
||||
@export(&c.formatter_free, .{ .name = "ghostty_formatter_free" });
|
||||
@export(&c.terminal_selection_format_buf, .{ .name = "ghostty_terminal_selection_format_buf" });
|
||||
@export(&c.terminal_selection_format_alloc, .{ .name = "ghostty_terminal_selection_format_alloc" });
|
||||
@export(&c.render_state_new, .{ .name = "ghostty_render_state_new" });
|
||||
@export(&c.render_state_update, .{ .name = "ghostty_render_state_update" });
|
||||
@export(&c.render_state_get, .{ .name = "ghostty_render_state_get" });
|
||||
@@ -239,6 +241,16 @@ comptime {
|
||||
@export(&c.terminal_mode_set, .{ .name = "ghostty_terminal_mode_set" });
|
||||
@export(&c.terminal_get, .{ .name = "ghostty_terminal_get" });
|
||||
@export(&c.terminal_get_multi, .{ .name = "ghostty_terminal_get_multi" });
|
||||
@export(&c.terminal_select_word, .{ .name = "ghostty_terminal_select_word" });
|
||||
@export(&c.terminal_select_word_between, .{ .name = "ghostty_terminal_select_word_between" });
|
||||
@export(&c.terminal_select_line, .{ .name = "ghostty_terminal_select_line" });
|
||||
@export(&c.terminal_select_all, .{ .name = "ghostty_terminal_select_all" });
|
||||
@export(&c.terminal_select_output, .{ .name = "ghostty_terminal_select_output" });
|
||||
@export(&c.terminal_selection_adjust, .{ .name = "ghostty_terminal_selection_adjust" });
|
||||
@export(&c.terminal_selection_order, .{ .name = "ghostty_terminal_selection_order" });
|
||||
@export(&c.terminal_selection_ordered, .{ .name = "ghostty_terminal_selection_ordered" });
|
||||
@export(&c.terminal_selection_contains, .{ .name = "ghostty_terminal_selection_contains" });
|
||||
@export(&c.terminal_selection_equal, .{ .name = "ghostty_terminal_selection_equal" });
|
||||
@export(&c.terminal_grid_ref, .{ .name = "ghostty_terminal_grid_ref" });
|
||||
@export(&c.terminal_grid_ref_track, .{ .name = "ghostty_terminal_grid_ref_track" });
|
||||
@export(&c.terminal_point_from_grid_ref, .{ .name = "ghostty_terminal_point_from_grid_ref" });
|
||||
|
||||
@@ -658,6 +658,11 @@ pub fn deinit(self: *PageList) void {
|
||||
pub fn reset(self: *PageList) void {
|
||||
defer self.assertIntegrity();
|
||||
|
||||
// Invalidate all external page refs to the previous list. The reset below
|
||||
// rebuilds the page list from the pools, so old untracked refs must be
|
||||
// rejected before any validation attempts to inspect their node pointers.
|
||||
self.page_serial_min = self.page_serial;
|
||||
|
||||
// We need enough pages/nodes to keep our active area. This should
|
||||
// never fail since we by definition have allocated a page already
|
||||
// that fits our size but I'm not confident to make that assertion.
|
||||
@@ -13543,6 +13548,30 @@ test "PageList reset" {
|
||||
}, s.getTopLeft(.active));
|
||||
}
|
||||
|
||||
test "PageList reset invalidates stale untracked refs even if node memory is reused" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try init(alloc, 80, 24, null);
|
||||
defer s.deinit();
|
||||
|
||||
const old_serial = s.pages.first.?.serial;
|
||||
try testing.expect(old_serial >= s.page_serial_min);
|
||||
try testing.expect(old_serial < s.page_serial);
|
||||
|
||||
s.reset();
|
||||
|
||||
// The important safety property is that stale serials are rejected before
|
||||
// the node pointer is inspected. Reset rebuilds the page list from the
|
||||
// pools, so old untracked refs may contain node pointers that are no
|
||||
// longer safe to dereference.
|
||||
try testing.expect(old_serial < s.page_serial_min);
|
||||
|
||||
const new_serial = s.pages.first.?.serial;
|
||||
try testing.expect(new_serial >= s.page_serial_min);
|
||||
try testing.expect(new_serial < s.page_serial);
|
||||
}
|
||||
|
||||
test "PageList reset across two pages" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
@@ -13,6 +13,7 @@ const tripwire = @import("../tripwire.zig");
|
||||
const unicode = @import("../unicode/main.zig");
|
||||
const Selection = @import("Selection.zig");
|
||||
const PageList = @import("PageList.zig");
|
||||
const selection_codepoints = @import("selection_codepoints.zig");
|
||||
const StringMap = @import("StringMap.zig");
|
||||
const ScreenFormatter = @import("formatter.zig").ScreenFormatter;
|
||||
const osc = @import("osc.zig");
|
||||
@@ -2516,7 +2517,7 @@ pub const SelectLine = struct {
|
||||
|
||||
/// These are the codepoints to consider whitespace to trim
|
||||
/// from the ends of the selection.
|
||||
whitespace: ?[]const u21 = &.{ 0, ' ', '\t' },
|
||||
whitespace: ?[]const u21 = &selection_codepoints.default_line_whitespace,
|
||||
|
||||
/// If true, line selection will consider semantic prompt
|
||||
/// state changing a boundary. State changing is ANY state
|
||||
@@ -2652,10 +2653,10 @@ pub fn selectLine(self: *const Screen, opts: SelectLine) ?Selection {
|
||||
if (!cell.hasText()) continue;
|
||||
|
||||
// Non-empty means we found it.
|
||||
const this_whitespace = std.mem.indexOfAny(
|
||||
const this_whitespace = std.mem.indexOfScalar(
|
||||
u21,
|
||||
whitespace,
|
||||
&[_]u21{cell.content.codepoint},
|
||||
cell.content.codepoint,
|
||||
) != null;
|
||||
if (this_whitespace) continue;
|
||||
|
||||
@@ -2674,10 +2675,10 @@ pub fn selectLine(self: *const Screen, opts: SelectLine) ?Selection {
|
||||
if (!cell.hasText()) continue;
|
||||
|
||||
// Non-empty means we found it.
|
||||
const this_whitespace = std.mem.indexOfAny(
|
||||
const this_whitespace = std.mem.indexOfScalar(
|
||||
u21,
|
||||
whitespace,
|
||||
&[_]u21{cell.content.codepoint},
|
||||
cell.content.codepoint,
|
||||
) != null;
|
||||
if (this_whitespace) continue;
|
||||
|
||||
@@ -2798,10 +2799,10 @@ pub fn selectWord(
|
||||
if (!start_cell.hasText()) return null;
|
||||
|
||||
// Determine if we are a boundary or not to determine what our boundary is.
|
||||
const expect_boundary = std.mem.indexOfAny(
|
||||
const expect_boundary = std.mem.indexOfScalar(
|
||||
u21,
|
||||
boundary_codepoints,
|
||||
&[_]u21{start_cell.content.codepoint},
|
||||
start_cell.content.codepoint,
|
||||
) != null;
|
||||
|
||||
// Go forwards to find our end boundary
|
||||
@@ -2816,10 +2817,10 @@ pub fn selectWord(
|
||||
if (!cell.hasText()) break :end prev;
|
||||
|
||||
// If we do not match our expected set, we hit a boundary
|
||||
const this_boundary = std.mem.indexOfAny(
|
||||
const this_boundary = std.mem.indexOfScalar(
|
||||
u21,
|
||||
boundary_codepoints,
|
||||
&[_]u21{cell.content.codepoint},
|
||||
cell.content.codepoint,
|
||||
) != null;
|
||||
if (this_boundary != expect_boundary) break :end prev;
|
||||
|
||||
@@ -2853,10 +2854,10 @@ pub fn selectWord(
|
||||
if (!cell.hasText()) break :start prev;
|
||||
|
||||
// If we do not match our expected set, we hit a boundary
|
||||
const this_boundary = std.mem.indexOfAny(
|
||||
const this_boundary = std.mem.indexOfScalar(
|
||||
u21,
|
||||
boundary_codepoints,
|
||||
&[_]u21{cell.content.codepoint},
|
||||
cell.content.codepoint,
|
||||
) != null;
|
||||
if (this_boundary != expect_boundary) break :start prev;
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ const Selection = @This();
|
||||
const std = @import("std");
|
||||
const assert = @import("../quirks.zig").inlineAssert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const lib = @import("lib.zig");
|
||||
const page = @import("page.zig");
|
||||
const point = @import("point.zig");
|
||||
const PageList = @import("PageList.zig");
|
||||
@@ -195,7 +196,12 @@ pub fn bottomRight(self: Selection, s: *const Screen) Pin {
|
||||
/// operations only flip the x or y axis, not both. Depending on the y axis
|
||||
/// direction, this is either mirrored_forward or mirrored_reverse.
|
||||
///
|
||||
pub const Order = enum { forward, reverse, mirrored_forward, mirrored_reverse };
|
||||
pub const Order = lib.Enum(lib.target, &.{
|
||||
"forward",
|
||||
"reverse",
|
||||
"mirrored_forward",
|
||||
"mirrored_reverse",
|
||||
});
|
||||
|
||||
pub fn order(self: Selection, s: *const Screen) Order {
|
||||
const start_pt = s.pages.pointFromPin(.screen, self.start()).?.screen;
|
||||
@@ -389,18 +395,18 @@ pub fn containedRowCached(
|
||||
}
|
||||
|
||||
/// Possible adjustments to the selection.
|
||||
pub const Adjustment = enum {
|
||||
left,
|
||||
right,
|
||||
up,
|
||||
down,
|
||||
home,
|
||||
end,
|
||||
page_up,
|
||||
page_down,
|
||||
beginning_of_line,
|
||||
end_of_line,
|
||||
};
|
||||
pub const Adjustment = lib.Enum(lib.target, &.{
|
||||
"left",
|
||||
"right",
|
||||
"up",
|
||||
"down",
|
||||
"home",
|
||||
"end",
|
||||
"page_up",
|
||||
"page_down",
|
||||
"beginning_of_line",
|
||||
"end_of_line",
|
||||
});
|
||||
|
||||
/// Adjust the selection by some given adjustment. An adjustment allows
|
||||
/// a selection to be expanded slightly left, right, up, down, etc.
|
||||
|
||||
@@ -170,6 +170,18 @@ pub const terminal_mode_get = terminal.mode_get;
|
||||
pub const terminal_mode_set = terminal.mode_set;
|
||||
pub const terminal_get = terminal.get;
|
||||
pub const terminal_get_multi = terminal.get_multi;
|
||||
pub const terminal_select_word = selection.word;
|
||||
pub const terminal_select_word_between = selection.word_between;
|
||||
pub const terminal_select_line = selection.line;
|
||||
pub const terminal_select_all = selection.all;
|
||||
pub const terminal_select_output = selection.output;
|
||||
pub const terminal_selection_format_buf = selection.format_buf;
|
||||
pub const terminal_selection_format_alloc = selection.format_alloc;
|
||||
pub const terminal_selection_adjust = selection.adjust;
|
||||
pub const terminal_selection_order = selection.order;
|
||||
pub const terminal_selection_ordered = selection.ordered;
|
||||
pub const terminal_selection_contains = selection.contains;
|
||||
pub const terminal_selection_equal = selection.equal;
|
||||
pub const terminal_grid_ref = terminal.grid_ref;
|
||||
pub const terminal_grid_ref_track = terminal.grid_ref_track;
|
||||
pub const terminal_point_from_grid_ref = terminal.point_from_grid_ref;
|
||||
|
||||
@@ -31,6 +31,7 @@ const RowIteratorWrapper = struct {
|
||||
/// These are the raw pointers into the render state data.
|
||||
raws: []const page.Row,
|
||||
cells: []const std.MultiArrayList(renderpkg.RenderState.Cell),
|
||||
selection: []const ?[2]size.CellCountInt,
|
||||
dirty: []bool,
|
||||
|
||||
/// The color palette from the render state, needed to resolve
|
||||
@@ -61,6 +62,13 @@ pub const RowCells = ?*RowCellsWrapper;
|
||||
/// C: GhosttyRenderStateDirty
|
||||
pub const Dirty = renderpkg.RenderState.Dirty;
|
||||
|
||||
/// C: GhosttyRenderStateRowSelection
|
||||
pub const RowSelection = extern struct {
|
||||
size: usize = @sizeOf(RowSelection),
|
||||
start_x: u16 = 0,
|
||||
end_x: u16 = 0,
|
||||
};
|
||||
|
||||
/// C: GhosttyRenderStateCursorVisualStyle
|
||||
pub const CursorVisualStyle = enum(c_int) {
|
||||
bar = 0,
|
||||
@@ -241,6 +249,7 @@ fn getTyped(
|
||||
.y = null,
|
||||
.raws = row_data.items(.raw),
|
||||
.cells = row_data.items(.cells),
|
||||
.selection = row_data.items(.selection),
|
||||
.dirty = row_data.items(.dirty),
|
||||
.palette = &state.state.colors.palette,
|
||||
};
|
||||
@@ -381,6 +390,7 @@ pub fn row_iterator_new(
|
||||
.y = undefined,
|
||||
.raws = undefined,
|
||||
.cells = undefined,
|
||||
.selection = undefined,
|
||||
.dirty = undefined,
|
||||
.palette = undefined,
|
||||
};
|
||||
@@ -564,6 +574,7 @@ pub const RowData = enum(c_int) {
|
||||
dirty = 1,
|
||||
raw = 2,
|
||||
cells = 3,
|
||||
selection = 4,
|
||||
|
||||
/// Output type expected for querying the data of the given kind.
|
||||
pub fn OutType(comptime self: RowData) type {
|
||||
@@ -572,6 +583,7 @@ pub const RowData = enum(c_int) {
|
||||
.dirty => bool,
|
||||
.raw => row.CRow,
|
||||
.cells => RowCells,
|
||||
.selection => RowSelection,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -654,6 +666,14 @@ fn rowGetTyped(
|
||||
.palette = it.palette,
|
||||
};
|
||||
},
|
||||
.selection => {
|
||||
const out_size = out.size;
|
||||
if (out_size < @sizeOf(RowSelection)) return .invalid_value;
|
||||
|
||||
const sel = it.selection[y] orelse return .no_value;
|
||||
out.start_x = sel[0];
|
||||
out.end_x = sel[1];
|
||||
},
|
||||
}
|
||||
|
||||
return .success;
|
||||
@@ -845,6 +865,7 @@ test "render: row iterator new/free" {
|
||||
try testing.expectEqual(@as(?size.CellCountInt, null), iterator_ptr.y);
|
||||
try testing.expectEqual(row_data.items(.raw).len, iterator_ptr.raws.len);
|
||||
try testing.expectEqual(row_data.items(.cells).len, iterator_ptr.cells.len);
|
||||
try testing.expectEqual(row_data.items(.selection).len, iterator_ptr.selection.len);
|
||||
try testing.expectEqual(row_data.items(.dirty).len, iterator_ptr.dirty.len);
|
||||
}
|
||||
|
||||
@@ -1026,6 +1047,60 @@ test "render: row get/set dirty" {
|
||||
try testing.expect(!dirty);
|
||||
}
|
||||
|
||||
test "render: row get selection" {
|
||||
var terminal: terminal_c.Terminal = null;
|
||||
try testing.expectEqual(Result.success, terminal_c.new(
|
||||
&lib.alloc.test_allocator,
|
||||
&terminal,
|
||||
.{
|
||||
.cols = 10,
|
||||
.rows = 3,
|
||||
.max_scrollback = 10_000,
|
||||
},
|
||||
));
|
||||
defer terminal_c.free(terminal);
|
||||
|
||||
const t = terminal.?.terminal;
|
||||
const screen = t.screens.active;
|
||||
try screen.select(.init(
|
||||
screen.pages.pin(.{ .active = .{ .x = 2, .y = 1 } }).?,
|
||||
screen.pages.pin(.{ .active = .{ .x = 4, .y = 1 } }).?,
|
||||
false,
|
||||
));
|
||||
|
||||
var state: RenderState = null;
|
||||
try testing.expectEqual(Result.success, new(
|
||||
&lib.alloc.test_allocator,
|
||||
&state,
|
||||
));
|
||||
defer free(state);
|
||||
|
||||
try testing.expectEqual(Result.success, update(state, terminal));
|
||||
|
||||
var it: RowIterator = null;
|
||||
try testing.expectEqual(Result.success, row_iterator_new(
|
||||
&lib.alloc.test_allocator,
|
||||
&it,
|
||||
));
|
||||
defer row_iterator_free(it);
|
||||
|
||||
try testing.expectEqual(Result.success, get(state, .row_iterator, @ptrCast(&it)));
|
||||
|
||||
var sel: RowSelection = .{};
|
||||
try testing.expect(row_iterator_next(it));
|
||||
try testing.expectEqual(Result.no_value, row_get(it, .selection, @ptrCast(&sel)));
|
||||
|
||||
try testing.expect(row_iterator_next(it));
|
||||
sel = .{};
|
||||
try testing.expectEqual(Result.success, row_get(it, .selection, @ptrCast(&sel)));
|
||||
try testing.expectEqual(@as(u16, 2), sel.start_x);
|
||||
try testing.expectEqual(@as(u16, 4), sel.end_x);
|
||||
|
||||
try testing.expect(row_iterator_next(it));
|
||||
sel = .{};
|
||||
try testing.expectEqual(Result.no_value, row_get(it, .selection, @ptrCast(&sel)));
|
||||
}
|
||||
|
||||
test "render: row iterator next" {
|
||||
var terminal: terminal_c.Terminal = null;
|
||||
try testing.expectEqual(Result.success, terminal_c.new(
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
const std = @import("std");
|
||||
const testing = std.testing;
|
||||
const lib = @import("../lib.zig");
|
||||
const CAllocator = lib.alloc.Allocator;
|
||||
const formatterpkg = @import("../formatter.zig");
|
||||
const grid_ref = @import("grid_ref.zig");
|
||||
const point = @import("../point.zig");
|
||||
const selection_codepoints = @import("../selection_codepoints.zig");
|
||||
const Selection = @import("../Selection.zig");
|
||||
const Result = @import("result.zig").Result;
|
||||
const terminal_c = @import("terminal.zig");
|
||||
|
||||
const log = std.log.scoped(.selection_c);
|
||||
|
||||
pub const Adjustment = Selection.Adjustment;
|
||||
pub const Order = Selection.Order;
|
||||
pub const Format = formatterpkg.Format;
|
||||
|
||||
/// C: GhosttySelection
|
||||
pub const CSelection = extern struct {
|
||||
@@ -13,4 +28,504 @@ pub const CSelection = extern struct {
|
||||
const end_pin = self.end.toPin() orelse return null;
|
||||
return Selection.init(start_pin, end_pin, self.rectangle);
|
||||
}
|
||||
|
||||
pub fn fromZig(sel: Selection) CSelection {
|
||||
return .{
|
||||
.start = .fromPin(sel.start()),
|
||||
.end = .fromPin(sel.end()),
|
||||
.rectangle = sel.rectangle,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// C: GhosttyTerminalSelectWordOptions
|
||||
pub const SelectWordOptions = extern struct {
|
||||
size: usize = @sizeOf(SelectWordOptions),
|
||||
ref: grid_ref.CGridRef,
|
||||
boundary_codepoints: ?[*]const u32 = null,
|
||||
boundary_codepoints_len: usize = 0,
|
||||
};
|
||||
|
||||
/// C: GhosttyTerminalSelectWordBetweenOptions
|
||||
pub const SelectWordBetweenOptions = extern struct {
|
||||
size: usize = @sizeOf(SelectWordBetweenOptions),
|
||||
start: grid_ref.CGridRef,
|
||||
end: grid_ref.CGridRef,
|
||||
boundary_codepoints: ?[*]const u32 = null,
|
||||
boundary_codepoints_len: usize = 0,
|
||||
};
|
||||
|
||||
/// C: GhosttyTerminalSelectLineOptions
|
||||
pub const SelectLineOptions = extern struct {
|
||||
size: usize = @sizeOf(SelectLineOptions),
|
||||
ref: grid_ref.CGridRef,
|
||||
whitespace: ?[*]const u32 = null,
|
||||
whitespace_len: usize = 0,
|
||||
semantic_prompt_boundary: bool = false,
|
||||
};
|
||||
|
||||
/// C: GhosttyTerminalSelectionFormatOptions
|
||||
pub const FormatOptions = extern struct {
|
||||
size: usize = @sizeOf(FormatOptions),
|
||||
emit: Format,
|
||||
unwrap: bool,
|
||||
trim: bool,
|
||||
selection: ?*const CSelection = null,
|
||||
};
|
||||
|
||||
pub fn word(
|
||||
terminal: terminal_c.Terminal,
|
||||
options: ?*const SelectWordOptions,
|
||||
out_selection: ?*CSelection,
|
||||
) callconv(lib.calling_conv) Result {
|
||||
const t = terminal_c.zigTerminal(terminal) orelse return .invalid_value;
|
||||
const opts = options orelse return .invalid_value;
|
||||
if (opts.size < @sizeOf(SelectWordOptions)) return .invalid_value;
|
||||
const out = out_selection orelse return .invalid_value;
|
||||
|
||||
const boundary_codepoints = codepointSlice(
|
||||
opts.boundary_codepoints,
|
||||
opts.boundary_codepoints_len,
|
||||
) catch return .invalid_value;
|
||||
|
||||
const screen = t.screens.active;
|
||||
const pin = opts.ref.toPin() orelse return .invalid_value;
|
||||
out.* = .fromZig(screen.selectWord(
|
||||
pin,
|
||||
boundary_codepoints orelse &selection_codepoints.default_word_boundaries,
|
||||
) orelse
|
||||
return .no_value);
|
||||
return .success;
|
||||
}
|
||||
|
||||
pub fn word_between(
|
||||
terminal: terminal_c.Terminal,
|
||||
options: ?*const SelectWordBetweenOptions,
|
||||
out_selection: ?*CSelection,
|
||||
) callconv(lib.calling_conv) Result {
|
||||
const t = terminal_c.zigTerminal(terminal) orelse return .invalid_value;
|
||||
const opts = options orelse return .invalid_value;
|
||||
if (opts.size < @sizeOf(SelectWordBetweenOptions)) return .invalid_value;
|
||||
const out = out_selection orelse return .invalid_value;
|
||||
|
||||
const boundary_codepoints = codepointSlice(
|
||||
opts.boundary_codepoints,
|
||||
opts.boundary_codepoints_len,
|
||||
) catch return .invalid_value;
|
||||
|
||||
const screen = t.screens.active;
|
||||
const start = opts.start.toPin() orelse return .invalid_value;
|
||||
const end = opts.end.toPin() orelse return .invalid_value;
|
||||
out.* = .fromZig(screen.selectWordBetween(
|
||||
start,
|
||||
end,
|
||||
boundary_codepoints orelse &selection_codepoints.default_word_boundaries,
|
||||
) orelse
|
||||
return .no_value);
|
||||
return .success;
|
||||
}
|
||||
|
||||
pub fn line(
|
||||
terminal: terminal_c.Terminal,
|
||||
options: ?*const SelectLineOptions,
|
||||
out_selection: ?*CSelection,
|
||||
) callconv(lib.calling_conv) Result {
|
||||
const t = terminal_c.zigTerminal(terminal) orelse return .invalid_value;
|
||||
const opts = options orelse return .invalid_value;
|
||||
if (opts.size < @sizeOf(SelectLineOptions)) return .invalid_value;
|
||||
const out = out_selection orelse return .invalid_value;
|
||||
|
||||
const whitespace = codepointSlice(
|
||||
opts.whitespace,
|
||||
opts.whitespace_len,
|
||||
) catch return .invalid_value;
|
||||
|
||||
const screen = t.screens.active;
|
||||
const pin = opts.ref.toPin() orelse return .invalid_value;
|
||||
out.* = .fromZig(screen.selectLine(.{
|
||||
.pin = pin,
|
||||
.whitespace = whitespace orelse &selection_codepoints.default_line_whitespace,
|
||||
.semantic_prompt_boundary = opts.semantic_prompt_boundary,
|
||||
}) orelse return .no_value);
|
||||
return .success;
|
||||
}
|
||||
|
||||
pub fn all(
|
||||
terminal: terminal_c.Terminal,
|
||||
out_selection: ?*CSelection,
|
||||
) callconv(lib.calling_conv) Result {
|
||||
const t = terminal_c.zigTerminal(terminal) orelse return .invalid_value;
|
||||
const out = out_selection orelse return .invalid_value;
|
||||
|
||||
out.* = .fromZig(t.screens.active.selectAll() orelse return .no_value);
|
||||
return .success;
|
||||
}
|
||||
|
||||
pub fn output(
|
||||
terminal: terminal_c.Terminal,
|
||||
ref: grid_ref.CGridRef,
|
||||
out_selection: ?*CSelection,
|
||||
) callconv(lib.calling_conv) Result {
|
||||
const t = terminal_c.zigTerminal(terminal) orelse return .invalid_value;
|
||||
const out = out_selection orelse return .invalid_value;
|
||||
|
||||
const screen = t.screens.active;
|
||||
const pin = ref.toPin() orelse return .invalid_value;
|
||||
out.* = .fromZig(screen.selectOutput(pin) orelse return .no_value);
|
||||
return .success;
|
||||
}
|
||||
|
||||
pub fn format_buf(
|
||||
terminal: terminal_c.Terminal,
|
||||
opts: FormatOptions,
|
||||
out_: ?[*]u8,
|
||||
out_len: usize,
|
||||
out_written: *usize,
|
||||
) callconv(lib.calling_conv) Result {
|
||||
const t = terminal_c.zigTerminal(terminal) orelse return .invalid_value;
|
||||
|
||||
if (out_ == null) {
|
||||
var discarding: std.Io.Writer.Discarding = .init(&.{});
|
||||
formatSelection(t, opts, &discarding.writer) catch |err| return switch (err) {
|
||||
error.InvalidValue => .invalid_value,
|
||||
error.NoValue => .no_value,
|
||||
error.WriteFailed => unreachable,
|
||||
};
|
||||
out_written.* = @intCast(discarding.count);
|
||||
return .out_of_space;
|
||||
}
|
||||
|
||||
var writer: std.Io.Writer = .fixed(out_.?[0..out_len]);
|
||||
formatSelection(t, opts, &writer) catch |err| switch (err) {
|
||||
error.InvalidValue => return .invalid_value,
|
||||
error.NoValue => return .no_value,
|
||||
error.WriteFailed => {
|
||||
var discarding: std.Io.Writer.Discarding = .init(&.{});
|
||||
formatSelection(t, opts, &discarding.writer) catch unreachable;
|
||||
out_written.* = @intCast(discarding.count);
|
||||
return .out_of_space;
|
||||
},
|
||||
};
|
||||
|
||||
out_written.* = writer.end;
|
||||
return .success;
|
||||
}
|
||||
|
||||
pub fn format_alloc(
|
||||
terminal: terminal_c.Terminal,
|
||||
alloc_: ?*const CAllocator,
|
||||
opts: FormatOptions,
|
||||
out_ptr: *?[*]u8,
|
||||
out_len: *usize,
|
||||
) callconv(lib.calling_conv) Result {
|
||||
out_ptr.* = null;
|
||||
out_len.* = 0;
|
||||
|
||||
const t = terminal_c.zigTerminal(terminal) orelse return .invalid_value;
|
||||
const alloc = lib.alloc.default(alloc_);
|
||||
|
||||
var aw: std.Io.Writer.Allocating = .init(alloc);
|
||||
defer aw.deinit();
|
||||
|
||||
formatSelection(t, opts, &aw.writer) catch |err| return switch (err) {
|
||||
error.InvalidValue => .invalid_value,
|
||||
error.NoValue => .no_value,
|
||||
error.WriteFailed => .out_of_memory,
|
||||
};
|
||||
|
||||
const buf = aw.toOwnedSlice() catch return .out_of_memory;
|
||||
out_ptr.* = buf.ptr;
|
||||
out_len.* = buf.len;
|
||||
return .success;
|
||||
}
|
||||
|
||||
fn formatSelection(
|
||||
t: *terminal_c.ZigTerminal,
|
||||
opts: FormatOptions,
|
||||
writer: *std.Io.Writer,
|
||||
) error{ InvalidValue, NoValue, WriteFailed }!void {
|
||||
var formatter = selectionFormatter(t, opts) catch |err| return err;
|
||||
try formatter.format(writer);
|
||||
}
|
||||
|
||||
fn selectionFormatter(
|
||||
t: *terminal_c.ZigTerminal,
|
||||
opts: FormatOptions,
|
||||
) error{ InvalidValue, NoValue }!formatterpkg.TerminalFormatter {
|
||||
if (opts.size < @sizeOf(FormatOptions)) return error.InvalidValue;
|
||||
_ = std.meta.intToEnum(Format, @intFromEnum(opts.emit)) catch
|
||||
return error.InvalidValue;
|
||||
|
||||
const sel = if (opts.selection) |sel|
|
||||
sel.toZig() orelse return error.InvalidValue
|
||||
else
|
||||
t.screens.active.selection orelse return error.NoValue;
|
||||
|
||||
var formatter: formatterpkg.TerminalFormatter = .init(t, .{
|
||||
.emit = opts.emit,
|
||||
.unwrap = opts.unwrap,
|
||||
.trim = opts.trim,
|
||||
});
|
||||
formatter.content = .{ .selection = sel };
|
||||
return formatter;
|
||||
}
|
||||
|
||||
/// Return the borrowed C array of `uint32_t` codepoints as a `[]const u21`.
|
||||
///
|
||||
/// `NULL + len 0` returns null, which callers treat as “use the API default
|
||||
/// set.” A non-null pointer with `len 0` returns an empty slice, meaning “use an
|
||||
/// explicitly empty set.” A non-zero length requires a non-null pointer.
|
||||
///
|
||||
/// This is intentionally zero-copy. In the C ABI, codepoints are `uint32_t`,
|
||||
/// but selection internals use Zig's `u21` to represent valid Unicode scalar
|
||||
/// values. Zig currently stores `u21` in the same size and alignment as `u32`,
|
||||
/// so we assert that layout relationship and reinterpret the borrowed slice.
|
||||
/// If Zig ever changes that representation, these comptime assertions fail
|
||||
/// loudly rather than silently making this cast wrong.
|
||||
fn codepointSlice(
|
||||
ptr: ?[*]const u32,
|
||||
len: usize,
|
||||
) error{InvalidValue}!?[]const u21 {
|
||||
comptime {
|
||||
std.debug.assert(@sizeOf(u21) == @sizeOf(u32));
|
||||
std.debug.assert(@alignOf(u21) == @alignOf(u32));
|
||||
}
|
||||
|
||||
if (len == 0) {
|
||||
const p = ptr orelse return null;
|
||||
_ = p;
|
||||
return &.{};
|
||||
}
|
||||
|
||||
const p = ptr orelse return error.InvalidValue;
|
||||
const cps: [*]const u21 = @ptrCast(p);
|
||||
return cps[0..len];
|
||||
}
|
||||
|
||||
pub fn adjust(
|
||||
terminal: terminal_c.Terminal,
|
||||
selection: ?*CSelection,
|
||||
adjustment: Selection.Adjustment,
|
||||
) callconv(lib.calling_conv) Result {
|
||||
if (comptime std.debug.runtime_safety) {
|
||||
_ = std.meta.intToEnum(Selection.Adjustment, @intFromEnum(adjustment)) catch {
|
||||
log.warn("terminal_selection_adjust invalid adjustment value={d}", .{@intFromEnum(adjustment)});
|
||||
return .invalid_value;
|
||||
};
|
||||
}
|
||||
|
||||
const t = terminal_c.zigTerminal(terminal) orelse return .invalid_value;
|
||||
const sel_ptr = selection orelse return .invalid_value;
|
||||
var sel = sel_ptr.toZig() orelse return .invalid_value;
|
||||
sel.adjust(t.screens.active, adjustment);
|
||||
sel_ptr.* = .fromZig(sel);
|
||||
return .success;
|
||||
}
|
||||
|
||||
pub fn order(
|
||||
terminal: terminal_c.Terminal,
|
||||
selection: ?*const CSelection,
|
||||
out_order: ?*Selection.Order,
|
||||
) callconv(lib.calling_conv) Result {
|
||||
const t = terminal_c.zigTerminal(terminal) orelse return .invalid_value;
|
||||
const sel = (selection orelse return .invalid_value).toZig() orelse
|
||||
return .invalid_value;
|
||||
const out = out_order orelse return .invalid_value;
|
||||
|
||||
out.* = sel.order(t.screens.active);
|
||||
return .success;
|
||||
}
|
||||
|
||||
pub fn ordered(
|
||||
terminal: terminal_c.Terminal,
|
||||
selection: ?*const CSelection,
|
||||
desired: Selection.Order,
|
||||
out_selection: ?*CSelection,
|
||||
) callconv(lib.calling_conv) Result {
|
||||
if (comptime std.debug.runtime_safety) {
|
||||
_ = std.meta.intToEnum(Selection.Order, @intFromEnum(desired)) catch {
|
||||
log.warn("terminal_selection_ordered invalid desired value={d}", .{@intFromEnum(desired)});
|
||||
return .invalid_value;
|
||||
};
|
||||
}
|
||||
|
||||
const t = terminal_c.zigTerminal(terminal) orelse return .invalid_value;
|
||||
const sel = (selection orelse return .invalid_value).toZig() orelse
|
||||
return .invalid_value;
|
||||
const out = out_selection orelse return .invalid_value;
|
||||
|
||||
out.* = .fromZig(sel.ordered(t.screens.active, desired));
|
||||
return .success;
|
||||
}
|
||||
|
||||
pub fn contains(
|
||||
terminal: terminal_c.Terminal,
|
||||
selection: ?*const CSelection,
|
||||
pt: point.Point.C,
|
||||
out_contains: ?*bool,
|
||||
) callconv(lib.calling_conv) Result {
|
||||
const t = terminal_c.zigTerminal(terminal) orelse return .invalid_value;
|
||||
const sel = (selection orelse return .invalid_value).toZig() orelse
|
||||
return .invalid_value;
|
||||
const out = out_contains orelse return .invalid_value;
|
||||
|
||||
const screen = t.screens.active;
|
||||
const pin = screen.pages.pin(.fromC(pt)) orelse return .invalid_value;
|
||||
out.* = sel.contains(screen, pin);
|
||||
return .success;
|
||||
}
|
||||
|
||||
pub fn equal(
|
||||
terminal: terminal_c.Terminal,
|
||||
a: ?*const CSelection,
|
||||
b: ?*const CSelection,
|
||||
out_equal: ?*bool,
|
||||
) callconv(lib.calling_conv) Result {
|
||||
_ = terminal_c.zigTerminal(terminal) orelse return .invalid_value;
|
||||
const sel_a = (a orelse return .invalid_value).toZig() orelse
|
||||
return .invalid_value;
|
||||
const sel_b = (b orelse return .invalid_value).toZig() orelse
|
||||
return .invalid_value;
|
||||
const out = out_equal orelse return .invalid_value;
|
||||
|
||||
out.* = sel_a.eql(sel_b);
|
||||
return .success;
|
||||
}
|
||||
|
||||
test "selection_format_alloc uses active 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);
|
||||
|
||||
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: CSelection = .{
|
||||
.start = start_ref,
|
||||
.end = end_ref,
|
||||
};
|
||||
try testing.expectEqual(Result.success, terminal_c.set(t, .selection, @ptrCast(&sel)));
|
||||
|
||||
const opts: FormatOptions = .{
|
||||
.emit = .plain,
|
||||
.unwrap = true,
|
||||
.trim = true,
|
||||
};
|
||||
|
||||
var required: usize = 0;
|
||||
try testing.expectEqual(Result.out_of_space, format_buf(
|
||||
t,
|
||||
opts,
|
||||
null,
|
||||
0,
|
||||
&required,
|
||||
));
|
||||
try testing.expectEqual(@as(usize, 5), required);
|
||||
|
||||
var out_ptr: ?[*]u8 = null;
|
||||
var out_len: usize = 0;
|
||||
try testing.expectEqual(Result.success, format_alloc(
|
||||
t,
|
||||
&lib.alloc.test_allocator,
|
||||
opts,
|
||||
&out_ptr,
|
||||
&out_len,
|
||||
));
|
||||
const ptr = out_ptr orelse return error.TestExpectedEqual;
|
||||
defer lib.alloc.default(&lib.alloc.test_allocator).free(ptr[0..out_len]);
|
||||
|
||||
try testing.expectEqualStrings("World", ptr[0..out_len]);
|
||||
}
|
||||
|
||||
test "selection_format_buf uses provided 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);
|
||||
|
||||
var start_ref: grid_ref.CGridRef = .{};
|
||||
try testing.expectEqual(Result.success, terminal_c.grid_ref(t, .{
|
||||
.tag = .active,
|
||||
.value = .{ .active = .{ .x = 0, .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 = 4, .y = 0 } },
|
||||
}, &end_ref));
|
||||
|
||||
const sel: CSelection = .{
|
||||
.start = start_ref,
|
||||
.end = end_ref,
|
||||
};
|
||||
const opts: FormatOptions = .{
|
||||
.emit = .plain,
|
||||
.unwrap = true,
|
||||
.trim = true,
|
||||
.selection = &sel,
|
||||
};
|
||||
|
||||
var small: [2]u8 = undefined;
|
||||
var written: usize = 0;
|
||||
try testing.expectEqual(Result.out_of_space, format_buf(
|
||||
t,
|
||||
opts,
|
||||
&small,
|
||||
small.len,
|
||||
&written,
|
||||
));
|
||||
try testing.expectEqual(@as(usize, 5), written);
|
||||
|
||||
var buf: [32]u8 = undefined;
|
||||
try testing.expectEqual(Result.success, format_buf(
|
||||
t,
|
||||
opts,
|
||||
&buf,
|
||||
buf.len,
|
||||
&written,
|
||||
));
|
||||
try testing.expectEqualStrings("Hello", buf[0..written]);
|
||||
}
|
||||
|
||||
test "selection_format_alloc returns no_value without active 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);
|
||||
|
||||
var out_ptr: ?[*]u8 = @ptrFromInt(1);
|
||||
var out_len: usize = 123;
|
||||
try testing.expectEqual(Result.no_value, format_alloc(
|
||||
t,
|
||||
&lib.alloc.test_allocator,
|
||||
.{ .emit = .plain, .unwrap = true, .trim = true },
|
||||
&out_ptr,
|
||||
&out_len,
|
||||
));
|
||||
try testing.expect(out_ptr == null);
|
||||
try testing.expectEqual(@as(usize, 0), out_len);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ const testing = std.testing;
|
||||
const build_options = @import("terminal_options");
|
||||
const lib = @import("../lib.zig");
|
||||
const CAllocator = lib.alloc.Allocator;
|
||||
const ZigTerminal = @import("../Terminal.zig");
|
||||
pub const ZigTerminal = @import("../Terminal.zig");
|
||||
const Stream = @import("../stream_terminal.zig").Stream;
|
||||
const ScreenSet = @import("../ScreenSet.zig");
|
||||
const PageList = @import("../PageList.zig");
|
||||
@@ -20,6 +20,7 @@ const cell_c = @import("cell.zig");
|
||||
const row_c = @import("row.zig");
|
||||
const grid_ref_c = @import("grid_ref.zig");
|
||||
const grid_ref_tracked_c = @import("grid_ref_tracked.zig");
|
||||
const selection_c = @import("selection.zig");
|
||||
const style_c = @import("style.zig");
|
||||
const color = @import("../color.zig");
|
||||
const Result = @import("result.zig").Result;
|
||||
@@ -209,6 +210,10 @@ const Effects = struct {
|
||||
/// C: GhosttyTerminal
|
||||
pub const Terminal = ?*TerminalWrapper;
|
||||
|
||||
pub fn zigTerminal(terminal_: Terminal) ?*ZigTerminal {
|
||||
return (terminal_ orelse return null).terminal;
|
||||
}
|
||||
|
||||
/// C: GhosttyTerminalOptions
|
||||
pub const Options = extern struct {
|
||||
cols: size.CellCountInt,
|
||||
@@ -314,6 +319,7 @@ pub const Option = enum(c_int) {
|
||||
kitty_image_medium_shared_mem = 18,
|
||||
apc_max_bytes = 19,
|
||||
apc_max_bytes_kitty = 20,
|
||||
selection = 21,
|
||||
|
||||
/// Input type expected for setting the option.
|
||||
pub fn InType(comptime self: Option) type {
|
||||
@@ -336,6 +342,7 @@ pub const Option = enum(c_int) {
|
||||
.kitty_image_medium_shared_mem,
|
||||
=> ?*const bool,
|
||||
.apc_max_bytes, .apc_max_bytes_kitty => ?*const usize,
|
||||
.selection => ?*const selection_c.CSelection,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -443,6 +450,14 @@ fn setTyped(
|
||||
wrapper.stream.handler.apc_handler.max_bytes.remove(.kitty);
|
||||
}
|
||||
},
|
||||
.selection => {
|
||||
if (value) |ptr| {
|
||||
const sel = ptr.toZig() orelse return .invalid_value;
|
||||
wrapper.terminal.screens.active.select(sel) catch return .out_of_memory;
|
||||
} else {
|
||||
wrapper.terminal.screens.active.clearSelection();
|
||||
}
|
||||
},
|
||||
}
|
||||
return .success;
|
||||
}
|
||||
@@ -576,6 +591,7 @@ pub const TerminalData = enum(c_int) {
|
||||
kitty_image_medium_temp_file = 28,
|
||||
kitty_image_medium_shared_mem = 29,
|
||||
kitty_graphics = 30,
|
||||
selection = 31,
|
||||
|
||||
/// Output type expected for querying the data of the given kind.
|
||||
pub fn OutType(comptime self: TerminalData) type {
|
||||
@@ -604,6 +620,7 @@ pub const TerminalData = enum(c_int) {
|
||||
.kitty_image_medium_shared_mem,
|
||||
=> bool,
|
||||
.kitty_graphics => KittyGraphics,
|
||||
.selection => selection_c.CSelection,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -713,6 +730,9 @@ fn getTyped(
|
||||
if (comptime !build_options.kitty_graphics) return .no_value;
|
||||
out.* = &t.screens.active.kitty_images;
|
||||
},
|
||||
.selection => out.* = selection_c.CSelection.fromZig(
|
||||
t.screens.active.selection orelse return .no_value,
|
||||
),
|
||||
}
|
||||
|
||||
return .success;
|
||||
@@ -1325,6 +1345,410 @@ test "get invalid" {
|
||||
try testing.expectEqual(Result.invalid_value, get(t, .invalid, null));
|
||||
}
|
||||
|
||||
test "set and get selection" {
|
||||
var t: Terminal = null;
|
||||
try testing.expectEqual(Result.success, new(
|
||||
&lib.alloc.test_allocator,
|
||||
&t,
|
||||
.{
|
||||
.cols = 80,
|
||||
.rows = 24,
|
||||
.max_scrollback = 0,
|
||||
},
|
||||
));
|
||||
defer free(t);
|
||||
|
||||
vt_write(t, "Hello", 5);
|
||||
|
||||
var start_ref: grid_ref_c.CGridRef = .{};
|
||||
try testing.expectEqual(Result.success, grid_ref(t, .{
|
||||
.tag = .active,
|
||||
.value = .{ .active = .{ .x = 0, .y = 0 } },
|
||||
}, &start_ref));
|
||||
|
||||
var end_ref: grid_ref_c.CGridRef = .{};
|
||||
try testing.expectEqual(Result.success, grid_ref(t, .{
|
||||
.tag = .active,
|
||||
.value = .{ .active = .{ .x = 4, .y = 0 } },
|
||||
}, &end_ref));
|
||||
|
||||
var out: selection_c.CSelection = undefined;
|
||||
try testing.expectEqual(Result.no_value, get(t, .selection, @ptrCast(&out)));
|
||||
|
||||
const sel: selection_c.CSelection = .{
|
||||
.start = start_ref,
|
||||
.end = end_ref,
|
||||
.rectangle = true,
|
||||
};
|
||||
try testing.expectEqual(Result.success, set(t, .selection, @ptrCast(&sel)));
|
||||
try testing.expect(t.?.terminal.screens.active.selection.?.tracked());
|
||||
|
||||
try testing.expectEqual(Result.success, get(t, .selection, @ptrCast(&out)));
|
||||
try testing.expect(out.start.toPin().?.eql(start_ref.toPin().?));
|
||||
try testing.expect(out.end.toPin().?.eql(end_ref.toPin().?));
|
||||
try testing.expect(out.rectangle);
|
||||
|
||||
try testing.expectEqual(Result.success, set(t, .selection, null));
|
||||
try testing.expect(t.?.terminal.screens.active.selection == null);
|
||||
try testing.expectEqual(Result.no_value, get(t, .selection, @ptrCast(&out)));
|
||||
}
|
||||
|
||||
test "selection derivation helpers" {
|
||||
var t: Terminal = null;
|
||||
try testing.expectEqual(Result.success, new(
|
||||
&lib.alloc.test_allocator,
|
||||
&t,
|
||||
.{
|
||||
.cols = 80,
|
||||
.rows = 24,
|
||||
.max_scrollback = 0,
|
||||
},
|
||||
));
|
||||
defer free(t);
|
||||
|
||||
vt_write(t, " Hello \r\nWorld", 16);
|
||||
|
||||
var out: selection_c.CSelection = undefined;
|
||||
|
||||
var word_ref: grid_ref_c.CGridRef = .{};
|
||||
try testing.expectEqual(Result.success, grid_ref(t, .{
|
||||
.tag = .active,
|
||||
.value = .{ .active = .{ .x = 3, .y = 0 } },
|
||||
}, &word_ref));
|
||||
|
||||
var empty_ref: grid_ref_c.CGridRef = .{};
|
||||
try testing.expectEqual(Result.success, grid_ref(t, .{
|
||||
.tag = .active,
|
||||
.value = .{ .active = .{ .x = 20, .y = 0 } },
|
||||
}, &empty_ref));
|
||||
|
||||
var line_ref: grid_ref_c.CGridRef = .{};
|
||||
try testing.expectEqual(Result.success, grid_ref(t, .{
|
||||
.tag = .active,
|
||||
.value = .{ .active = .{ .x = 0, .y = 0 } },
|
||||
}, &line_ref));
|
||||
|
||||
var word_opts: selection_c.SelectWordOptions = .{
|
||||
.ref = word_ref,
|
||||
};
|
||||
try testing.expectEqual(Result.success, selection_c.word(t, &word_opts, &out));
|
||||
try testing.expectEqual(@as(u16, 2), out.start.toPin().?.x);
|
||||
try testing.expectEqual(@as(u16, 6), out.end.toPin().?.x);
|
||||
|
||||
word_opts.ref = empty_ref;
|
||||
try testing.expectEqual(Result.no_value, selection_c.word(t, &word_opts, &out));
|
||||
|
||||
var between_start_ref: grid_ref_c.CGridRef = .{};
|
||||
try testing.expectEqual(Result.success, grid_ref(t, .{
|
||||
.tag = .active,
|
||||
.value = .{ .active = .{ .x = 20, .y = 1 } },
|
||||
}, &between_start_ref));
|
||||
|
||||
var between_end_ref: grid_ref_c.CGridRef = .{};
|
||||
try testing.expectEqual(Result.success, grid_ref(t, .{
|
||||
.tag = .active,
|
||||
.value = .{ .active = .{ .x = 0, .y = 1 } },
|
||||
}, &between_end_ref));
|
||||
|
||||
var word_between_opts: selection_c.SelectWordBetweenOptions = .{
|
||||
.start = between_start_ref,
|
||||
.end = between_end_ref,
|
||||
};
|
||||
try testing.expectEqual(Result.success, selection_c.word_between(t, &word_between_opts, &out));
|
||||
try testing.expectEqual(@as(u16, 0), out.start.toPin().?.x);
|
||||
try testing.expectEqual(@as(u16, 1), out.start.toPin().?.y);
|
||||
try testing.expectEqual(@as(u16, 4), out.end.toPin().?.x);
|
||||
try testing.expectEqual(@as(u16, 1), out.end.toPin().?.y);
|
||||
|
||||
var line_opts: selection_c.SelectLineOptions = .{
|
||||
.ref = line_ref,
|
||||
};
|
||||
try testing.expectEqual(Result.success, selection_c.line(t, &line_opts, &out));
|
||||
try testing.expectEqual(@as(u16, 2), out.start.toPin().?.x);
|
||||
try testing.expectEqual(@as(u16, 6), out.end.toPin().?.x);
|
||||
|
||||
try testing.expectEqual(Result.success, selection_c.all(t, &out));
|
||||
try testing.expectEqual(@as(u16, 2), out.start.toPin().?.x);
|
||||
try testing.expectEqual(@as(u16, 0), out.start.toPin().?.y);
|
||||
try testing.expectEqual(@as(u16, 4), out.end.toPin().?.x);
|
||||
try testing.expectEqual(@as(u16, 1), out.end.toPin().?.y);
|
||||
|
||||
try testing.expectEqual(Result.no_value, selection_c.output(t, line_ref, &out));
|
||||
|
||||
line_opts.size = @sizeOf(usize) - 1;
|
||||
try testing.expectEqual(Result.invalid_value, selection_c.line(t, &line_opts, &out));
|
||||
try testing.expectEqual(Result.invalid_value, selection_c.word(t, null, &out));
|
||||
try testing.expectEqual(Result.invalid_value, selection_c.word(t, &word_opts, null));
|
||||
try testing.expectEqual(Result.invalid_value, selection_c.word_between(t, null, &out));
|
||||
try testing.expectEqual(Result.invalid_value, selection_c.word_between(t, &word_between_opts, null));
|
||||
}
|
||||
|
||||
test "selection_adjust mutates snapshot end" {
|
||||
var t: Terminal = null;
|
||||
try testing.expectEqual(Result.success, new(
|
||||
&lib.alloc.test_allocator,
|
||||
&t,
|
||||
.{
|
||||
.cols = 80,
|
||||
.rows = 24,
|
||||
.max_scrollback = 0,
|
||||
},
|
||||
));
|
||||
defer free(t);
|
||||
|
||||
vt_write(t, "Hello", 5);
|
||||
|
||||
var start_ref: grid_ref_c.CGridRef = .{};
|
||||
try testing.expectEqual(Result.success, grid_ref(t, .{
|
||||
.tag = .active,
|
||||
.value = .{ .active = .{ .x = 0, .y = 0 } },
|
||||
}, &start_ref));
|
||||
|
||||
var end_ref: grid_ref_c.CGridRef = .{};
|
||||
try testing.expectEqual(Result.success, grid_ref(t, .{
|
||||
.tag = .active,
|
||||
.value = .{ .active = .{ .x = 1, .y = 0 } },
|
||||
}, &end_ref));
|
||||
|
||||
var sel: selection_c.CSelection = .{
|
||||
.start = start_ref,
|
||||
.end = end_ref,
|
||||
};
|
||||
try testing.expectEqual(Result.success, selection_c.adjust(t, &sel, .right));
|
||||
try testing.expectEqual(@as(u16, 0), sel.start.toPin().?.x);
|
||||
try testing.expectEqual(@as(u16, 2), sel.end.toPin().?.x);
|
||||
|
||||
try testing.expectEqual(Result.success, selection_c.adjust(t, &sel, .left));
|
||||
try testing.expectEqual(@as(u16, 0), sel.start.toPin().?.x);
|
||||
try testing.expectEqual(@as(u16, 1), sel.end.toPin().?.x);
|
||||
|
||||
sel = .{
|
||||
.start = end_ref,
|
||||
.end = start_ref,
|
||||
};
|
||||
try testing.expectEqual(Result.success, selection_c.adjust(t, &sel, .right));
|
||||
try testing.expectEqual(@as(u16, 1), sel.start.toPin().?.x);
|
||||
try testing.expectEqual(@as(u16, 1), sel.end.toPin().?.x);
|
||||
}
|
||||
|
||||
test "selection_order and selection_ordered" {
|
||||
var t: Terminal = null;
|
||||
try testing.expectEqual(Result.success, new(
|
||||
&lib.alloc.test_allocator,
|
||||
&t,
|
||||
.{
|
||||
.cols = 80,
|
||||
.rows = 24,
|
||||
.max_scrollback = 0,
|
||||
},
|
||||
));
|
||||
defer free(t);
|
||||
|
||||
vt_write(t, "Hello\r\nWorld", 12);
|
||||
|
||||
var start_ref: grid_ref_c.CGridRef = .{};
|
||||
try testing.expectEqual(Result.success, grid_ref(t, .{
|
||||
.tag = .active,
|
||||
.value = .{ .active = .{ .x = 3, .y = 0 } },
|
||||
}, &start_ref));
|
||||
|
||||
var end_ref: grid_ref_c.CGridRef = .{};
|
||||
try testing.expectEqual(Result.success, grid_ref(t, .{
|
||||
.tag = .active,
|
||||
.value = .{ .active = .{ .x = 1, .y = 1 } },
|
||||
}, &end_ref));
|
||||
|
||||
const sel: selection_c.CSelection = .{
|
||||
.start = start_ref,
|
||||
.end = end_ref,
|
||||
.rectangle = true,
|
||||
};
|
||||
|
||||
var order: selection_c.Order = undefined;
|
||||
try testing.expectEqual(Result.success, selection_c.order(t, &sel, &order));
|
||||
try testing.expectEqual(selection_c.Order.mirrored_forward, order);
|
||||
|
||||
var out: selection_c.CSelection = undefined;
|
||||
try testing.expectEqual(Result.success, selection_c.ordered(t, &sel, .forward, &out));
|
||||
try testing.expectEqual(@as(u16, 1), out.start.toPin().?.x);
|
||||
try testing.expectEqual(@as(u16, 0), out.start.toPin().?.y);
|
||||
try testing.expectEqual(@as(u16, 3), out.end.toPin().?.x);
|
||||
try testing.expectEqual(@as(u16, 1), out.end.toPin().?.y);
|
||||
try testing.expect(out.rectangle);
|
||||
|
||||
try testing.expectEqual(Result.success, selection_c.ordered(t, &sel, .reverse, &out));
|
||||
try testing.expectEqual(@as(u16, 3), out.start.toPin().?.x);
|
||||
try testing.expectEqual(@as(u16, 1), out.start.toPin().?.y);
|
||||
try testing.expectEqual(@as(u16, 1), out.end.toPin().?.x);
|
||||
try testing.expectEqual(@as(u16, 0), out.end.toPin().?.y);
|
||||
try testing.expect(out.rectangle);
|
||||
}
|
||||
|
||||
test "selection_contains" {
|
||||
var t: Terminal = null;
|
||||
try testing.expectEqual(Result.success, new(
|
||||
&lib.alloc.test_allocator,
|
||||
&t,
|
||||
.{
|
||||
.cols = 80,
|
||||
.rows = 24,
|
||||
.max_scrollback = 0,
|
||||
},
|
||||
));
|
||||
defer free(t);
|
||||
|
||||
vt_write(t, "Hello\r\nWorld", 12);
|
||||
|
||||
var start_ref: grid_ref_c.CGridRef = .{};
|
||||
try testing.expectEqual(Result.success, grid_ref(t, .{
|
||||
.tag = .active,
|
||||
.value = .{ .active = .{ .x = 3, .y = 0 } },
|
||||
}, &start_ref));
|
||||
|
||||
var end_ref: grid_ref_c.CGridRef = .{};
|
||||
try testing.expectEqual(Result.success, grid_ref(t, .{
|
||||
.tag = .active,
|
||||
.value = .{ .active = .{ .x = 1, .y = 1 } },
|
||||
}, &end_ref));
|
||||
|
||||
const linear: selection_c.CSelection = .{
|
||||
.start = start_ref,
|
||||
.end = end_ref,
|
||||
};
|
||||
|
||||
var contains: bool = undefined;
|
||||
try testing.expectEqual(Result.success, selection_c.contains(t, &linear, .{
|
||||
.tag = .active,
|
||||
.value = .{ .active = .{ .x = 4, .y = 0 } },
|
||||
}, &contains));
|
||||
try testing.expect(contains);
|
||||
|
||||
try testing.expectEqual(Result.success, selection_c.contains(t, &linear, .{
|
||||
.tag = .active,
|
||||
.value = .{ .active = .{ .x = 2, .y = 0 } },
|
||||
}, &contains));
|
||||
try testing.expect(!contains);
|
||||
|
||||
const rectangle: selection_c.CSelection = .{
|
||||
.start = start_ref,
|
||||
.end = end_ref,
|
||||
.rectangle = true,
|
||||
};
|
||||
|
||||
try testing.expectEqual(Result.success, selection_c.contains(t, &rectangle, .{
|
||||
.tag = .active,
|
||||
.value = .{ .active = .{ .x = 2, .y = 0 } },
|
||||
}, &contains));
|
||||
try testing.expect(contains);
|
||||
}
|
||||
|
||||
test "selection_equal" {
|
||||
var t: Terminal = null;
|
||||
try testing.expectEqual(Result.success, new(
|
||||
&lib.alloc.test_allocator,
|
||||
&t,
|
||||
.{
|
||||
.cols = 80,
|
||||
.rows = 24,
|
||||
.max_scrollback = 0,
|
||||
},
|
||||
));
|
||||
defer free(t);
|
||||
|
||||
var other_t: Terminal = null;
|
||||
try testing.expectEqual(Result.success, new(
|
||||
&lib.alloc.test_allocator,
|
||||
&other_t,
|
||||
.{
|
||||
.cols = 80,
|
||||
.rows = 24,
|
||||
.max_scrollback = 0,
|
||||
},
|
||||
));
|
||||
defer free(other_t);
|
||||
|
||||
vt_write(t, "Hello", 5);
|
||||
vt_write(other_t, "Hello", 5);
|
||||
|
||||
var start_ref: grid_ref_c.CGridRef = .{};
|
||||
try testing.expectEqual(Result.success, grid_ref(t, .{
|
||||
.tag = .active,
|
||||
.value = .{ .active = .{ .x = 0, .y = 0 } },
|
||||
}, &start_ref));
|
||||
|
||||
var end_ref: grid_ref_c.CGridRef = .{};
|
||||
try testing.expectEqual(Result.success, grid_ref(t, .{
|
||||
.tag = .active,
|
||||
.value = .{ .active = .{ .x = 1, .y = 0 } },
|
||||
}, &end_ref));
|
||||
|
||||
var other_end_ref: grid_ref_c.CGridRef = .{};
|
||||
try testing.expectEqual(Result.success, grid_ref(t, .{
|
||||
.tag = .active,
|
||||
.value = .{ .active = .{ .x = 2, .y = 0 } },
|
||||
}, &other_end_ref));
|
||||
|
||||
var cross_terminal_ref: grid_ref_c.CGridRef = .{};
|
||||
try testing.expectEqual(Result.success, grid_ref(other_t, .{
|
||||
.tag = .active,
|
||||
.value = .{ .active = .{ .x = 1, .y = 0 } },
|
||||
}, &cross_terminal_ref));
|
||||
|
||||
const sel: selection_c.CSelection = .{
|
||||
.start = start_ref,
|
||||
.end = end_ref,
|
||||
};
|
||||
const equal_sel: selection_c.CSelection = .{
|
||||
.start = start_ref,
|
||||
.end = end_ref,
|
||||
};
|
||||
const different_endpoint: selection_c.CSelection = .{
|
||||
.start = start_ref,
|
||||
.end = other_end_ref,
|
||||
};
|
||||
const different_rectangle: selection_c.CSelection = .{
|
||||
.start = start_ref,
|
||||
.end = end_ref,
|
||||
.rectangle = true,
|
||||
};
|
||||
const cross_terminal: selection_c.CSelection = .{
|
||||
.start = start_ref,
|
||||
.end = cross_terminal_ref,
|
||||
};
|
||||
|
||||
var equal: bool = undefined;
|
||||
try testing.expectEqual(Result.success, selection_c.equal(t, &sel, &equal_sel, &equal));
|
||||
try testing.expect(equal);
|
||||
|
||||
try testing.expectEqual(Result.success, selection_c.equal(t, &sel, &different_endpoint, &equal));
|
||||
try testing.expect(!equal);
|
||||
|
||||
try testing.expectEqual(Result.success, selection_c.equal(t, &sel, &different_rectangle, &equal));
|
||||
try testing.expect(!equal);
|
||||
|
||||
try testing.expectEqual(Result.success, selection_c.equal(t, &sel, &cross_terminal, &equal));
|
||||
try testing.expect(!equal);
|
||||
try testing.expectEqual(Result.invalid_value, selection_c.equal(t, &sel, &equal_sel, null));
|
||||
}
|
||||
|
||||
test "selection_order invalid values" {
|
||||
var t: Terminal = null;
|
||||
try testing.expectEqual(Result.success, new(
|
||||
&lib.alloc.test_allocator,
|
||||
&t,
|
||||
.{
|
||||
.cols = 80,
|
||||
.rows = 24,
|
||||
.max_scrollback = 0,
|
||||
},
|
||||
));
|
||||
defer free(t);
|
||||
|
||||
var order: selection_c.Order = undefined;
|
||||
try testing.expectEqual(Result.invalid_value, selection_c.order(null, null, &order));
|
||||
try testing.expectEqual(Result.invalid_value, selection_c.order(t, null, &order));
|
||||
}
|
||||
|
||||
test "grid_ref" {
|
||||
var t: Terminal = null;
|
||||
try testing.expectEqual(Result.success, new(
|
||||
|
||||
@@ -20,30 +20,36 @@ const mouse_encode = @import("mouse_encode.zig");
|
||||
const grid_ref = @import("grid_ref.zig");
|
||||
|
||||
/// All C API structs and their Ghostty C names.
|
||||
pub const structs: std.StaticStringMap(StructInfo) = .initComptime(.{
|
||||
.{ "GhosttyColorRgb", StructInfo.init(color.RGB.C) },
|
||||
.{ "GhosttyDeviceAttributes", StructInfo.init(terminal.DeviceAttributes) },
|
||||
.{ "GhosttyDeviceAttributesPrimary", StructInfo.init(terminal.DeviceAttributes.Primary) },
|
||||
.{ "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) },
|
||||
.{ "GhosttyMouseEncoderSize", StructInfo.init(mouse_encode.Size) },
|
||||
.{ "GhosttyMousePosition", StructInfo.init(mouse_event.Position) },
|
||||
.{ "GhosttyPoint", StructInfo.init(point.Point.C) },
|
||||
.{ "GhosttyPointCoordinate", StructInfo.init(point.Coordinate) },
|
||||
.{ "GhosttyRenderStateColors", StructInfo.init(render.Colors) },
|
||||
.{ "GhosttySizeReportSize", StructInfo.init(size_report.Size) },
|
||||
.{ "GhosttyString", StructInfo.init(lib.String) },
|
||||
.{ "GhosttyStyle", StructInfo.init(style_c.Style) },
|
||||
.{ "GhosttyStyleColor", StructInfo.init(style_c.Color) },
|
||||
.{ "GhosttyTerminalOptions", StructInfo.init(terminal.Options) },
|
||||
.{ "GhosttyTerminalScrollbar", StructInfo.init(terminal.TerminalScrollbar) },
|
||||
.{ "GhosttyTerminalScrollViewport", StructInfo.init(terminal.ScrollViewport) },
|
||||
});
|
||||
pub const structs: std.StaticStringMap(StructInfo) = structs: {
|
||||
@setEvalBranchQuota(10_000);
|
||||
break :structs .initComptime(.{
|
||||
.{ "GhosttyColorRgb", StructInfo.init(color.RGB.C) },
|
||||
.{ "GhosttyDeviceAttributes", StructInfo.init(terminal.DeviceAttributes) },
|
||||
.{ "GhosttyDeviceAttributesPrimary", StructInfo.init(terminal.DeviceAttributes.Primary) },
|
||||
.{ "GhosttyDeviceAttributesSecondary", StructInfo.init(terminal.DeviceAttributes.Secondary) },
|
||||
.{ "GhosttyDeviceAttributesTertiary", StructInfo.init(terminal.DeviceAttributes.Tertiary) },
|
||||
.{ "GhosttyFormatterTerminalOptions", StructInfo.init(formatter.TerminalOptions) },
|
||||
.{ "GhosttySelection", StructInfo.init(selection.CSelection) },
|
||||
.{ "GhosttyTerminalSelectWordOptions", StructInfo.init(selection.SelectWordOptions) },
|
||||
.{ "GhosttyTerminalSelectWordBetweenOptions", StructInfo.init(selection.SelectWordBetweenOptions) },
|
||||
.{ "GhosttyTerminalSelectLineOptions", StructInfo.init(selection.SelectLineOptions) },
|
||||
.{ "GhosttyFormatterTerminalExtra", StructInfo.init(formatter.TerminalOptions.Extra) },
|
||||
.{ "GhosttyFormatterScreenExtra", StructInfo.init(formatter.ScreenOptions.Extra) },
|
||||
.{ "GhosttyGridRef", StructInfo.init(grid_ref.CGridRef) },
|
||||
.{ "GhosttyMouseEncoderSize", StructInfo.init(mouse_encode.Size) },
|
||||
.{ "GhosttyMousePosition", StructInfo.init(mouse_event.Position) },
|
||||
.{ "GhosttyPoint", StructInfo.init(point.Point.C) },
|
||||
.{ "GhosttyPointCoordinate", StructInfo.init(point.Coordinate) },
|
||||
.{ "GhosttyRenderStateColors", StructInfo.init(render.Colors) },
|
||||
.{ "GhosttySizeReportSize", StructInfo.init(size_report.Size) },
|
||||
.{ "GhosttyString", StructInfo.init(lib.String) },
|
||||
.{ "GhosttyStyle", StructInfo.init(style_c.Style) },
|
||||
.{ "GhosttyStyleColor", StructInfo.init(style_c.Color) },
|
||||
.{ "GhosttyTerminalOptions", StructInfo.init(terminal.Options) },
|
||||
.{ "GhosttyTerminalScrollbar", StructInfo.init(terminal.TerminalScrollbar) },
|
||||
.{ "GhosttyTerminalScrollViewport", StructInfo.init(terminal.ScrollViewport) },
|
||||
});
|
||||
};
|
||||
|
||||
/// The comptime-generated JSON string of all structs.
|
||||
pub const json: [:0]const u8 = json: {
|
||||
|
||||
31
src/terminal/selection_codepoints.zig
Normal file
31
src/terminal/selection_codepoints.zig
Normal file
@@ -0,0 +1,31 @@
|
||||
// This file contains various default word boundaries used for
|
||||
// selection logic. We put it in a separate file so that different
|
||||
// subsystems can import it without introducing a number of
|
||||
// dependencies.
|
||||
|
||||
/// Default boundary characters for word selection: ` \t'"│`|:;,()[]{}<>$`
|
||||
pub const default_word_boundaries = [_]u21{
|
||||
0, // null
|
||||
' ', // space
|
||||
'\t', // tab
|
||||
'\'', // single quote
|
||||
'"', // double quote
|
||||
'│', // U+2502 box drawing
|
||||
'`', // backtick
|
||||
'|', // pipe
|
||||
':', // colon
|
||||
';', // semicolon
|
||||
',', // comma
|
||||
'(', // left paren
|
||||
')', // right paren
|
||||
'[', // left bracket
|
||||
']', // right bracket
|
||||
'{', // left brace
|
||||
'}', // right brace
|
||||
'<', // less than
|
||||
'>', // greater than
|
||||
'$', // dollar
|
||||
};
|
||||
|
||||
/// Default whitespace characters trimmed from line selections.
|
||||
pub const default_line_whitespace = [_]u21{ 0, ' ', '\t' };
|
||||
Reference in New Issue
Block a user