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