mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
381 lines
12 KiB
Zig
381 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("../datastruct/main.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);
|
|
|
|
/// Initialize 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),
|
|
);
|
|
},
|
|
|
|
.Union => |info| {
|
|
const Tag = info.tag_type orelse @compileError("Unions must have a tag");
|
|
const tag_name = @tagName(@as(Tag, v));
|
|
inline for (info.fields) |field| {
|
|
if (std.mem.eql(u8, field.name, tag_name)) {
|
|
if (field.type == void) {
|
|
break try md.put("data", tag_name);
|
|
} else {
|
|
break try encodeMetadataSingle(alloc, md, tag_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, u16 => 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;
|
|
}
|
|
};
|