mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-06 07:38:21 +00:00
libghostty: C API for terminal "effects" for processing output and side effects (#11814)
This adds the terminal effects callback system to the libghostty-vt C
API.
Previously, `ghostty_terminal_vt_write()` silently ignored VT sequences
that produce side effects or require responses (bell, title changes,
device status queries, etc.). With this change, embedders can register
callbacks via `ghostty_terminal_set()` to handle these sequences.
This has already existed in the Zig API.
## Effects
| Option | Callback Type | Trigger |
|--------|--------------|---------|
| `GHOSTTY_TERMINAL_OPT_WRITE_PTY` | `GhosttyTerminalWritePtyFn` | Query
responses written back to the pty |
| `GHOSTTY_TERMINAL_OPT_BELL` | `GhosttyTerminalBellFn` | BEL character
(0x07) |
| `GHOSTTY_TERMINAL_OPT_TITLE_CHANGED` | `GhosttyTerminalTitleChangedFn`
| Title change via OSC 0 / OSC 2 |
| `GHOSTTY_TERMINAL_OPT_ENQUIRY` | `GhosttyTerminalEnquiryFn` | ENQ
character (0x05) |
| `GHOSTTY_TERMINAL_OPT_XTVERSION` | `GhosttyTerminalXtversionFn` |
XTVERSION query (CSI > q) |
| `GHOSTTY_TERMINAL_OPT_SIZE` | `GhosttyTerminalSizeFn` | XTWINOPS size
query (CSI 14/16/18 t) |
| `GHOSTTY_TERMINAL_OPT_COLOR_SCHEME` | `GhosttyTerminalColorSchemeFn` |
Color scheme query (CSI ? 996 n) |
| `GHOSTTY_TERMINAL_OPT_DEVICE_ATTRIBUTES` |
`GhosttyTerminalDeviceAttributesFn` | Device attributes query (CSI c / >
c / = c) |
## Example
```c
#include <stdio.h>
#include <string.h>
#include <ghostty/vt.h>
void on_write_pty(GhosttyTerminal terminal, void* userdata,
const uint8_t* data, size_t len) {
(void)terminal; (void)userdata;
printf(" write_pty (%zu bytes): ", len);
fwrite(data, 1, len, stdout);
printf("\n");
}
void on_bell(GhosttyTerminal terminal, void* userdata) {
(void)terminal;
int* count = (int*)userdata;
(*count)++;
printf(" bell! (count=%d)\n", *count);
}
int main() {
GhosttyTerminal terminal = NULL;
GhosttyTerminalOptions opts = { .cols = 80, .rows = 24, .max_scrollback = 0 };
if (ghostty_terminal_new(NULL, &terminal, opts) != GHOSTTY_SUCCESS)
return 1;
// Attach userdata and callbacks
int bell_count = 0;
void* ud = &bell_count;
ghostty_terminal_set(terminal, GHOSTTY_TERMINAL_OPT_USERDATA, &ud);
GhosttyTerminalWritePtyFn write_fn = on_write_pty;
ghostty_terminal_set(terminal, GHOSTTY_TERMINAL_OPT_WRITE_PTY, &write_fn);
GhosttyTerminalBellFn bell_fn = on_bell;
ghostty_terminal_set(terminal, GHOSTTY_TERMINAL_OPT_BELL, &bell_fn);
// BEL triggers the bell callback
const uint8_t bel = 0x07;
ghostty_terminal_vt_write(terminal, &bel, 1);
// DECRQM triggers write_pty with the mode response
const char* decrqm = "\x1B[?7$p";
ghostty_terminal_vt_write(terminal, (const uint8_t*)decrqm, strlen(decrqm));
ghostty_terminal_free(terminal);
return 0;
}
```
This commit is contained in:
18
example/c-vt-effects/README.md
Normal file
18
example/c-vt-effects/README.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# Example: `ghostty-vt` Terminal Effects
|
||||
|
||||
This contains a simple example of how to register and use terminal
|
||||
effect callbacks (`write_pty`, `bell`, `title_changed`) with the
|
||||
`ghostty-vt` C library.
|
||||
|
||||
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-effects/build.zig
Normal file
42
example/c-vt-effects/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_effects",
|
||||
.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-effects/build.zig.zon
Normal file
24
example/c-vt-effects/build.zig.zon
Normal file
@@ -0,0 +1,24 @@
|
||||
.{
|
||||
.name = .c_vt_effects,
|
||||
.version = "0.0.0",
|
||||
.fingerprint = 0xc02634cd65f5b583,
|
||||
.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",
|
||||
},
|
||||
}
|
||||
100
example/c-vt-effects/src/main.c
Normal file
100
example/c-vt-effects/src/main.c
Normal file
@@ -0,0 +1,100 @@
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <ghostty/vt.h>
|
||||
|
||||
//! [effects-write-pty]
|
||||
void on_write_pty(GhosttyTerminal terminal,
|
||||
void* userdata,
|
||||
const uint8_t* data,
|
||||
size_t len) {
|
||||
(void)terminal;
|
||||
(void)userdata;
|
||||
printf(" write_pty (%zu bytes): ", len);
|
||||
fwrite(data, 1, len, stdout);
|
||||
printf("\n");
|
||||
}
|
||||
//! [effects-write-pty]
|
||||
|
||||
//! [effects-bell]
|
||||
void on_bell(GhosttyTerminal terminal, void* userdata) {
|
||||
(void)terminal;
|
||||
int* count = (int*)userdata;
|
||||
(*count)++;
|
||||
printf(" bell! (count=%d)\n", *count);
|
||||
}
|
||||
//! [effects-bell]
|
||||
|
||||
//! [effects-title-changed]
|
||||
void on_title_changed(GhosttyTerminal terminal, void* userdata) {
|
||||
(void)userdata;
|
||||
// Query the cursor position to confirm the terminal processed the
|
||||
// title change (the title itself is tracked by the embedder via the
|
||||
// OSC parser or its own state).
|
||||
uint16_t col = 0;
|
||||
ghostty_terminal_get(terminal, GHOSTTY_TERMINAL_DATA_CURSOR_X, &col);
|
||||
printf(" title changed (cursor at col %u)\n", col);
|
||||
}
|
||||
//! [effects-title-changed]
|
||||
|
||||
//! [effects-register]
|
||||
int main() {
|
||||
// Create a terminal
|
||||
GhosttyTerminal terminal = NULL;
|
||||
GhosttyTerminalOptions opts = {
|
||||
.cols = 80,
|
||||
.rows = 24,
|
||||
.max_scrollback = 0,
|
||||
};
|
||||
if (ghostty_terminal_new(NULL, &terminal, opts) != GHOSTTY_SUCCESS) {
|
||||
fprintf(stderr, "Failed to create terminal\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Set up userdata — a simple bell counter
|
||||
int bell_count = 0;
|
||||
void* ud = &bell_count;
|
||||
ghostty_terminal_set(terminal, GHOSTTY_TERMINAL_OPT_USERDATA, &ud);
|
||||
|
||||
// Register effect callbacks
|
||||
GhosttyTerminalWritePtyFn write_fn = on_write_pty;
|
||||
ghostty_terminal_set(terminal, GHOSTTY_TERMINAL_OPT_WRITE_PTY, &write_fn);
|
||||
|
||||
GhosttyTerminalBellFn bell_fn = on_bell;
|
||||
ghostty_terminal_set(terminal, GHOSTTY_TERMINAL_OPT_BELL, &bell_fn);
|
||||
|
||||
GhosttyTerminalTitleChangedFn title_fn = on_title_changed;
|
||||
ghostty_terminal_set(terminal, GHOSTTY_TERMINAL_OPT_TITLE_CHANGED, &title_fn);
|
||||
|
||||
// Feed VT data that triggers effects:
|
||||
|
||||
// 1. Bell (BEL = 0x07)
|
||||
printf("Sending BEL:\n");
|
||||
const uint8_t bel = 0x07;
|
||||
ghostty_terminal_vt_write(terminal, &bel, 1);
|
||||
|
||||
// 2. Title change (OSC 2 ; <title> ST)
|
||||
printf("Sending title change:\n");
|
||||
const char* title_seq = "\x1B]2;Hello Effects\x1B\\";
|
||||
ghostty_terminal_vt_write(terminal, (const uint8_t*)title_seq,
|
||||
strlen(title_seq));
|
||||
|
||||
// 3. Device status report (DECRQM for wraparound mode ?7)
|
||||
// triggers write_pty with the response
|
||||
printf("Sending DECRQM query:\n");
|
||||
const char* decrqm = "\x1B[?7$p";
|
||||
ghostty_terminal_vt_write(terminal, (const uint8_t*)decrqm,
|
||||
strlen(decrqm));
|
||||
|
||||
// 4. Another bell to show the counter increments
|
||||
printf("Sending another BEL:\n");
|
||||
ghostty_terminal_vt_write(terminal, &bel, 1);
|
||||
|
||||
printf("Total bells: %d\n", bell_count);
|
||||
|
||||
ghostty_terminal_free(terminal);
|
||||
return 0;
|
||||
}
|
||||
//! [effects-register]
|
||||
@@ -109,6 +109,7 @@ extern "C" {
|
||||
#include <ghostty/vt/allocator.h>
|
||||
#include <ghostty/vt/build_info.h>
|
||||
#include <ghostty/vt/color.h>
|
||||
#include <ghostty/vt/device.h>
|
||||
#include <ghostty/vt/focus.h>
|
||||
#include <ghostty/vt/formatter.h>
|
||||
#include <ghostty/vt/render.h>
|
||||
|
||||
150
include/ghostty/vt/device.h
Normal file
150
include/ghostty/vt/device.h
Normal file
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* @file device.h
|
||||
*
|
||||
* Device types used by the terminal for device status and device attribute
|
||||
* queries.
|
||||
*/
|
||||
|
||||
#ifndef GHOSTTY_VT_DEVICE_H
|
||||
#define GHOSTTY_VT_DEVICE_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/* DA1 conformance levels (Pp parameter). */
|
||||
#define GHOSTTY_DA_CONFORMANCE_VT100 1
|
||||
#define GHOSTTY_DA_CONFORMANCE_VT101 1
|
||||
#define GHOSTTY_DA_CONFORMANCE_VT102 6
|
||||
#define GHOSTTY_DA_CONFORMANCE_VT125 12
|
||||
#define GHOSTTY_DA_CONFORMANCE_VT131 7
|
||||
#define GHOSTTY_DA_CONFORMANCE_VT132 4
|
||||
#define GHOSTTY_DA_CONFORMANCE_VT220 62
|
||||
#define GHOSTTY_DA_CONFORMANCE_VT240 62
|
||||
#define GHOSTTY_DA_CONFORMANCE_VT320 63
|
||||
#define GHOSTTY_DA_CONFORMANCE_VT340 63
|
||||
#define GHOSTTY_DA_CONFORMANCE_VT420 64
|
||||
#define GHOSTTY_DA_CONFORMANCE_VT510 65
|
||||
#define GHOSTTY_DA_CONFORMANCE_VT520 65
|
||||
#define GHOSTTY_DA_CONFORMANCE_VT525 65
|
||||
#define GHOSTTY_DA_CONFORMANCE_LEVEL_2 62
|
||||
#define GHOSTTY_DA_CONFORMANCE_LEVEL_3 63
|
||||
#define GHOSTTY_DA_CONFORMANCE_LEVEL_4 64
|
||||
#define GHOSTTY_DA_CONFORMANCE_LEVEL_5 65
|
||||
|
||||
/* DA1 feature codes (Ps parameters). */
|
||||
#define GHOSTTY_DA_FEATURE_COLUMNS_132 1
|
||||
#define GHOSTTY_DA_FEATURE_PRINTER 2
|
||||
#define GHOSTTY_DA_FEATURE_REGIS 3
|
||||
#define GHOSTTY_DA_FEATURE_SIXEL 4
|
||||
#define GHOSTTY_DA_FEATURE_SELECTIVE_ERASE 6
|
||||
#define GHOSTTY_DA_FEATURE_USER_DEFINED_KEYS 8
|
||||
#define GHOSTTY_DA_FEATURE_NATIONAL_REPLACEMENT 9
|
||||
#define GHOSTTY_DA_FEATURE_TECHNICAL_CHARACTERS 15
|
||||
#define GHOSTTY_DA_FEATURE_LOCATOR 16
|
||||
#define GHOSTTY_DA_FEATURE_TERMINAL_STATE 17
|
||||
#define GHOSTTY_DA_FEATURE_WINDOWING 18
|
||||
#define GHOSTTY_DA_FEATURE_HORIZONTAL_SCROLLING 21
|
||||
#define GHOSTTY_DA_FEATURE_ANSI_COLOR 22
|
||||
#define GHOSTTY_DA_FEATURE_RECTANGULAR_EDITING 28
|
||||
#define GHOSTTY_DA_FEATURE_ANSI_TEXT_LOCATOR 29
|
||||
#define GHOSTTY_DA_FEATURE_CLIPBOARD 52
|
||||
|
||||
/* DA2 device type identifiers (Pp parameter). */
|
||||
#define GHOSTTY_DA_DEVICE_TYPE_VT100 0
|
||||
#define GHOSTTY_DA_DEVICE_TYPE_VT220 1
|
||||
#define GHOSTTY_DA_DEVICE_TYPE_VT240 2
|
||||
#define GHOSTTY_DA_DEVICE_TYPE_VT330 18
|
||||
#define GHOSTTY_DA_DEVICE_TYPE_VT340 19
|
||||
#define GHOSTTY_DA_DEVICE_TYPE_VT320 24
|
||||
#define GHOSTTY_DA_DEVICE_TYPE_VT382 32
|
||||
#define GHOSTTY_DA_DEVICE_TYPE_VT420 41
|
||||
#define GHOSTTY_DA_DEVICE_TYPE_VT510 61
|
||||
#define GHOSTTY_DA_DEVICE_TYPE_VT520 64
|
||||
#define GHOSTTY_DA_DEVICE_TYPE_VT525 65
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Color scheme reported in response to a CSI ? 996 n query.
|
||||
*
|
||||
* @ingroup terminal
|
||||
*/
|
||||
typedef enum {
|
||||
GHOSTTY_COLOR_SCHEME_LIGHT = 0,
|
||||
GHOSTTY_COLOR_SCHEME_DARK = 1,
|
||||
} GhosttyColorScheme;
|
||||
|
||||
/**
|
||||
* Primary device attributes (DA1) response data.
|
||||
*
|
||||
* Returned as part of GhosttyDeviceAttributes in response to a CSI c query.
|
||||
* The conformance_level is the Pp parameter and features contains the Ps
|
||||
* feature codes.
|
||||
*
|
||||
* @ingroup terminal
|
||||
*/
|
||||
typedef struct {
|
||||
/** Conformance level (Pp parameter). E.g. 62 for VT220. */
|
||||
uint16_t conformance_level;
|
||||
|
||||
/** DA1 feature codes. Only the first num_features entries are valid. */
|
||||
uint16_t features[64];
|
||||
|
||||
/** Number of valid entries in the features array. */
|
||||
size_t num_features;
|
||||
} GhosttyDeviceAttributesPrimary;
|
||||
|
||||
/**
|
||||
* Secondary device attributes (DA2) response data.
|
||||
*
|
||||
* Returned as part of GhosttyDeviceAttributes in response to a CSI > c query.
|
||||
* Response format: CSI > Pp ; Pv ; Pc c
|
||||
*
|
||||
* @ingroup terminal
|
||||
*/
|
||||
typedef struct {
|
||||
/** Terminal type identifier (Pp). E.g. 1 for VT220. */
|
||||
uint16_t device_type;
|
||||
|
||||
/** Firmware/patch version number (Pv). */
|
||||
uint16_t firmware_version;
|
||||
|
||||
/** ROM cartridge registration number (Pc). Always 0 for emulators. */
|
||||
uint16_t rom_cartridge;
|
||||
} GhosttyDeviceAttributesSecondary;
|
||||
|
||||
/**
|
||||
* Tertiary device attributes (DA3) response data.
|
||||
*
|
||||
* Returned as part of GhosttyDeviceAttributes in response to a CSI = c query.
|
||||
* Response format: DCS ! | D...D ST (DECRPTUI).
|
||||
*
|
||||
* @ingroup terminal
|
||||
*/
|
||||
typedef struct {
|
||||
/** Unit ID encoded as 8 uppercase hex digits in the response. */
|
||||
uint32_t unit_id;
|
||||
} GhosttyDeviceAttributesTertiary;
|
||||
|
||||
/**
|
||||
* Device attributes response data for all three DA levels.
|
||||
*
|
||||
* Filled by the device_attributes callback in response to CSI c,
|
||||
* CSI > c, or CSI = c queries. The terminal uses whichever sub-struct
|
||||
* matches the request type.
|
||||
*
|
||||
* @ingroup terminal
|
||||
*/
|
||||
typedef struct {
|
||||
GhosttyDeviceAttributesPrimary primary;
|
||||
GhosttyDeviceAttributesSecondary secondary;
|
||||
GhosttyDeviceAttributesTertiary tertiary;
|
||||
} GhosttyDeviceAttributes;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* GHOSTTY_VT_DEVICE_H */
|
||||
@@ -12,7 +12,9 @@
|
||||
#include <stdint.h>
|
||||
#include <ghostty/vt/types.h>
|
||||
#include <ghostty/vt/allocator.h>
|
||||
#include <ghostty/vt/device.h>
|
||||
#include <ghostty/vt/modes.h>
|
||||
#include <ghostty/vt/size_report.h>
|
||||
#include <ghostty/vt/grid_ref.h>
|
||||
#include <ghostty/vt/screen.h>
|
||||
#include <ghostty/vt/point.h>
|
||||
@@ -32,6 +34,60 @@ extern "C" {
|
||||
* Once a terminal session is up and running, you can configure a key encoder
|
||||
* to write keyboard input via ghostty_key_encoder_setopt_from_terminal().
|
||||
*
|
||||
* ## Effects
|
||||
*
|
||||
* By default, the terminal sequence processing with ghostty_terminal_vt_write()
|
||||
* only process sequences that directly affect terminal state and
|
||||
* ignores sequences that have side effect behavior or require responses.
|
||||
* These sequences include things like bell characters, title changes, device
|
||||
* attributes queries, and more. To handle these sequences, the embedder
|
||||
* must configure "effects."
|
||||
*
|
||||
* Effects are callbacks that the terminal invokes in response to VT
|
||||
* sequences processed during ghostty_terminal_vt_write(). They let the
|
||||
* embedding application react to terminal-initiated events such as bell
|
||||
* characters, title changes, device status report responses, and more.
|
||||
*
|
||||
* Each effect is registered with ghostty_terminal_set() using the
|
||||
* corresponding `GhosttyTerminalOption` identifier. A `NULL` value
|
||||
* pointer clears the callback and disables the effect.
|
||||
*
|
||||
* A userdata pointer can be attached via `GHOSTTY_TERMINAL_OPT_USERDATA`
|
||||
* and is passed to every callback, allowing callers to route events
|
||||
* back to their own application state without global variables.
|
||||
* You cannot specify different userdata for different callbacks.
|
||||
*
|
||||
* All callbacks are invoked synchronously during
|
||||
* ghostty_terminal_vt_write(). Callbacks **must not** call
|
||||
* ghostty_terminal_vt_write() on the same terminal (no reentrancy).
|
||||
* And callbacks must be very careful to not block for too long or perform
|
||||
* expensive operations, since they are blocking further IO processing.
|
||||
*
|
||||
* The available effects are:
|
||||
*
|
||||
* | Option | Callback Type | Trigger |
|
||||
* |-----------------------------------------|-----------------------------------|-------------------------------------------|
|
||||
* | `GHOSTTY_TERMINAL_OPT_WRITE_PTY` | `GhosttyTerminalWritePtyFn` | Query responses written back to the pty |
|
||||
* | `GHOSTTY_TERMINAL_OPT_BELL` | `GhosttyTerminalBellFn` | BEL character (0x07) |
|
||||
* | `GHOSTTY_TERMINAL_OPT_TITLE_CHANGED` | `GhosttyTerminalTitleChangedFn` | Title change via OSC 0 / OSC 2 |
|
||||
* | `GHOSTTY_TERMINAL_OPT_ENQUIRY` | `GhosttyTerminalEnquiryFn` | ENQ character (0x05) |
|
||||
* | `GHOSTTY_TERMINAL_OPT_XTVERSION` | `GhosttyTerminalXtversionFn` | XTVERSION query (CSI > q) |
|
||||
* | `GHOSTTY_TERMINAL_OPT_SIZE` | `GhosttyTerminalSizeFn` | XTWINOPS size query (CSI 14/16/18 t) |
|
||||
* | `GHOSTTY_TERMINAL_OPT_COLOR_SCHEME` | `GhosttyTerminalColorSchemeFn` | Color scheme query (CSI ? 996 n) |
|
||||
* | `GHOSTTY_TERMINAL_OPT_DEVICE_ATTRIBUTES`| `GhosttyTerminalDeviceAttributesFn`| Device attributes query (CSI c / > c / = c)|
|
||||
*
|
||||
* ### Defining a write_pty callback
|
||||
* @snippet c-vt-effects/src/main.c effects-write-pty
|
||||
*
|
||||
* ### Defining a bell callback
|
||||
* @snippet c-vt-effects/src/main.c effects-bell
|
||||
*
|
||||
* ### Defining a title_changed callback
|
||||
* @snippet c-vt-effects/src/main.c effects-title-changed
|
||||
*
|
||||
* ### Registering effects and processing VT data
|
||||
* @snippet c-vt-effects/src/main.c effects-register
|
||||
*
|
||||
* @{
|
||||
*/
|
||||
|
||||
@@ -134,6 +190,232 @@ typedef struct {
|
||||
uint64_t len;
|
||||
} GhosttyTerminalScrollbar;
|
||||
|
||||
/**
|
||||
* Callback function type for bell.
|
||||
*
|
||||
* Called when the terminal receives a BEL character (0x07).
|
||||
*
|
||||
* @param terminal The terminal handle
|
||||
* @param userdata The userdata pointer set via GHOSTTY_TERMINAL_OPT_USERDATA
|
||||
*
|
||||
* @ingroup terminal
|
||||
*/
|
||||
typedef void (*GhosttyTerminalBellFn)(GhosttyTerminal terminal,
|
||||
void* userdata);
|
||||
|
||||
/**
|
||||
* Callback function type for color scheme queries (CSI ? 996 n).
|
||||
*
|
||||
* Called when the terminal receives a color scheme device status report
|
||||
* query. Return true and fill *out_scheme with the current color scheme,
|
||||
* or return false to silently ignore the query.
|
||||
*
|
||||
* @param terminal The terminal handle
|
||||
* @param userdata The userdata pointer set via GHOSTTY_TERMINAL_OPT_USERDATA
|
||||
* @param[out] out_scheme Pointer to store the current color scheme
|
||||
* @return true if the color scheme was filled, false to ignore the query
|
||||
*
|
||||
* @ingroup terminal
|
||||
*/
|
||||
typedef bool (*GhosttyTerminalColorSchemeFn)(GhosttyTerminal terminal,
|
||||
void* userdata,
|
||||
GhosttyColorScheme* out_scheme);
|
||||
|
||||
/**
|
||||
* Callback function type for device attributes queries (DA1/DA2/DA3).
|
||||
*
|
||||
* Called when the terminal receives a device attributes query (CSI c,
|
||||
* CSI > c, or CSI = c). Return true and fill *out_attrs with the
|
||||
* response data, or return false to silently ignore the query.
|
||||
*
|
||||
* The terminal uses whichever sub-struct (primary, secondary, tertiary)
|
||||
* matches the request type, but all three should be filled for simplicity.
|
||||
*
|
||||
* @param terminal The terminal handle
|
||||
* @param userdata The userdata pointer set via GHOSTTY_TERMINAL_OPT_USERDATA
|
||||
* @param[out] out_attrs Pointer to store the device attributes response
|
||||
* @return true if attributes were filled, false to ignore the query
|
||||
*
|
||||
* @ingroup terminal
|
||||
*/
|
||||
typedef bool (*GhosttyTerminalDeviceAttributesFn)(GhosttyTerminal terminal,
|
||||
void* userdata,
|
||||
GhosttyDeviceAttributes* out_attrs);
|
||||
|
||||
/**
|
||||
* Callback function type for enquiry (ENQ, 0x05).
|
||||
*
|
||||
* Called when the terminal receives an ENQ character. Return the
|
||||
* response bytes as a GhosttyString. The memory must remain valid
|
||||
* until the callback returns. Return a zero-length string to send
|
||||
* no response.
|
||||
*
|
||||
* @param terminal The terminal handle
|
||||
* @param userdata The userdata pointer set via GHOSTTY_TERMINAL_OPT_USERDATA
|
||||
* @return The response bytes to write back to the pty
|
||||
*
|
||||
* @ingroup terminal
|
||||
*/
|
||||
typedef GhosttyString (*GhosttyTerminalEnquiryFn)(GhosttyTerminal terminal,
|
||||
void* userdata);
|
||||
|
||||
/**
|
||||
* Callback function type for size queries (XTWINOPS).
|
||||
*
|
||||
* Called in response to XTWINOPS size queries (CSI 14/16/18 t).
|
||||
* Return true and fill *out_size with the current terminal geometry,
|
||||
* or return false to silently ignore the query.
|
||||
*
|
||||
* @param terminal The terminal handle
|
||||
* @param userdata The userdata pointer set via GHOSTTY_TERMINAL_OPT_USERDATA
|
||||
* @param[out] out_size Pointer to store the terminal size information
|
||||
* @return true if size was filled, false to ignore the query
|
||||
*
|
||||
* @ingroup terminal
|
||||
*/
|
||||
typedef bool (*GhosttyTerminalSizeFn)(GhosttyTerminal terminal,
|
||||
void* userdata,
|
||||
GhosttySizeReportSize* out_size);
|
||||
|
||||
/**
|
||||
* Callback function type for title_changed.
|
||||
*
|
||||
* Called when the terminal title changes via escape sequences
|
||||
* (e.g. OSC 0 or OSC 2). The new title can be queried from the
|
||||
* terminal after the callback returns.
|
||||
*
|
||||
* @param terminal The terminal handle
|
||||
* @param userdata The userdata pointer set via GHOSTTY_TERMINAL_OPT_USERDATA
|
||||
*
|
||||
* @ingroup terminal
|
||||
*/
|
||||
typedef void (*GhosttyTerminalTitleChangedFn)(GhosttyTerminal terminal,
|
||||
void* userdata);
|
||||
|
||||
/**
|
||||
* Callback function type for write_pty.
|
||||
*
|
||||
* Called when the terminal needs to write data back to the pty, for
|
||||
* example in response to a device status report or mode query. The
|
||||
* data is only valid for the duration of the call; callers must copy
|
||||
* it if it needs to persist.
|
||||
*
|
||||
* @param terminal The terminal handle
|
||||
* @param userdata The userdata pointer set via GHOSTTY_TERMINAL_OPT_USERDATA
|
||||
* @param data Pointer to the response bytes
|
||||
* @param len Length of the response in bytes
|
||||
*
|
||||
* @ingroup terminal
|
||||
*/
|
||||
typedef void (*GhosttyTerminalWritePtyFn)(GhosttyTerminal terminal,
|
||||
void* userdata,
|
||||
const uint8_t* data,
|
||||
size_t len);
|
||||
|
||||
/**
|
||||
* Callback function type for XTVERSION.
|
||||
*
|
||||
* Called when the terminal receives an XTVERSION query (CSI > q).
|
||||
* Return the version string (e.g. "myterm 1.0") as a GhosttyString.
|
||||
* The memory must remain valid until the callback returns. Return a
|
||||
* zero-length string to report the default "libghostty" version.
|
||||
*
|
||||
* @param terminal The terminal handle
|
||||
* @param userdata The userdata pointer set via GHOSTTY_TERMINAL_OPT_USERDATA
|
||||
* @return The version string to report
|
||||
*
|
||||
* @ingroup terminal
|
||||
*/
|
||||
typedef GhosttyString (*GhosttyTerminalXtversionFn)(GhosttyTerminal terminal,
|
||||
void* userdata);
|
||||
|
||||
/**
|
||||
* Terminal option identifiers.
|
||||
*
|
||||
* These values are used with ghostty_terminal_set() to configure
|
||||
* terminal callbacks and associated state.
|
||||
*
|
||||
* @ingroup terminal
|
||||
*/
|
||||
typedef enum {
|
||||
/**
|
||||
* Opaque userdata pointer passed to all callbacks.
|
||||
*
|
||||
* Input type: void**
|
||||
*/
|
||||
GHOSTTY_TERMINAL_OPT_USERDATA = 0,
|
||||
|
||||
/**
|
||||
* Callback invoked when the terminal needs to write data back
|
||||
* to the pty (e.g. in response to a DECRQM query or device
|
||||
* status report). Set to NULL to ignore such sequences.
|
||||
*
|
||||
* Input type: GhosttyTerminalWritePtyFn*
|
||||
*/
|
||||
GHOSTTY_TERMINAL_OPT_WRITE_PTY = 1,
|
||||
|
||||
/**
|
||||
* Callback invoked when the terminal receives a BEL character
|
||||
* (0x07). Set to NULL to ignore bell events.
|
||||
*
|
||||
* Input type: GhosttyTerminalBellFn*
|
||||
*/
|
||||
GHOSTTY_TERMINAL_OPT_BELL = 2,
|
||||
|
||||
/**
|
||||
* Callback invoked when the terminal receives an ENQ character
|
||||
* (0x05). Set to NULL to send no response.
|
||||
*
|
||||
* Input type: GhosttyTerminalEnquiryFn*
|
||||
*/
|
||||
GHOSTTY_TERMINAL_OPT_ENQUIRY = 3,
|
||||
|
||||
/**
|
||||
* Callback invoked when the terminal receives an XTVERSION query
|
||||
* (CSI > q). Set to NULL to report the default "libghostty" string.
|
||||
*
|
||||
* Input type: GhosttyTerminalXtversionFn*
|
||||
*/
|
||||
GHOSTTY_TERMINAL_OPT_XTVERSION = 4,
|
||||
|
||||
/**
|
||||
* Callback invoked when the terminal title changes via escape
|
||||
* sequences (e.g. OSC 0 or OSC 2). Set to NULL to ignore title
|
||||
* change events.
|
||||
*
|
||||
* Input type: GhosttyTerminalTitleChangedFn*
|
||||
*/
|
||||
GHOSTTY_TERMINAL_OPT_TITLE_CHANGED = 5,
|
||||
|
||||
/**
|
||||
* Callback invoked in response to XTWINOPS size queries
|
||||
* (CSI 14/16/18 t). Set to NULL to silently ignore size queries.
|
||||
*
|
||||
* Input type: GhosttyTerminalSizeFn*
|
||||
*/
|
||||
GHOSTTY_TERMINAL_OPT_SIZE = 6,
|
||||
|
||||
/**
|
||||
* Callback invoked in response to a color scheme device status
|
||||
* report query (CSI ? 996 n). Return true and fill the out pointer
|
||||
* to report the current scheme, or return false to silently ignore.
|
||||
* Set to NULL to ignore color scheme queries.
|
||||
*
|
||||
* Input type: GhosttyTerminalColorSchemeFn*
|
||||
*/
|
||||
GHOSTTY_TERMINAL_OPT_COLOR_SCHEME = 7,
|
||||
|
||||
/**
|
||||
* Callback invoked in response to a device attributes query
|
||||
* (CSI c, CSI > c, or CSI = c). Return true and fill the out
|
||||
* pointer with response data, or return false to silently ignore.
|
||||
* Set to NULL to ignore device attributes queries.
|
||||
*
|
||||
* Input type: GhosttyTerminalDeviceAttributesFn*
|
||||
*/
|
||||
GHOSTTY_TERMINAL_OPT_DEVICE_ATTRIBUTES = 8,
|
||||
} GhosttyTerminalOption;
|
||||
|
||||
/**
|
||||
* Terminal data types.
|
||||
*
|
||||
@@ -290,15 +572,36 @@ GhosttyResult ghostty_terminal_resize(GhosttyTerminal terminal,
|
||||
uint16_t cols,
|
||||
uint16_t rows);
|
||||
|
||||
/**
|
||||
* Set an option on the terminal.
|
||||
*
|
||||
* Configures terminal callbacks and associated state such as the
|
||||
* write_pty callback and userdata pointer. A NULL value pointer
|
||||
* clears the option to its default (NULL/disabled).
|
||||
*
|
||||
* Callbacks are invoked synchronously during ghostty_terminal_vt_write().
|
||||
* Callbacks must not call ghostty_terminal_vt_write() on the same
|
||||
* terminal (no reentrancy).
|
||||
*
|
||||
* @param terminal The terminal handle (may be NULL, in which case this is a no-op)
|
||||
* @param option The option to set
|
||||
* @param value Pointer to the value to set (type depends on the option),
|
||||
* or NULL to clear the option
|
||||
*
|
||||
* @ingroup terminal
|
||||
*/
|
||||
void ghostty_terminal_set(GhosttyTerminal terminal,
|
||||
GhosttyTerminalOption option,
|
||||
const void* value);
|
||||
|
||||
/**
|
||||
* Write VT-encoded data to the terminal for processing.
|
||||
*
|
||||
* Feeds raw bytes through the terminal's VT stream parser, updating
|
||||
* terminal state accordingly. Only read-only sequences are processed;
|
||||
* sequences that require output (queries) are ignored.
|
||||
*
|
||||
* In the future, a callback-based API will be added to allow handling
|
||||
* of output or side effect sequences.
|
||||
* terminal state accordingly. By default, sequences that require output
|
||||
* (queries, device status reports) are silently ignored. Use
|
||||
* ghostty_terminal_set() with GHOSTTY_TERMINAL_OPT_WRITE_PTY to install
|
||||
* a callback that receives response data.
|
||||
*
|
||||
* This never fails. Any erroneous input or errors in processing the
|
||||
* input are logged internally but do not cause this function to fail
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
#ifndef GHOSTTY_VT_TYPES_H
|
||||
#define GHOSTTY_VT_TYPES_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/**
|
||||
* Result codes for libghostty-vt operations.
|
||||
*/
|
||||
@@ -21,6 +24,20 @@ typedef enum {
|
||||
GHOSTTY_OUT_OF_SPACE = -3,
|
||||
} GhosttyResult;
|
||||
|
||||
/**
|
||||
* A borrowed byte string (pointer + length).
|
||||
*
|
||||
* The memory is not owned by this struct. The pointer is only valid
|
||||
* for the lifetime documented by the API that produces or consumes it.
|
||||
*/
|
||||
typedef struct {
|
||||
/** Pointer to the string bytes. */
|
||||
const uint8_t* ptr;
|
||||
|
||||
/** Length of the string in bytes. */
|
||||
size_t len;
|
||||
} GhosttyString;
|
||||
|
||||
/**
|
||||
* Initialize a sized struct to zero and set its size field.
|
||||
*
|
||||
|
||||
@@ -206,6 +206,7 @@ comptime {
|
||||
@export(&c.terminal_free, .{ .name = "ghostty_terminal_free" });
|
||||
@export(&c.terminal_reset, .{ .name = "ghostty_terminal_reset" });
|
||||
@export(&c.terminal_resize, .{ .name = "ghostty_terminal_resize" });
|
||||
@export(&c.terminal_set, .{ .name = "ghostty_terminal_set" });
|
||||
@export(&c.terminal_vt_write, .{ .name = "ghostty_terminal_vt_write" });
|
||||
@export(&c.terminal_scroll_viewport, .{ .name = "ghostty_terminal_scroll_viewport" });
|
||||
@export(&c.terminal_mode_get, .{ .name = "ghostty_terminal_mode_get" });
|
||||
|
||||
@@ -132,6 +132,7 @@ pub const terminal_new = terminal.new;
|
||||
pub const terminal_free = terminal.free;
|
||||
pub const terminal_reset = terminal.reset;
|
||||
pub const terminal_resize = terminal.resize;
|
||||
pub const terminal_set = terminal.set;
|
||||
pub const terminal_vt_write = terminal.vt_write;
|
||||
pub const terminal_scroll_viewport = terminal.scroll_viewport;
|
||||
pub const terminal_mode_get = terminal.mode_get;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const std = @import("std");
|
||||
const testing = std.testing;
|
||||
const lib = @import("../../lib/main.zig");
|
||||
const lib_alloc = @import("../../lib/allocator.zig");
|
||||
const CAllocator = lib_alloc.Allocator;
|
||||
const ZigTerminal = @import("../Terminal.zig");
|
||||
@@ -10,12 +11,17 @@ const kitty = @import("../kitty/key.zig");
|
||||
const modes = @import("../modes.zig");
|
||||
const point = @import("../point.zig");
|
||||
const size = @import("../size.zig");
|
||||
const device_attributes = @import("../device_attributes.zig");
|
||||
const device_status = @import("../device_status.zig");
|
||||
const size_report = @import("../size_report.zig");
|
||||
const cell_c = @import("cell.zig");
|
||||
const row_c = @import("row.zig");
|
||||
const grid_ref_c = @import("grid_ref.zig");
|
||||
const style_c = @import("style.zig");
|
||||
const Result = @import("result.zig").Result;
|
||||
|
||||
const Handler = @import("../stream_terminal.zig").Handler;
|
||||
|
||||
const log = std.log.scoped(.terminal_c);
|
||||
|
||||
/// Wrapper around ZigTerminal that tracks additional state for C API usage,
|
||||
@@ -24,6 +30,176 @@ const log = std.log.scoped(.terminal_c);
|
||||
const TerminalWrapper = struct {
|
||||
terminal: *ZigTerminal,
|
||||
stream: Stream,
|
||||
effects: Effects = .{},
|
||||
};
|
||||
|
||||
/// C callback state for terminal effects. Trampolines are always
|
||||
/// installed on the stream handler; they check these fields and
|
||||
/// no-op when the corresponding callback is null.
|
||||
const Effects = struct {
|
||||
userdata: ?*anyopaque = null,
|
||||
write_pty: ?WritePtyFn = null,
|
||||
bell: ?BellFn = null,
|
||||
color_scheme: ?ColorSchemeFn = null,
|
||||
device_attributes_cb: ?DeviceAttributesFn = null,
|
||||
enquiry: ?EnquiryFn = null,
|
||||
xtversion: ?XtversionFn = null,
|
||||
title_changed: ?TitleChangedFn = null,
|
||||
size_cb: ?SizeFn = null,
|
||||
|
||||
/// Scratch buffer for DA1 feature codes. The device attributes
|
||||
/// trampoline converts C feature codes into this buffer and returns
|
||||
/// a slice pointing into it. Storing it here ensures the slice
|
||||
/// remains valid after the trampoline returns, since the caller
|
||||
/// (`reportDeviceAttributes`) reads it before any re-entrant call.
|
||||
da_features_buf: [64]device_attributes.Primary.Feature = undefined,
|
||||
|
||||
/// C function pointer type for the write_pty callback.
|
||||
pub const WritePtyFn = *const fn (Terminal, ?*anyopaque, [*]const u8, usize) callconv(.c) void;
|
||||
|
||||
/// C function pointer type for the bell callback.
|
||||
pub const BellFn = *const fn (Terminal, ?*anyopaque) callconv(.c) void;
|
||||
|
||||
/// C function pointer type for the color_scheme callback.
|
||||
/// Returns true and fills out_scheme if a color scheme is available,
|
||||
/// or returns false to silently ignore the query.
|
||||
pub const ColorSchemeFn = *const fn (Terminal, ?*anyopaque, *device_status.ColorScheme) callconv(.c) bool;
|
||||
|
||||
/// C function pointer type for the enquiry callback.
|
||||
/// Returns the response bytes. The memory must remain valid
|
||||
/// until the callback returns.
|
||||
pub const EnquiryFn = *const fn (Terminal, ?*anyopaque) callconv(.c) lib.String;
|
||||
|
||||
/// C function pointer type for the xtversion callback.
|
||||
/// Returns the version string (e.g. "ghostty 1.2.3"). The memory
|
||||
/// must remain valid until the callback returns. An empty string
|
||||
/// (len=0) causes the default "libghostty" to be reported.
|
||||
pub const XtversionFn = *const fn (Terminal, ?*anyopaque) callconv(.c) lib.String;
|
||||
|
||||
/// C function pointer type for the title_changed callback.
|
||||
pub const TitleChangedFn = *const fn (Terminal, ?*anyopaque) callconv(.c) void;
|
||||
|
||||
/// C function pointer type for the size callback.
|
||||
/// Returns true and fills out_size if size is available,
|
||||
/// or returns false to silently ignore the query.
|
||||
pub const SizeFn = *const fn (Terminal, ?*anyopaque, *size_report.Size) callconv(.c) bool;
|
||||
|
||||
/// C function pointer type for the device_attributes callback.
|
||||
/// Returns true and fills out_attrs if attributes are available,
|
||||
/// or returns false to silently ignore the query.
|
||||
pub const DeviceAttributesFn = *const fn (Terminal, ?*anyopaque, *CDeviceAttributes) callconv(.c) bool;
|
||||
|
||||
/// C-compatible device attributes struct.
|
||||
/// C: GhosttyDeviceAttributes
|
||||
pub const CDeviceAttributes = extern struct {
|
||||
primary: Primary,
|
||||
secondary: Secondary,
|
||||
tertiary: Tertiary,
|
||||
|
||||
pub const Primary = extern struct {
|
||||
conformance_level: u16,
|
||||
features: [64]u16,
|
||||
num_features: usize,
|
||||
};
|
||||
|
||||
pub const Secondary = extern struct {
|
||||
device_type: u16,
|
||||
firmware_version: u16,
|
||||
rom_cartridge: u16,
|
||||
};
|
||||
|
||||
pub const Tertiary = extern struct {
|
||||
unit_id: u32,
|
||||
};
|
||||
};
|
||||
|
||||
fn writePtyTrampoline(handler: *Handler, data: [:0]const u8) void {
|
||||
const stream_ptr: *Stream = @fieldParentPtr("handler", handler);
|
||||
const wrapper: *TerminalWrapper = @fieldParentPtr("stream", stream_ptr);
|
||||
const func = wrapper.effects.write_pty orelse return;
|
||||
func(@ptrCast(wrapper), wrapper.effects.userdata, data.ptr, data.len);
|
||||
}
|
||||
|
||||
fn bellTrampoline(handler: *Handler) void {
|
||||
const stream_ptr: *Stream = @fieldParentPtr("handler", handler);
|
||||
const wrapper: *TerminalWrapper = @fieldParentPtr("stream", stream_ptr);
|
||||
const func = wrapper.effects.bell orelse return;
|
||||
func(@ptrCast(wrapper), wrapper.effects.userdata);
|
||||
}
|
||||
|
||||
fn colorSchemeTrampoline(handler: *Handler) ?device_status.ColorScheme {
|
||||
const stream_ptr: *Stream = @fieldParentPtr("handler", handler);
|
||||
const wrapper: *TerminalWrapper = @fieldParentPtr("stream", stream_ptr);
|
||||
const func = wrapper.effects.color_scheme orelse return null;
|
||||
var scheme: device_status.ColorScheme = undefined;
|
||||
if (func(@ptrCast(wrapper), wrapper.effects.userdata, &scheme)) return scheme;
|
||||
return null;
|
||||
}
|
||||
|
||||
fn deviceAttributesTrampoline(handler: *Handler) device_attributes.Attributes {
|
||||
const stream_ptr: *Stream = @fieldParentPtr("handler", handler);
|
||||
const wrapper: *TerminalWrapper = @fieldParentPtr("stream", stream_ptr);
|
||||
const func = wrapper.effects.device_attributes_cb orelse return .{};
|
||||
|
||||
// Get our attributes from the callback.
|
||||
var c_attrs: CDeviceAttributes = undefined;
|
||||
if (!func(@ptrCast(wrapper), wrapper.effects.userdata, &c_attrs)) return .{};
|
||||
|
||||
// Note below we use a lot of enumFromInt but its always safe
|
||||
// because all our types are non-exhaustive enums.
|
||||
|
||||
const n: usize = @min(c_attrs.primary.num_features, 64);
|
||||
for (0..n) |i| wrapper.effects.da_features_buf[i] = @enumFromInt(c_attrs.primary.features[i]);
|
||||
|
||||
return .{
|
||||
.primary = .{
|
||||
.conformance_level = @enumFromInt(c_attrs.primary.conformance_level),
|
||||
.features = wrapper.effects.da_features_buf[0..n],
|
||||
},
|
||||
.secondary = .{
|
||||
.device_type = @enumFromInt(c_attrs.secondary.device_type),
|
||||
.firmware_version = c_attrs.secondary.firmware_version,
|
||||
.rom_cartridge = c_attrs.secondary.rom_cartridge,
|
||||
},
|
||||
.tertiary = .{
|
||||
.unit_id = c_attrs.tertiary.unit_id,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn enquiryTrampoline(handler: *Handler) []const u8 {
|
||||
const stream_ptr: *Stream = @fieldParentPtr("handler", handler);
|
||||
const wrapper: *TerminalWrapper = @fieldParentPtr("stream", stream_ptr);
|
||||
const func = wrapper.effects.enquiry orelse return "";
|
||||
const result = func(@ptrCast(wrapper), wrapper.effects.userdata);
|
||||
if (result.len == 0) return "";
|
||||
return result.ptr[0..result.len];
|
||||
}
|
||||
|
||||
fn xtversionTrampoline(handler: *Handler) []const u8 {
|
||||
const stream_ptr: *Stream = @fieldParentPtr("handler", handler);
|
||||
const wrapper: *TerminalWrapper = @fieldParentPtr("stream", stream_ptr);
|
||||
const func = wrapper.effects.xtversion orelse return "";
|
||||
const result = func(@ptrCast(wrapper), wrapper.effects.userdata);
|
||||
if (result.len == 0) return "";
|
||||
return result.ptr[0..result.len];
|
||||
}
|
||||
|
||||
fn titleChangedTrampoline(handler: *Handler) void {
|
||||
const stream_ptr: *Stream = @fieldParentPtr("handler", handler);
|
||||
const wrapper: *TerminalWrapper = @fieldParentPtr("stream", stream_ptr);
|
||||
const func = wrapper.effects.title_changed orelse return;
|
||||
func(@ptrCast(wrapper), wrapper.effects.userdata);
|
||||
}
|
||||
|
||||
fn sizeTrampoline(handler: *Handler) ?size_report.Size {
|
||||
const stream_ptr: *Stream = @fieldParentPtr("handler", handler);
|
||||
const wrapper: *TerminalWrapper = @fieldParentPtr("stream", stream_ptr);
|
||||
const func = wrapper.effects.size_cb orelse return null;
|
||||
var s: size_report.Size = undefined;
|
||||
if (func(@ptrCast(wrapper), wrapper.effects.userdata, &s)) return s;
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/// C: GhosttyTerminal
|
||||
@@ -80,8 +256,19 @@ fn new_(
|
||||
});
|
||||
errdefer t.deinit(alloc);
|
||||
|
||||
// Setup our stream
|
||||
const handler: Stream.Handler = t.vtHandler();
|
||||
// Setup our stream with trampolines always installed so that
|
||||
// setting C callbacks at any time takes effect immediately.
|
||||
var handler: Stream.Handler = t.vtHandler();
|
||||
handler.effects = .{
|
||||
.write_pty = &Effects.writePtyTrampoline,
|
||||
.bell = &Effects.bellTrampoline,
|
||||
.color_scheme = &Effects.colorSchemeTrampoline,
|
||||
.device_attributes = &Effects.deviceAttributesTrampoline,
|
||||
.enquiry = &Effects.enquiryTrampoline,
|
||||
.xtversion = &Effects.xtversionTrampoline,
|
||||
.title_changed = &Effects.titleChangedTrampoline,
|
||||
.size = &Effects.sizeTrampoline,
|
||||
};
|
||||
|
||||
wrapper.* = .{
|
||||
.terminal = t,
|
||||
@@ -100,6 +287,74 @@ pub fn vt_write(
|
||||
wrapper.stream.nextSlice(ptr[0..len]);
|
||||
}
|
||||
|
||||
/// C: GhosttyTerminalOption
|
||||
pub const Option = enum(c_int) {
|
||||
userdata = 0,
|
||||
write_pty = 1,
|
||||
bell = 2,
|
||||
enquiry = 3,
|
||||
xtversion = 4,
|
||||
title_changed = 5,
|
||||
size_cb = 6,
|
||||
color_scheme = 7,
|
||||
device_attributes = 8,
|
||||
|
||||
/// Input type expected for setting the option.
|
||||
pub fn InType(comptime self: Option) type {
|
||||
return switch (self) {
|
||||
.userdata => ?*anyopaque,
|
||||
.write_pty => ?Effects.WritePtyFn,
|
||||
.bell => ?Effects.BellFn,
|
||||
.color_scheme => ?Effects.ColorSchemeFn,
|
||||
.device_attributes => ?Effects.DeviceAttributesFn,
|
||||
.enquiry => ?Effects.EnquiryFn,
|
||||
.xtversion => ?Effects.XtversionFn,
|
||||
.title_changed => ?Effects.TitleChangedFn,
|
||||
.size_cb => ?Effects.SizeFn,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub fn set(
|
||||
terminal_: Terminal,
|
||||
option: Option,
|
||||
value: ?*const anyopaque,
|
||||
) callconv(.c) void {
|
||||
if (comptime std.debug.runtime_safety) {
|
||||
_ = std.meta.intToEnum(Option, @intFromEnum(option)) catch {
|
||||
log.warn("terminal_set invalid option value={d}", .{@intFromEnum(option)});
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
||||
return switch (option) {
|
||||
inline else => |comptime_option| setTyped(
|
||||
terminal_,
|
||||
comptime_option,
|
||||
@ptrCast(@alignCast(value)),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
fn setTyped(
|
||||
terminal_: Terminal,
|
||||
comptime option: Option,
|
||||
value: ?*const option.InType(),
|
||||
) void {
|
||||
const wrapper = terminal_ orelse return;
|
||||
switch (option) {
|
||||
.userdata => wrapper.effects.userdata = if (value) |v| v.* else null,
|
||||
.write_pty => wrapper.effects.write_pty = if (value) |v| v.* else null,
|
||||
.bell => wrapper.effects.bell = if (value) |v| v.* else null,
|
||||
.color_scheme => wrapper.effects.color_scheme = if (value) |v| v.* else null,
|
||||
.device_attributes => wrapper.effects.device_attributes_cb = if (value) |v| v.* else null,
|
||||
.enquiry => wrapper.effects.enquiry = if (value) |v| v.* else null,
|
||||
.xtversion => wrapper.effects.xtversion = if (value) |v| v.* else null,
|
||||
.title_changed => wrapper.effects.title_changed = if (value) |v| v.* else null,
|
||||
.size_cb => wrapper.effects.size_cb = if (value) |v| v.* else null,
|
||||
}
|
||||
}
|
||||
|
||||
/// C: GhosttyTerminalScrollViewport
|
||||
pub const ScrollViewport = ZigTerminal.ScrollViewport.C;
|
||||
|
||||
@@ -779,6 +1034,672 @@ test "grid_ref null terminal" {
|
||||
}, &out_ref));
|
||||
}
|
||||
|
||||
test "set write_pty callback" {
|
||||
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);
|
||||
|
||||
const S = struct {
|
||||
var last_data: ?[]u8 = null;
|
||||
var last_userdata: ?*anyopaque = null;
|
||||
|
||||
fn deinit() void {
|
||||
if (last_data) |d| testing.allocator.free(d);
|
||||
last_data = null;
|
||||
last_userdata = null;
|
||||
}
|
||||
|
||||
fn writePty(_: Terminal, ud: ?*anyopaque, ptr: [*]const u8, len: usize) callconv(.c) void {
|
||||
if (last_data) |d| testing.allocator.free(d);
|
||||
last_data = testing.allocator.dupe(u8, ptr[0..len]) catch @panic("OOM");
|
||||
last_userdata = ud;
|
||||
}
|
||||
};
|
||||
defer S.deinit();
|
||||
|
||||
// Set userdata and write_pty callback
|
||||
var sentinel: u8 = 42;
|
||||
const ud: ?*anyopaque = @ptrCast(&sentinel);
|
||||
set(t, .userdata, @ptrCast(&ud));
|
||||
const cb: ?Effects.WritePtyFn = &S.writePty;
|
||||
set(t, .write_pty, @ptrCast(&cb));
|
||||
|
||||
// DECRQM for wraparound mode (mode 7, set by default) should trigger write_pty
|
||||
vt_write(t, "\x1B[?7$p", 6);
|
||||
try testing.expect(S.last_data != null);
|
||||
try testing.expectEqualStrings("\x1B[?7;1$y", S.last_data.?);
|
||||
try testing.expectEqual(@as(?*anyopaque, @ptrCast(&sentinel)), S.last_userdata);
|
||||
}
|
||||
|
||||
test "set write_pty without callback ignores queries" {
|
||||
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);
|
||||
|
||||
// Without setting a callback, DECRQM should be silently ignored (no crash)
|
||||
vt_write(t, "\x1B[?7$p", 6);
|
||||
}
|
||||
|
||||
test "set write_pty null clears callback" {
|
||||
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);
|
||||
|
||||
const S = struct {
|
||||
var called: bool = false;
|
||||
fn writePty(_: Terminal, _: ?*anyopaque, _: [*]const u8, _: usize) callconv(.c) void {
|
||||
called = true;
|
||||
}
|
||||
};
|
||||
S.called = false;
|
||||
|
||||
// Set then clear the callback
|
||||
const cb: ?Effects.WritePtyFn = &S.writePty;
|
||||
set(t, .write_pty, @ptrCast(&cb));
|
||||
set(t, .write_pty, null);
|
||||
|
||||
vt_write(t, "\x1B[?7$p", 6);
|
||||
try testing.expect(!S.called);
|
||||
}
|
||||
|
||||
test "set bell callback" {
|
||||
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);
|
||||
|
||||
const S = struct {
|
||||
var bell_count: usize = 0;
|
||||
var last_userdata: ?*anyopaque = null;
|
||||
|
||||
fn bell(_: Terminal, ud: ?*anyopaque) callconv(.c) void {
|
||||
bell_count += 1;
|
||||
last_userdata = ud;
|
||||
}
|
||||
};
|
||||
S.bell_count = 0;
|
||||
S.last_userdata = null;
|
||||
|
||||
// Set userdata and bell callback
|
||||
var sentinel: u8 = 99;
|
||||
const ud: ?*anyopaque = @ptrCast(&sentinel);
|
||||
set(t, .userdata, @ptrCast(&ud));
|
||||
const cb: ?Effects.BellFn = &S.bell;
|
||||
set(t, .bell, @ptrCast(&cb));
|
||||
|
||||
// Single BEL
|
||||
vt_write(t, "\x07", 1);
|
||||
try testing.expectEqual(@as(usize, 1), S.bell_count);
|
||||
try testing.expectEqual(@as(?*anyopaque, @ptrCast(&sentinel)), S.last_userdata);
|
||||
|
||||
// Multiple BELs
|
||||
vt_write(t, "\x07\x07", 2);
|
||||
try testing.expectEqual(@as(usize, 3), S.bell_count);
|
||||
}
|
||||
|
||||
test "bell without callback is silent" {
|
||||
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);
|
||||
|
||||
// BEL without a callback should not crash
|
||||
vt_write(t, "\x07", 1);
|
||||
}
|
||||
|
||||
test "set enquiry callback" {
|
||||
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);
|
||||
|
||||
const S = struct {
|
||||
var last_data: ?[]u8 = null;
|
||||
|
||||
fn deinit() void {
|
||||
if (last_data) |d| testing.allocator.free(d);
|
||||
last_data = null;
|
||||
}
|
||||
|
||||
fn writePty(_: Terminal, _: ?*anyopaque, ptr: [*]const u8, len: usize) callconv(.c) void {
|
||||
if (last_data) |d| testing.allocator.free(d);
|
||||
last_data = testing.allocator.dupe(u8, ptr[0..len]) catch @panic("OOM");
|
||||
}
|
||||
|
||||
const response = "OK";
|
||||
fn enquiry(_: Terminal, _: ?*anyopaque) callconv(.c) lib.String {
|
||||
return .{ .ptr = response, .len = response.len };
|
||||
}
|
||||
};
|
||||
defer S.deinit();
|
||||
|
||||
const write_cb: ?Effects.WritePtyFn = &S.writePty;
|
||||
set(t, .write_pty, @ptrCast(&write_cb));
|
||||
const enq_cb: ?Effects.EnquiryFn = &S.enquiry;
|
||||
set(t, .enquiry, @ptrCast(&enq_cb));
|
||||
|
||||
// ENQ (0x05) should trigger the enquiry callback and write response via write_pty
|
||||
vt_write(t, "\x05", 1);
|
||||
try testing.expect(S.last_data != null);
|
||||
try testing.expectEqualStrings("OK", S.last_data.?);
|
||||
}
|
||||
|
||||
test "enquiry without callback is silent" {
|
||||
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);
|
||||
|
||||
// ENQ without a callback should not crash
|
||||
vt_write(t, "\x05", 1);
|
||||
}
|
||||
|
||||
test "set xtversion callback" {
|
||||
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);
|
||||
|
||||
const S = struct {
|
||||
var last_data: ?[]u8 = null;
|
||||
|
||||
fn deinit() void {
|
||||
if (last_data) |d| testing.allocator.free(d);
|
||||
last_data = null;
|
||||
}
|
||||
|
||||
fn writePty(_: Terminal, _: ?*anyopaque, ptr: [*]const u8, len: usize) callconv(.c) void {
|
||||
if (last_data) |d| testing.allocator.free(d);
|
||||
last_data = testing.allocator.dupe(u8, ptr[0..len]) catch @panic("OOM");
|
||||
}
|
||||
|
||||
const version = "myterm 1.0";
|
||||
fn xtversion(_: Terminal, _: ?*anyopaque) callconv(.c) lib.String {
|
||||
return .{ .ptr = version, .len = version.len };
|
||||
}
|
||||
};
|
||||
defer S.deinit();
|
||||
|
||||
const write_cb: ?Effects.WritePtyFn = &S.writePty;
|
||||
set(t, .write_pty, @ptrCast(&write_cb));
|
||||
const xtv_cb: ?Effects.XtversionFn = &S.xtversion;
|
||||
set(t, .xtversion, @ptrCast(&xtv_cb));
|
||||
|
||||
// XTVERSION: CSI > q
|
||||
vt_write(t, "\x1B[>q", 4);
|
||||
try testing.expect(S.last_data != null);
|
||||
// Response should be DCS >| version ST
|
||||
try testing.expectEqualStrings("\x1BP>|myterm 1.0\x1B\\", S.last_data.?);
|
||||
}
|
||||
|
||||
test "xtversion without callback reports default" {
|
||||
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);
|
||||
|
||||
const S = struct {
|
||||
var last_data: ?[]u8 = null;
|
||||
|
||||
fn deinit() void {
|
||||
if (last_data) |d| testing.allocator.free(d);
|
||||
last_data = null;
|
||||
}
|
||||
|
||||
fn writePty(_: Terminal, _: ?*anyopaque, ptr: [*]const u8, len: usize) callconv(.c) void {
|
||||
if (last_data) |d| testing.allocator.free(d);
|
||||
last_data = testing.allocator.dupe(u8, ptr[0..len]) catch @panic("OOM");
|
||||
}
|
||||
};
|
||||
defer S.deinit();
|
||||
|
||||
// Set write_pty but not xtversion — should get default "libghostty"
|
||||
const write_cb: ?Effects.WritePtyFn = &S.writePty;
|
||||
set(t, .write_pty, @ptrCast(&write_cb));
|
||||
|
||||
vt_write(t, "\x1B[>q", 4);
|
||||
try testing.expect(S.last_data != null);
|
||||
try testing.expectEqualStrings("\x1BP>|libghostty\x1B\\", S.last_data.?);
|
||||
}
|
||||
|
||||
test "set title_changed callback" {
|
||||
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);
|
||||
|
||||
const S = struct {
|
||||
var title_count: usize = 0;
|
||||
var last_userdata: ?*anyopaque = null;
|
||||
|
||||
fn titleChanged(_: Terminal, ud: ?*anyopaque) callconv(.c) void {
|
||||
title_count += 1;
|
||||
last_userdata = ud;
|
||||
}
|
||||
};
|
||||
S.title_count = 0;
|
||||
S.last_userdata = null;
|
||||
|
||||
var sentinel: u8 = 77;
|
||||
const ud: ?*anyopaque = @ptrCast(&sentinel);
|
||||
set(t, .userdata, @ptrCast(&ud));
|
||||
const cb: ?Effects.TitleChangedFn = &S.titleChanged;
|
||||
set(t, .title_changed, @ptrCast(&cb));
|
||||
|
||||
// OSC 2 ; title ST — set window title
|
||||
vt_write(t, "\x1B]2;Hello\x1B\\", 10);
|
||||
try testing.expectEqual(@as(usize, 1), S.title_count);
|
||||
try testing.expectEqual(@as(?*anyopaque, @ptrCast(&sentinel)), S.last_userdata);
|
||||
|
||||
// Another title change
|
||||
vt_write(t, "\x1B]2;World\x1B\\", 10);
|
||||
try testing.expectEqual(@as(usize, 2), S.title_count);
|
||||
}
|
||||
|
||||
test "title_changed without callback is silent" {
|
||||
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);
|
||||
|
||||
// OSC 2 without a callback should not crash
|
||||
vt_write(t, "\x1B]2;Hello\x1B\\", 10);
|
||||
}
|
||||
|
||||
test "set size callback" {
|
||||
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);
|
||||
|
||||
const S = struct {
|
||||
var last_data: ?[]u8 = null;
|
||||
|
||||
fn deinit() void {
|
||||
if (last_data) |d| testing.allocator.free(d);
|
||||
last_data = null;
|
||||
}
|
||||
|
||||
fn writePty(_: Terminal, _: ?*anyopaque, ptr: [*]const u8, len: usize) callconv(.c) void {
|
||||
if (last_data) |d| testing.allocator.free(d);
|
||||
last_data = testing.allocator.dupe(u8, ptr[0..len]) catch @panic("OOM");
|
||||
}
|
||||
|
||||
fn sizeCb(_: Terminal, _: ?*anyopaque, out_size: *size_report.Size) callconv(.c) bool {
|
||||
out_size.* = .{
|
||||
.rows = 24,
|
||||
.columns = 80,
|
||||
.cell_width = 8,
|
||||
.cell_height = 16,
|
||||
};
|
||||
return true;
|
||||
}
|
||||
};
|
||||
defer S.deinit();
|
||||
|
||||
const write_cb: ?Effects.WritePtyFn = &S.writePty;
|
||||
set(t, .write_pty, @ptrCast(&write_cb));
|
||||
const size_cb_fn: ?Effects.SizeFn = &S.sizeCb;
|
||||
set(t, .size_cb, @ptrCast(&size_cb_fn));
|
||||
|
||||
// CSI 18 t — report text area size in characters
|
||||
vt_write(t, "\x1B[18t", 5);
|
||||
try testing.expect(S.last_data != null);
|
||||
try testing.expectEqualStrings("\x1b[8;24;80t", S.last_data.?);
|
||||
}
|
||||
|
||||
test "size without callback is silent" {
|
||||
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);
|
||||
|
||||
// CSI 18 t without a size callback should not crash
|
||||
vt_write(t, "\x1B[18t", 5);
|
||||
}
|
||||
|
||||
test "set device_attributes callback primary" {
|
||||
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);
|
||||
|
||||
const S = struct {
|
||||
var last_data: ?[]u8 = null;
|
||||
|
||||
fn deinit() void {
|
||||
if (last_data) |d| testing.allocator.free(d);
|
||||
last_data = null;
|
||||
}
|
||||
|
||||
fn writePty(_: Terminal, _: ?*anyopaque, ptr: [*]const u8, len: usize) callconv(.c) void {
|
||||
if (last_data) |d| testing.allocator.free(d);
|
||||
last_data = testing.allocator.dupe(u8, ptr[0..len]) catch @panic("OOM");
|
||||
}
|
||||
|
||||
fn da(_: Terminal, _: ?*anyopaque, out: *Effects.CDeviceAttributes) callconv(.c) bool {
|
||||
out.* = .{
|
||||
.primary = .{
|
||||
.conformance_level = 64,
|
||||
.features = .{ 22, 52 } ++ .{0} ** 62,
|
||||
.num_features = 2,
|
||||
},
|
||||
.secondary = .{
|
||||
.device_type = 1,
|
||||
.firmware_version = 10,
|
||||
.rom_cartridge = 0,
|
||||
},
|
||||
.tertiary = .{ .unit_id = 0 },
|
||||
};
|
||||
return true;
|
||||
}
|
||||
};
|
||||
defer S.deinit();
|
||||
|
||||
const write_cb: ?Effects.WritePtyFn = &S.writePty;
|
||||
set(t, .write_pty, @ptrCast(&write_cb));
|
||||
const da_cb: ?Effects.DeviceAttributesFn = &S.da;
|
||||
set(t, .device_attributes, @ptrCast(&da_cb));
|
||||
|
||||
// CSI c — primary DA
|
||||
vt_write(t, "\x1B[c", 3);
|
||||
try testing.expect(S.last_data != null);
|
||||
try testing.expectEqualStrings("\x1b[?64;22;52c", S.last_data.?);
|
||||
}
|
||||
|
||||
test "set device_attributes callback secondary" {
|
||||
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);
|
||||
|
||||
const S = struct {
|
||||
var last_data: ?[]u8 = null;
|
||||
|
||||
fn deinit() void {
|
||||
if (last_data) |d| testing.allocator.free(d);
|
||||
last_data = null;
|
||||
}
|
||||
|
||||
fn writePty(_: Terminal, _: ?*anyopaque, ptr: [*]const u8, len: usize) callconv(.c) void {
|
||||
if (last_data) |d| testing.allocator.free(d);
|
||||
last_data = testing.allocator.dupe(u8, ptr[0..len]) catch @panic("OOM");
|
||||
}
|
||||
|
||||
fn da(_: Terminal, _: ?*anyopaque, out: *Effects.CDeviceAttributes) callconv(.c) bool {
|
||||
out.* = .{
|
||||
.primary = .{
|
||||
.conformance_level = 62,
|
||||
.features = .{22} ++ .{0} ** 63,
|
||||
.num_features = 1,
|
||||
},
|
||||
.secondary = .{
|
||||
.device_type = 1,
|
||||
.firmware_version = 10,
|
||||
.rom_cartridge = 0,
|
||||
},
|
||||
.tertiary = .{ .unit_id = 0 },
|
||||
};
|
||||
return true;
|
||||
}
|
||||
};
|
||||
defer S.deinit();
|
||||
|
||||
const write_cb: ?Effects.WritePtyFn = &S.writePty;
|
||||
set(t, .write_pty, @ptrCast(&write_cb));
|
||||
const da_cb: ?Effects.DeviceAttributesFn = &S.da;
|
||||
set(t, .device_attributes, @ptrCast(&da_cb));
|
||||
|
||||
// CSI > c — secondary DA
|
||||
vt_write(t, "\x1B[>c", 4);
|
||||
try testing.expect(S.last_data != null);
|
||||
try testing.expectEqualStrings("\x1b[>1;10;0c", S.last_data.?);
|
||||
}
|
||||
|
||||
test "set device_attributes callback tertiary" {
|
||||
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);
|
||||
|
||||
const S = struct {
|
||||
var last_data: ?[]u8 = null;
|
||||
|
||||
fn deinit() void {
|
||||
if (last_data) |d| testing.allocator.free(d);
|
||||
last_data = null;
|
||||
}
|
||||
|
||||
fn writePty(_: Terminal, _: ?*anyopaque, ptr: [*]const u8, len: usize) callconv(.c) void {
|
||||
if (last_data) |d| testing.allocator.free(d);
|
||||
last_data = testing.allocator.dupe(u8, ptr[0..len]) catch @panic("OOM");
|
||||
}
|
||||
|
||||
fn da(_: Terminal, _: ?*anyopaque, out: *Effects.CDeviceAttributes) callconv(.c) bool {
|
||||
out.* = .{
|
||||
.primary = .{
|
||||
.conformance_level = 62,
|
||||
.features = .{0} ** 64,
|
||||
.num_features = 0,
|
||||
},
|
||||
.secondary = .{
|
||||
.device_type = 1,
|
||||
.firmware_version = 0,
|
||||
.rom_cartridge = 0,
|
||||
},
|
||||
.tertiary = .{ .unit_id = 0xAABBCCDD },
|
||||
};
|
||||
return true;
|
||||
}
|
||||
};
|
||||
defer S.deinit();
|
||||
|
||||
const write_cb: ?Effects.WritePtyFn = &S.writePty;
|
||||
set(t, .write_pty, @ptrCast(&write_cb));
|
||||
const da_cb: ?Effects.DeviceAttributesFn = &S.da;
|
||||
set(t, .device_attributes, @ptrCast(&da_cb));
|
||||
|
||||
// CSI = c — tertiary DA
|
||||
vt_write(t, "\x1B[=c", 4);
|
||||
try testing.expect(S.last_data != null);
|
||||
try testing.expectEqualStrings("\x1bP!|AABBCCDD\x1b\\", S.last_data.?);
|
||||
}
|
||||
|
||||
test "device_attributes without callback uses default" {
|
||||
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);
|
||||
|
||||
const S = struct {
|
||||
var last_data: ?[]u8 = null;
|
||||
|
||||
fn deinit() void {
|
||||
if (last_data) |d| testing.allocator.free(d);
|
||||
last_data = null;
|
||||
}
|
||||
|
||||
fn writePty(_: Terminal, _: ?*anyopaque, ptr: [*]const u8, len: usize) callconv(.c) void {
|
||||
if (last_data) |d| testing.allocator.free(d);
|
||||
last_data = testing.allocator.dupe(u8, ptr[0..len]) catch @panic("OOM");
|
||||
}
|
||||
};
|
||||
defer S.deinit();
|
||||
|
||||
const write_cb: ?Effects.WritePtyFn = &S.writePty;
|
||||
set(t, .write_pty, @ptrCast(&write_cb));
|
||||
|
||||
// Without setting a device_attributes callback, DA1 should return the default
|
||||
vt_write(t, "\x1B[c", 3);
|
||||
try testing.expect(S.last_data != null);
|
||||
try testing.expectEqualStrings("\x1b[?62;22c", S.last_data.?);
|
||||
}
|
||||
|
||||
test "device_attributes callback returns false uses default" {
|
||||
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);
|
||||
|
||||
const S = struct {
|
||||
var last_data: ?[]u8 = null;
|
||||
|
||||
fn deinit() void {
|
||||
if (last_data) |d| testing.allocator.free(d);
|
||||
last_data = null;
|
||||
}
|
||||
|
||||
fn writePty(_: Terminal, _: ?*anyopaque, ptr: [*]const u8, len: usize) callconv(.c) void {
|
||||
if (last_data) |d| testing.allocator.free(d);
|
||||
last_data = testing.allocator.dupe(u8, ptr[0..len]) catch @panic("OOM");
|
||||
}
|
||||
|
||||
fn da(_: Terminal, _: ?*anyopaque, _: *Effects.CDeviceAttributes) callconv(.c) bool {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
defer S.deinit();
|
||||
|
||||
const write_cb: ?Effects.WritePtyFn = &S.writePty;
|
||||
set(t, .write_pty, @ptrCast(&write_cb));
|
||||
const da_cb: ?Effects.DeviceAttributesFn = &S.da;
|
||||
set(t, .device_attributes, @ptrCast(&da_cb));
|
||||
|
||||
// Callback returns false, should use default response
|
||||
vt_write(t, "\x1B[c", 3);
|
||||
try testing.expect(S.last_data != null);
|
||||
try testing.expectEqualStrings("\x1b[?62;22c", S.last_data.?);
|
||||
}
|
||||
|
||||
test "grid_ref out of bounds" {
|
||||
var t: Terminal = null;
|
||||
try testing.expectEqual(Result.success, new(
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
const std = @import("std");
|
||||
const build_options = @import("terminal_options");
|
||||
const lib = @import("../lib/main.zig");
|
||||
const lib_target: lib.Target = if (build_options.c_abi) .c else .zig;
|
||||
|
||||
/// The color scheme reported in response to a CSI ? 996 n query.
|
||||
pub const ColorScheme = enum {
|
||||
light,
|
||||
dark,
|
||||
};
|
||||
pub const ColorScheme = lib.Enum(lib_target, &.{
|
||||
"light",
|
||||
"dark",
|
||||
});
|
||||
|
||||
/// An enum(u16) of the available device status requests.
|
||||
pub const Request = dsr_enum: {
|
||||
|
||||
Reference in New Issue
Block a user