mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-07-10 03:09:42 +00:00
lib-vt: add color scheme report encoder
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. Example: GHOSTTY_COLOR_SCHEME_DARK encodes to ESC [ ? 997 ; 1 n, while GHOSTTY_COLOR_SCHEME_LIGHT encodes to ESC [ ? 997 ; 2 n.
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