mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-20 18:56:08 +03:00

Kitty 0.36.0 added support for a new OSC escape sequence for quering, setting, and resetting the terminal colors. Details can be found [here](https://sw.kovidgoyal.net/kitty/color-stack/#setting-and-querying-colors). This fully parses the OSC 21 escape sequences, but only supports actually querying and changing the foreground color, the background color, and the cursor color because that's what Ghostty currently supports. Adding support for the other settings that Kitty supports changing ranges from easy (cursor text) to difficult (visual bell, second transparent background color).
367 lines
12 KiB
Zig
367 lines
12 KiB
Zig
const std = @import("std");
|
|
const Allocator = std.mem.Allocator;
|
|
const cimgui = @import("cimgui");
|
|
const terminal = @import("../terminal/main.zig");
|
|
const CircBuf = @import("../circ_buf.zig").CircBuf;
|
|
const Surface = @import("../Surface.zig");
|
|
|
|
/// The stream handler for our inspector.
|
|
pub const Stream = terminal.Stream(VTHandler);
|
|
|
|
/// VT event circular buffer.
|
|
pub const VTEventRing = CircBuf(VTEvent, undefined);
|
|
|
|
/// VT event
|
|
pub const VTEvent = struct {
|
|
/// Sequence number, just monotonically increasing.
|
|
seq: usize = 1,
|
|
|
|
/// Kind of event, for filtering
|
|
kind: Kind,
|
|
|
|
/// The formatted string of the event. This is allocated. We format the
|
|
/// event for now because there is so much data to copy if we wanted to
|
|
/// store the raw event.
|
|
str: [:0]const u8,
|
|
|
|
/// Various metadata at the time of the event (before processing).
|
|
cursor: terminal.Screen.Cursor,
|
|
scrolling_region: terminal.Terminal.ScrollingRegion,
|
|
metadata: Metadata.Unmanaged = .{},
|
|
|
|
/// imgui selection state
|
|
imgui_selected: bool = false,
|
|
|
|
const Kind = enum { print, execute, csi, esc, osc, dcs, apc };
|
|
const Metadata = std.StringHashMap([:0]const u8);
|
|
|
|
/// Initiaze the event information for the given parser action.
|
|
pub fn init(
|
|
alloc: Allocator,
|
|
surface: *Surface,
|
|
action: terminal.Parser.Action,
|
|
) !VTEvent {
|
|
var md = Metadata.init(alloc);
|
|
errdefer md.deinit();
|
|
var buf = std.ArrayList(u8).init(alloc);
|
|
defer buf.deinit();
|
|
try encodeAction(alloc, buf.writer(), &md, action);
|
|
const str = try buf.toOwnedSliceSentinel(0);
|
|
errdefer alloc.free(str);
|
|
|
|
const kind: Kind = switch (action) {
|
|
.print => .print,
|
|
.execute => .execute,
|
|
.csi_dispatch => .csi,
|
|
.esc_dispatch => .esc,
|
|
.osc_dispatch => .osc,
|
|
.dcs_hook, .dcs_put, .dcs_unhook => .dcs,
|
|
.apc_start, .apc_put, .apc_end => .apc,
|
|
};
|
|
|
|
const t = surface.renderer_state.terminal;
|
|
|
|
return .{
|
|
.kind = kind,
|
|
.str = str,
|
|
.cursor = t.screen.cursor,
|
|
.scrolling_region = t.scrolling_region,
|
|
.metadata = md.unmanaged,
|
|
};
|
|
}
|
|
|
|
pub fn deinit(self: *VTEvent, alloc: Allocator) void {
|
|
{
|
|
var it = self.metadata.valueIterator();
|
|
while (it.next()) |v| alloc.free(v.*);
|
|
self.metadata.deinit(alloc);
|
|
}
|
|
|
|
alloc.free(self.str);
|
|
}
|
|
|
|
/// Returns true if the event passes the given filter.
|
|
pub fn passFilter(
|
|
self: *const VTEvent,
|
|
filter: *cimgui.c.ImGuiTextFilter,
|
|
) bool {
|
|
// Check our main string
|
|
if (cimgui.c.ImGuiTextFilter_PassFilter(
|
|
filter,
|
|
self.str.ptr,
|
|
null,
|
|
)) return true;
|
|
|
|
// We also check all metadata keys and values
|
|
var it = self.metadata.iterator();
|
|
while (it.next()) |entry| {
|
|
var buf: [256]u8 = undefined;
|
|
const key = std.fmt.bufPrintZ(&buf, "{s}", .{entry.key_ptr.*}) catch continue;
|
|
if (cimgui.c.ImGuiTextFilter_PassFilter(
|
|
filter,
|
|
key.ptr,
|
|
null,
|
|
)) return true;
|
|
if (cimgui.c.ImGuiTextFilter_PassFilter(
|
|
filter,
|
|
entry.value_ptr.ptr,
|
|
null,
|
|
)) return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// Encode a parser action as a string that we show in the logs.
|
|
fn encodeAction(
|
|
alloc: Allocator,
|
|
writer: anytype,
|
|
md: *Metadata,
|
|
action: terminal.Parser.Action,
|
|
) !void {
|
|
switch (action) {
|
|
.print => try encodePrint(writer, action),
|
|
.execute => try encodeExecute(writer, action),
|
|
.csi_dispatch => |v| try encodeCSI(writer, v),
|
|
.esc_dispatch => |v| try encodeEsc(writer, v),
|
|
.osc_dispatch => |v| try encodeOSC(alloc, writer, md, v),
|
|
else => try writer.print("{}", .{action}),
|
|
}
|
|
}
|
|
|
|
fn encodePrint(writer: anytype, action: terminal.Parser.Action) !void {
|
|
const ch = action.print;
|
|
try writer.print("'{u}' (U+{X})", .{ ch, ch });
|
|
}
|
|
|
|
fn encodeExecute(writer: anytype, action: terminal.Parser.Action) !void {
|
|
const ch = action.execute;
|
|
switch (ch) {
|
|
0x00 => try writer.writeAll("NUL"),
|
|
0x01 => try writer.writeAll("SOH"),
|
|
0x02 => try writer.writeAll("STX"),
|
|
0x03 => try writer.writeAll("ETX"),
|
|
0x04 => try writer.writeAll("EOT"),
|
|
0x05 => try writer.writeAll("ENQ"),
|
|
0x06 => try writer.writeAll("ACK"),
|
|
0x07 => try writer.writeAll("BEL"),
|
|
0x08 => try writer.writeAll("BS"),
|
|
0x09 => try writer.writeAll("HT"),
|
|
0x0A => try writer.writeAll("LF"),
|
|
0x0B => try writer.writeAll("VT"),
|
|
0x0C => try writer.writeAll("FF"),
|
|
0x0D => try writer.writeAll("CR"),
|
|
0x0E => try writer.writeAll("SO"),
|
|
0x0F => try writer.writeAll("SI"),
|
|
else => try writer.writeAll("?"),
|
|
}
|
|
try writer.print(" (0x{X})", .{ch});
|
|
}
|
|
|
|
fn encodeCSI(writer: anytype, csi: terminal.Parser.Action.CSI) !void {
|
|
for (csi.intermediates) |v| try writer.print("{c} ", .{v});
|
|
for (csi.params, 0..) |v, i| {
|
|
if (i != 0) try writer.writeByte(';');
|
|
try writer.print("{d}", .{v});
|
|
}
|
|
if (csi.intermediates.len > 0 or csi.params.len > 0) try writer.writeByte(' ');
|
|
try writer.writeByte(csi.final);
|
|
}
|
|
|
|
fn encodeEsc(writer: anytype, esc: terminal.Parser.Action.ESC) !void {
|
|
for (esc.intermediates) |v| try writer.print("{c} ", .{v});
|
|
try writer.writeByte(esc.final);
|
|
}
|
|
|
|
fn encodeOSC(
|
|
alloc: Allocator,
|
|
writer: anytype,
|
|
md: *Metadata,
|
|
osc: terminal.osc.Command,
|
|
) !void {
|
|
// The description is just the tag
|
|
try writer.print("{s} ", .{@tagName(osc)});
|
|
|
|
// Add additional fields to metadata
|
|
switch (osc) {
|
|
inline else => |v, tag| if (tag == osc) {
|
|
try encodeMetadata(alloc, md, v);
|
|
},
|
|
}
|
|
}
|
|
|
|
fn encodeMetadata(
|
|
alloc: Allocator,
|
|
md: *Metadata,
|
|
v: anytype,
|
|
) !void {
|
|
switch (@TypeOf(v)) {
|
|
void => {},
|
|
[]const u8 => try md.put("data", try alloc.dupeZ(u8, v)),
|
|
else => |T| switch (@typeInfo(T)) {
|
|
.Struct => |info| inline for (info.fields) |field| {
|
|
try encodeMetadataSingle(
|
|
alloc,
|
|
md,
|
|
field.name,
|
|
@field(v, field.name),
|
|
);
|
|
},
|
|
|
|
else => {
|
|
@compileLog(T);
|
|
@compileError("unsupported type, see log");
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
fn encodeMetadataSingle(
|
|
alloc: Allocator,
|
|
md: *Metadata,
|
|
key: []const u8,
|
|
value: anytype,
|
|
) !void {
|
|
const Value = @TypeOf(value);
|
|
const info = @typeInfo(Value);
|
|
switch (info) {
|
|
.Optional => if (value) |unwrapped| {
|
|
try encodeMetadataSingle(alloc, md, key, unwrapped);
|
|
} else {
|
|
try md.put(key, try alloc.dupeZ(u8, "(unset)"));
|
|
},
|
|
|
|
.Bool => try md.put(
|
|
key,
|
|
try alloc.dupeZ(u8, if (value) "true" else "false"),
|
|
),
|
|
|
|
.Enum => try md.put(
|
|
key,
|
|
try alloc.dupeZ(u8, @tagName(value)),
|
|
),
|
|
|
|
.Union => |u| {
|
|
const Tag = u.tag_type orelse @compileError("Unions must have a tag");
|
|
const tag_name = @tagName(@as(Tag, value));
|
|
inline for (u.fields) |field| {
|
|
if (std.mem.eql(u8, field.name, tag_name)) {
|
|
const s = if (field.type == void)
|
|
try alloc.dupeZ(u8, tag_name)
|
|
else
|
|
try std.fmt.allocPrintZ(alloc, "{s}={}", .{
|
|
tag_name,
|
|
@field(value, field.name),
|
|
});
|
|
|
|
try md.put(key, s);
|
|
}
|
|
}
|
|
},
|
|
|
|
.Struct => try md.put(
|
|
key,
|
|
try alloc.dupeZ(u8, @typeName(Value)),
|
|
),
|
|
|
|
else => switch (Value) {
|
|
u8 => try md.put(
|
|
key,
|
|
try std.fmt.allocPrintZ(alloc, "{}", .{value}),
|
|
),
|
|
|
|
[]const u8 => try md.put(key, try alloc.dupeZ(u8, value)),
|
|
|
|
else => |T| {
|
|
@compileLog(T);
|
|
@compileError("unsupported type, see log");
|
|
},
|
|
},
|
|
}
|
|
}
|
|
};
|
|
|
|
/// Our VT stream handler.
|
|
pub const VTHandler = struct {
|
|
/// The surface that the inspector is attached to. We use this instead
|
|
/// of the inspector because this is pointer-stable.
|
|
surface: *Surface,
|
|
|
|
/// True if the handler is currently recording.
|
|
active: bool = true,
|
|
|
|
/// Current sequence number
|
|
current_seq: usize = 1,
|
|
|
|
/// Exclude certain actions by tag.
|
|
filter_exclude: ActionTagSet = ActionTagSet.initMany(&.{.print}),
|
|
filter_text: *cimgui.c.ImGuiTextFilter,
|
|
|
|
const ActionTagSet = std.EnumSet(terminal.Parser.Action.Tag);
|
|
|
|
pub fn init(surface: *Surface) VTHandler {
|
|
return .{
|
|
.surface = surface,
|
|
.filter_text = cimgui.c.ImGuiTextFilter_ImGuiTextFilter(""),
|
|
};
|
|
}
|
|
|
|
pub fn deinit(self: *VTHandler) void {
|
|
cimgui.c.ImGuiTextFilter_destroy(self.filter_text);
|
|
}
|
|
|
|
/// This is called with every single terminal action.
|
|
pub fn handleManually(self: *VTHandler, action: terminal.Parser.Action) !bool {
|
|
const insp = self.surface.inspector orelse return false;
|
|
|
|
// We always increment the sequence number, even if we're paused or
|
|
// filter out the event. This helps show the user that there is a gap
|
|
// between events and roughly how large that gap was.
|
|
defer self.current_seq +%= 1;
|
|
|
|
// If we're pausing, then we ignore all events.
|
|
if (!self.active) return true;
|
|
|
|
// We ignore certain action types that are too noisy.
|
|
switch (action) {
|
|
.dcs_put, .apc_put => return true,
|
|
else => {},
|
|
}
|
|
|
|
// If we requested a specific type to be ignored, ignore it.
|
|
// We return true because we did "handle" it by ignoring it.
|
|
if (self.filter_exclude.contains(std.meta.activeTag(action))) return true;
|
|
|
|
// Build our event
|
|
const alloc = self.surface.alloc;
|
|
var ev = try VTEvent.init(alloc, self.surface, action);
|
|
ev.seq = self.current_seq;
|
|
errdefer ev.deinit(alloc);
|
|
|
|
// Check if the event passes the filter
|
|
if (!ev.passFilter(self.filter_text)) {
|
|
ev.deinit(alloc);
|
|
return true;
|
|
}
|
|
|
|
const max_capacity = 100;
|
|
insp.vt_events.append(ev) catch |err| switch (err) {
|
|
error.OutOfMemory => if (insp.vt_events.capacity() < max_capacity) {
|
|
// We're out of memory, but we can allocate to our capacity.
|
|
const new_capacity = @min(insp.vt_events.capacity() * 2, max_capacity);
|
|
try insp.vt_events.resize(insp.surface.alloc, new_capacity);
|
|
try insp.vt_events.append(ev);
|
|
} else {
|
|
var it = insp.vt_events.iterator(.forward);
|
|
if (it.next()) |old_ev| old_ev.deinit(insp.surface.alloc);
|
|
insp.vt_events.deleteOldest(1);
|
|
try insp.vt_events.append(ev);
|
|
},
|
|
|
|
else => return err,
|
|
};
|
|
|
|
return true;
|
|
}
|
|
};
|