ghostty/src/inspector/termio.zig
2025-01-05 21:58:45 +01:00

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;
}
};