mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-07-09 15:59:38 +08:00
lib-vt: add color scheme report encoder (#13192)
Add a shared encoder for CSI ? 997 ; Ps n color scheme reports and use it for both CSI ? 996 n replies and unsolicited Termio reports. Export the same encoder through the libghostty-vt C API with docs and an example. This is a really light API, arguably easy for consumers to hardcode, but it didn't match the rest of our style in the libghostty API so we should expose it.
This commit is contained in:
18
example/c-vt-color-scheme/README.md
Normal file
18
example/c-vt-color-scheme/README.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# Example: `ghostty-vt` Color Scheme Report Encoding
|
||||
|
||||
This contains a simple example of how to use the `ghostty-vt` color scheme
|
||||
report encoding API to encode terminal color scheme reports into escape
|
||||
sequences.
|
||||
|
||||
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-color-scheme/build.zig
Normal file
42
example/c-vt-color-scheme/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_color_scheme",
|
||||
.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-color-scheme/build.zig.zon
Normal file
24
example/c-vt-color-scheme/build.zig.zon
Normal file
@@ -0,0 +1,24 @@
|
||||
.{
|
||||
.name = .c_vt_color_scheme,
|
||||
.version = "0.0.0",
|
||||
.fingerprint = 0xb794dffb11875b23,
|
||||
.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",
|
||||
},
|
||||
}
|
||||
20
example/c-vt-color-scheme/src/main.c
Normal file
20
example/c-vt-color-scheme/src/main.c
Normal file
@@ -0,0 +1,20 @@
|
||||
#include <stdio.h>
|
||||
#include <ghostty/vt.h>
|
||||
|
||||
//! [color-scheme-report-encode]
|
||||
int main() {
|
||||
char buf[16];
|
||||
size_t written = 0;
|
||||
|
||||
GhosttyResult result = ghostty_color_scheme_report_encode(
|
||||
GHOSTTY_COLOR_SCHEME_DARK, buf, sizeof(buf), &written);
|
||||
|
||||
if (result == GHOSTTY_SUCCESS) {
|
||||
printf("Encoded %zu bytes: ", written);
|
||||
fwrite(buf, 1, written, stdout);
|
||||
printf("\n");
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
//! [color-scheme-report-encode]
|
||||
@@ -126,6 +126,7 @@ extern "C" {
|
||||
#include <ghostty/vt/allocator.h>
|
||||
#include <ghostty/vt/build_info.h>
|
||||
#include <ghostty/vt/color.h>
|
||||
#include <ghostty/vt/color_scheme.h>
|
||||
#include <ghostty/vt/device.h>
|
||||
#include <ghostty/vt/focus.h>
|
||||
#include <ghostty/vt/formatter.h>
|
||||
|
||||
73
include/ghostty/vt/color_scheme.h
Normal file
73
include/ghostty/vt/color_scheme.h
Normal file
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* @file color_scheme.h
|
||||
*
|
||||
* Color scheme report encoding - encode terminal color scheme reports into
|
||||
* escape sequences.
|
||||
*/
|
||||
|
||||
#ifndef GHOSTTY_VT_COLOR_SCHEME_H
|
||||
#define GHOSTTY_VT_COLOR_SCHEME_H
|
||||
|
||||
/** @defgroup color_scheme Color Scheme Report Encoding
|
||||
*
|
||||
* Utilities for encoding color scheme reports into terminal escape
|
||||
* sequences for color scheme reporting mode (mode 2031).
|
||||
*
|
||||
* ## Basic Usage
|
||||
*
|
||||
* Use ghostty_color_scheme_report_encode() to encode a color scheme report
|
||||
* into a caller-provided buffer. If the buffer is too small, the function
|
||||
* returns GHOSTTY_OUT_OF_SPACE and sets the required size in the output
|
||||
* parameter.
|
||||
*
|
||||
* ## Example
|
||||
*
|
||||
* @snippet c-vt-color-scheme/src/main.c color-scheme-report-encode
|
||||
*
|
||||
* @{
|
||||
*/
|
||||
|
||||
#include <stddef.h>
|
||||
#include <ghostty/vt/types.h>
|
||||
#include <ghostty/vt/device.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Encode a color scheme report into an escape sequence.
|
||||
*
|
||||
* Encodes a color scheme report into the provided buffer. Dark color schemes
|
||||
* emit ESC [ ? 997 ; 1 n, and light color schemes emit ESC [ ? 997 ; 2 n.
|
||||
* The encoded bytes are identical to the terminal's internal CSI ? 996 n
|
||||
* query response.
|
||||
*
|
||||
* Hosts should gate unsolicited sends on GHOSTTY_MODE_COLOR_SCHEME_REPORT
|
||||
* (mode 2031) being set, which can be checked via the mode getters.
|
||||
*
|
||||
* If the buffer is too small, the function returns GHOSTTY_OUT_OF_SPACE
|
||||
* and writes the required buffer size to @p out_written. The caller can
|
||||
* then retry with a sufficiently sized buffer.
|
||||
*
|
||||
* @param scheme The color scheme to encode
|
||||
* @param buf Output buffer to write the encoded sequence into (may be NULL)
|
||||
* @param buf_len Size of the output buffer in bytes
|
||||
* @param[out] out_written On success, the number of bytes written. On
|
||||
* GHOSTTY_OUT_OF_SPACE, the required buffer size.
|
||||
* @return GHOSTTY_SUCCESS on success, GHOSTTY_OUT_OF_SPACE if the buffer
|
||||
* is too small
|
||||
*/
|
||||
GHOSTTY_API GhosttyResult ghostty_color_scheme_report_encode(
|
||||
GhosttyColorScheme scheme,
|
||||
char* buf,
|
||||
size_t buf_len,
|
||||
size_t* out_written);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
/** @} */
|
||||
|
||||
#endif /* GHOSTTY_VT_COLOR_SCHEME_H */
|
||||
@@ -191,6 +191,7 @@ comptime {
|
||||
@export(&c.osc_end, .{ .name = "ghostty_osc_end" });
|
||||
@export(&c.osc_command_type, .{ .name = "ghostty_osc_command_type" });
|
||||
@export(&c.osc_command_data, .{ .name = "ghostty_osc_command_data" });
|
||||
@export(&c.color_scheme_report_encode, .{ .name = "ghostty_color_scheme_report_encode" });
|
||||
@export(&c.focus_encode, .{ .name = "ghostty_focus_encode" });
|
||||
@export(&c.mode_report_encode, .{ .name = "ghostty_mode_report_encode" });
|
||||
@export(&c.paste_is_safe, .{ .name = "ghostty_paste_is_safe" });
|
||||
|
||||
63
src/terminal/c/color_scheme.zig
Normal file
63
src/terminal/c/color_scheme.zig
Normal file
@@ -0,0 +1,63 @@
|
||||
const std = @import("std");
|
||||
const lib = @import("../lib.zig");
|
||||
const device_status = @import("../device_status.zig");
|
||||
const Result = @import("result.zig").Result;
|
||||
|
||||
pub fn report_encode(
|
||||
scheme: device_status.ColorScheme,
|
||||
out_: ?[*]u8,
|
||||
out_len: usize,
|
||||
out_written: *usize,
|
||||
) callconv(lib.calling_conv) Result {
|
||||
var writer: std.Io.Writer = .fixed(if (out_) |out| out[0..out_len] else &.{});
|
||||
device_status.encodeColorSchemeReport(&writer, scheme) catch |err| switch (err) {
|
||||
error.WriteFailed => {
|
||||
var discarding: std.Io.Writer.Discarding = .init(&.{});
|
||||
device_status.encodeColorSchemeReport(&discarding.writer, scheme) catch unreachable;
|
||||
out_written.* = @intCast(discarding.count);
|
||||
return .out_of_space;
|
||||
},
|
||||
};
|
||||
|
||||
out_written.* = writer.end;
|
||||
return .success;
|
||||
}
|
||||
|
||||
test "encode color scheme report dark" {
|
||||
var buf: [device_status.max_color_scheme_report_encode_size]u8 = undefined;
|
||||
var written: usize = 0;
|
||||
const result = report_encode(.dark, &buf, buf.len, &written);
|
||||
try std.testing.expectEqual(.success, result);
|
||||
try std.testing.expectEqualStrings("\x1B[?997;1n", buf[0..written]);
|
||||
}
|
||||
|
||||
test "encode color scheme report light" {
|
||||
var buf: [device_status.max_color_scheme_report_encode_size]u8 = undefined;
|
||||
var written: usize = 0;
|
||||
const result = report_encode(.light, &buf, buf.len, &written);
|
||||
try std.testing.expectEqual(.success, result);
|
||||
try std.testing.expectEqualStrings("\x1B[?997;2n", buf[0..written]);
|
||||
}
|
||||
|
||||
test "encode color scheme report with null buffer" {
|
||||
var written: usize = 0;
|
||||
const result = report_encode(.dark, null, 0, &written);
|
||||
try std.testing.expectEqual(.out_of_space, result);
|
||||
try std.testing.expectEqual(@as(usize, 9), written);
|
||||
}
|
||||
|
||||
test "encode color scheme report with insufficient buffer" {
|
||||
var buf: [3]u8 = undefined;
|
||||
var written: usize = 0;
|
||||
const result = report_encode(.light, &buf, buf.len, &written);
|
||||
try std.testing.expectEqual(.out_of_space, result);
|
||||
try std.testing.expectEqual(@as(usize, 9), written);
|
||||
}
|
||||
|
||||
test "encode color scheme report with exact buffer" {
|
||||
var buf: [9]u8 = undefined;
|
||||
var written: usize = 0;
|
||||
const result = report_encode(.dark, &buf, buf.len, &written);
|
||||
try std.testing.expectEqual(.success, result);
|
||||
try std.testing.expectEqual(@as(usize, 9), written);
|
||||
}
|
||||
@@ -5,6 +5,7 @@ const buildpkg = @import("build_info.zig");
|
||||
pub const allocator = @import("allocator.zig");
|
||||
pub const cell = @import("cell.zig");
|
||||
pub const color = @import("color.zig");
|
||||
pub const color_scheme = @import("color_scheme.zig");
|
||||
pub const focus = @import("focus.zig");
|
||||
pub const formatter = @import("formatter.zig");
|
||||
pub const grid_ref = @import("grid_ref.zig");
|
||||
@@ -58,6 +59,8 @@ pub const osc_command_data = osc.commandData;
|
||||
|
||||
pub const color_rgb_get = color.rgb_get;
|
||||
|
||||
pub const color_scheme_report_encode = color_scheme.report_encode;
|
||||
|
||||
pub const focus_encode = focus.encode;
|
||||
|
||||
pub const mode_report_encode = modes.report_encode;
|
||||
@@ -218,6 +221,7 @@ test {
|
||||
_ = buildpkg;
|
||||
_ = cell;
|
||||
_ = color;
|
||||
_ = color_scheme;
|
||||
_ = grid_ref;
|
||||
_ = grid_ref_tracked;
|
||||
_ = kitty_graphics;
|
||||
|
||||
@@ -7,6 +7,32 @@ pub const ColorScheme = lib.Enum(lib.target, &.{
|
||||
"dark",
|
||||
});
|
||||
|
||||
/// Maximum number of bytes that `encodeColorSchemeReport` will write.
|
||||
pub const max_color_scheme_report_encode_size = max: {
|
||||
var result: usize = 0;
|
||||
for (@typeInfo(ColorScheme).@"enum".fields) |field| {
|
||||
var discarding: std.Io.Writer.Discarding = .init(&.{});
|
||||
encodeColorSchemeReport(
|
||||
&discarding.writer,
|
||||
@enumFromInt(field.value),
|
||||
) catch unreachable;
|
||||
result = @max(result, @as(usize, @intCast(discarding.count)));
|
||||
}
|
||||
|
||||
break :max result;
|
||||
};
|
||||
|
||||
/// Encode a color scheme report response for CSI ? 996 n queries.
|
||||
pub fn encodeColorSchemeReport(
|
||||
writer: *std.Io.Writer,
|
||||
scheme: ColorScheme,
|
||||
) std.Io.Writer.Error!void {
|
||||
try writer.writeAll(switch (scheme) {
|
||||
.dark => "\x1B[?997;1n",
|
||||
.light => "\x1B[?997;2n",
|
||||
});
|
||||
}
|
||||
|
||||
/// An enum(u16) of the available device status requests.
|
||||
pub const Request = dsr_enum: {
|
||||
const EnumField = std.builtin.Type.EnumField;
|
||||
@@ -72,3 +98,19 @@ const entries: []const Entry = &.{
|
||||
.{ .name = "cursor_position", .value = 6 },
|
||||
.{ .name = "color_scheme", .value = 996, .question = true },
|
||||
};
|
||||
|
||||
test "encode color scheme report dark" {
|
||||
try std.testing.expectEqual(@as(usize, 9), max_color_scheme_report_encode_size);
|
||||
|
||||
var buf: [max_color_scheme_report_encode_size]u8 = undefined;
|
||||
var writer: std.Io.Writer = .fixed(&buf);
|
||||
try encodeColorSchemeReport(&writer, .dark);
|
||||
try std.testing.expectEqualStrings("\x1B[?997;1n", writer.buffered());
|
||||
}
|
||||
|
||||
test "encode color scheme report light" {
|
||||
var buf: [max_color_scheme_report_encode_size]u8 = undefined;
|
||||
var writer: std.Io.Writer = .fixed(&buf);
|
||||
try encodeColorSchemeReport(&writer, .light);
|
||||
try std.testing.expectEqualStrings("\x1B[?997;2n", writer.buffered());
|
||||
}
|
||||
|
||||
@@ -348,10 +348,11 @@ pub const Handler = struct {
|
||||
.color_scheme => {
|
||||
const func = self.effects.color_scheme orelse return;
|
||||
const scheme = func(self) orelse return;
|
||||
self.writePty(switch (scheme) {
|
||||
.dark => "\x1B[?997;1n",
|
||||
.light => "\x1B[?997;2n",
|
||||
});
|
||||
var buf: [device_status.max_color_scheme_report_encode_size + 1]u8 = undefined;
|
||||
var writer: std.Io.Writer = .fixed(buf[0..device_status.max_color_scheme_report_encode_size]);
|
||||
device_status.encodeColorSchemeReport(&writer, scheme) catch return;
|
||||
buf[writer.end] = 0;
|
||||
self.writePty(buf[0..writer.end :0]);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -712,11 +712,15 @@ pub fn colorSchemeReportLocked(self: *Termio, td: *ThreadData, force: bool) !voi
|
||||
if (!force and !self.renderer_state.terminal.modes.get(.report_color_scheme)) {
|
||||
return;
|
||||
}
|
||||
const output = switch (self.config.conditional_state.theme) {
|
||||
.light => "\x1B[?997;2n",
|
||||
.dark => "\x1B[?997;1n",
|
||||
const scheme: terminalpkg.device_status.ColorScheme = switch (self.config.conditional_state.theme) {
|
||||
.light => .light,
|
||||
.dark => .dark,
|
||||
};
|
||||
try self.queueWrite(td, output, false);
|
||||
|
||||
var buf: [terminalpkg.device_status.max_color_scheme_report_encode_size]u8 = undefined;
|
||||
var writer: std.Io.Writer = .fixed(&buf);
|
||||
try terminalpkg.device_status.encodeColorSchemeReport(&writer, scheme);
|
||||
try self.queueWrite(td, writer.buffered(), false);
|
||||
}
|
||||
|
||||
/// ThreadData is the data created and stored in the termio thread
|
||||
|
||||
Reference in New Issue
Block a user