aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/inspector/widgets/termio.zig
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-04-04 19:22:04 +0300
committerCan Celik <ogulcancelik@gmail.com>2026-04-05 01:23:06 +0300
commit41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch)
tree3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/inspector/widgets/termio.zig
parent55c138263e190a1e79d4b997bfb9397306c0ec1f (diff)
feat: finish ghostty pane backend integration
Diffstat (limited to 'vendor/libghostty-vt/src/inspector/widgets/termio.zig')
-rw-r--r--vendor/libghostty-vt/src/inspector/widgets/termio.zig811
1 files changed, 811 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/inspector/widgets/termio.zig b/vendor/libghostty-vt/src/inspector/widgets/termio.zig
new file mode 100644
index 00000000..b721d942
--- /dev/null
+++ b/vendor/libghostty-vt/src/inspector/widgets/termio.zig
@@ -0,0 +1,811 @@
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const ArenaAllocator = std.heap.ArenaAllocator;
+const cimgui = @import("dcimgui");
+const terminal = @import("../../terminal/main.zig");
+const CircBuf = @import("../../datastruct/main.zig").CircBuf;
+const Surface = @import("../../Surface.zig");
+const screen = @import("screen.zig");
+
+/// VT event stream inspector widget.
+pub const Stream = struct {
+ events: VTEvent.Ring,
+ parser_stream: VTHandler.Stream,
+
+ /// The currently selected event sequence number for keyboard navigation
+ selected_event_seq: ?u32 = null,
+
+ /// Flag indicating whether we need to scroll to the selected item
+ need_scroll_to_selected: bool = false,
+
+ /// Flag indicating whether the selection was made by keyboard
+ is_keyboard_selection: bool = false,
+
+ pub fn init(alloc: Allocator) !Stream {
+ var events: VTEvent.Ring = try .init(alloc, 2);
+ errdefer events.deinit(alloc);
+
+ var handler: VTHandler = .init;
+ errdefer handler.deinit();
+
+ return .{
+ .events = events,
+ .parser_stream = .initAlloc(alloc, handler),
+ };
+ }
+
+ pub fn deinit(self: *Stream, alloc: Allocator) void {
+ var it = self.events.iterator(.forward);
+ while (it.next()) |v| v.deinit(alloc);
+ self.events.deinit(alloc);
+
+ self.parser_stream.deinit();
+ }
+
+ pub fn recordPtyRead(
+ self: *Stream,
+ alloc: Allocator,
+ t: *terminal.Terminal,
+ data: []const u8,
+ ) !void {
+ self.parser_stream.handler.state = .{
+ .alloc = alloc,
+ .terminal = t,
+ .events = &self.events,
+ };
+ defer self.parser_stream.handler.state = null;
+ self.parser_stream.nextSlice(data);
+ }
+
+ pub fn draw(
+ self: *Stream,
+ alloc: Allocator,
+ palette: *const terminal.color.Palette,
+ ) void {
+ const events = &self.events;
+ const handler = &self.parser_stream.handler;
+ const popup_filter = "Filter";
+
+ // Controls
+ {
+ const pause_play: [:0]const u8 = if (!handler.paused)
+ "Pause##pause_play"
+ else
+ "Resume##pause_play";
+ if (cimgui.c.ImGui_Button(pause_play.ptr)) {
+ handler.paused = !handler.paused;
+ }
+
+ cimgui.c.ImGui_SameLineEx(0, cimgui.c.ImGui_GetStyle().*.ItemInnerSpacing.x);
+ if (cimgui.c.ImGui_Button("Filter")) {
+ cimgui.c.ImGui_OpenPopup(
+ popup_filter,
+ cimgui.c.ImGuiPopupFlags_None,
+ );
+ }
+
+ if (!events.empty()) {
+ cimgui.c.ImGui_SameLineEx(0, cimgui.c.ImGui_GetStyle().*.ItemInnerSpacing.x);
+ if (cimgui.c.ImGui_Button("Clear")) {
+ var it = events.iterator(.forward);
+ while (it.next()) |v| v.deinit(alloc);
+ events.clear();
+
+ handler.current_seq = 1;
+ }
+ }
+ }
+
+ // Events Table
+ if (events.empty()) {
+ cimgui.c.ImGui_Text("Waiting for events...");
+ } else {
+ // TODO: Eventually
+ // eventTable(events);
+ }
+
+ {
+ cimgui.c.ImGui_Separator();
+
+ _ = cimgui.c.ImGui_BeginTable(
+ "table_vt_events",
+ 3,
+ cimgui.c.ImGuiTableFlags_RowBg |
+ cimgui.c.ImGuiTableFlags_Borders,
+ );
+ defer cimgui.c.ImGui_EndTable();
+
+ cimgui.c.ImGui_TableSetupColumn(
+ "Seq",
+ cimgui.c.ImGuiTableColumnFlags_WidthFixed,
+ );
+ cimgui.c.ImGui_TableSetupColumn(
+ "Kind",
+ cimgui.c.ImGuiTableColumnFlags_WidthFixed,
+ );
+ cimgui.c.ImGui_TableSetupColumn(
+ "Description",
+ cimgui.c.ImGuiTableColumnFlags_WidthStretch,
+ );
+
+ // Handle keyboard navigation when window is focused
+ if (cimgui.c.ImGui_IsWindowFocused(cimgui.c.ImGuiFocusedFlags_RootAndChildWindows)) {
+ const key_pressed = getKeyAction();
+
+ switch (key_pressed) {
+ .none => {},
+ .up, .down => {
+ // If no event is selected, select the first/last event based on direction
+ if (self.selected_event_seq == null) {
+ if (!events.empty()) {
+ var it = events.iterator(if (key_pressed == .up) .forward else .reverse);
+ if (it.next()) |ev| {
+ self.selected_event_seq = @as(u32, @intCast(ev.seq));
+ }
+ }
+ } else {
+ // Find next/previous event based on current selection
+ var it = events.iterator(.reverse);
+ switch (key_pressed) {
+ .down => {
+ var found = false;
+ while (it.next()) |ev| {
+ if (found) {
+ self.selected_event_seq = @as(u32, @intCast(ev.seq));
+ break;
+ }
+ if (ev.seq == self.selected_event_seq.?) {
+ found = true;
+ }
+ }
+ },
+ .up => {
+ var prev_ev: ?*const VTEvent = null;
+ while (it.next()) |ev| {
+ if (ev.seq == self.selected_event_seq.?) {
+ if (prev_ev) |prev| {
+ self.selected_event_seq = @as(u32, @intCast(prev.seq));
+ break;
+ }
+ }
+ prev_ev = ev;
+ }
+ },
+ .none => unreachable,
+ }
+ }
+
+ // Mark that we need to scroll to the newly selected item
+ self.need_scroll_to_selected = true;
+ self.is_keyboard_selection = true;
+ },
+ }
+ }
+
+ var it = events.iterator(.reverse);
+ while (it.next()) |ev| {
+ // Need to push an ID so that our selectable is unique.
+ cimgui.c.ImGui_PushIDPtr(ev);
+ defer cimgui.c.ImGui_PopID();
+
+ cimgui.c.ImGui_TableNextRow();
+ _ = cimgui.c.ImGui_TableNextColumn();
+
+ // Store the previous selection state to detect changes
+ const was_selected = ev.imgui_selected;
+
+ // Update selection state based on keyboard navigation
+ if (self.selected_event_seq) |seq| {
+ ev.imgui_selected = (@as(u32, @intCast(ev.seq)) == seq);
+ }
+
+ // Handle selectable widget
+ if (cimgui.c.ImGui_SelectableBoolPtr(
+ "##select",
+ &ev.imgui_selected,
+ cimgui.c.ImGuiSelectableFlags_SpanAllColumns,
+ )) {
+ // If selection state changed, update keyboard navigation state
+ if (ev.imgui_selected != was_selected) {
+ self.selected_event_seq = if (ev.imgui_selected)
+ @as(u32, @intCast(ev.seq))
+ else
+ null;
+ self.is_keyboard_selection = false;
+ }
+ }
+
+ cimgui.c.ImGui_SameLine();
+ cimgui.c.ImGui_Text("%d", ev.seq);
+ _ = cimgui.c.ImGui_TableNextColumn();
+ cimgui.c.ImGui_Text("%s", @tagName(ev.kind).ptr);
+ _ = cimgui.c.ImGui_TableNextColumn();
+ cimgui.c.ImGui_Text("%s", ev.raw_description.ptr);
+
+ // If the event is selected, we render info about it. For now
+ // we put this in the last column because that's the widest and
+ // imgui has no way to make a column span.
+ if (ev.imgui_selected) {
+ {
+ screen.cursorTable(&ev.cursor);
+ screen.cursorStyle(&ev.cursor, palette);
+
+ _ = cimgui.c.ImGui_BeginTable(
+ "details",
+ 2,
+ cimgui.c.ImGuiTableFlags_None,
+ );
+ defer cimgui.c.ImGui_EndTable();
+ {
+ cimgui.c.ImGui_TableNextRow();
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Scroll Region");
+ }
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ cimgui.c.ImGui_Text(
+ "T=%d B=%d L=%d R=%d",
+ ev.scrolling_region.top,
+ ev.scrolling_region.bottom,
+ ev.scrolling_region.left,
+ ev.scrolling_region.right,
+ );
+ }
+ }
+
+ var md_it = ev.metadata.iterator();
+ while (md_it.next()) |entry| {
+ var buf: [256]u8 = undefined;
+ const key = std.fmt.bufPrintZ(&buf, "{s}", .{entry.key_ptr.*}) catch
+ "<internal error>";
+ cimgui.c.ImGui_TableNextRow();
+ _ = cimgui.c.ImGui_TableNextColumn();
+ cimgui.c.ImGui_Text("%s", key.ptr);
+ _ = cimgui.c.ImGui_TableNextColumn();
+ cimgui.c.ImGui_Text("%s", entry.value_ptr.ptr);
+ }
+ }
+
+ // If this is the selected event and scrolling is needed, scroll to it
+ if (self.need_scroll_to_selected and self.is_keyboard_selection) {
+ cimgui.c.ImGui_SetScrollHereY(0.5);
+ self.need_scroll_to_selected = false;
+ }
+ }
+ }
+ } // table
+
+ if (cimgui.c.ImGui_BeginPopupModal(
+ popup_filter,
+ null,
+ cimgui.c.ImGuiWindowFlags_AlwaysAutoResize,
+ )) {
+ defer cimgui.c.ImGui_EndPopup();
+
+ cimgui.c.ImGui_Text("Changed filter settings will only affect future events.");
+
+ cimgui.c.ImGui_Separator();
+
+ {
+ _ = cimgui.c.ImGui_BeginTable(
+ "table_filter_kind",
+ 3,
+ cimgui.c.ImGuiTableFlags_None,
+ );
+ defer cimgui.c.ImGui_EndTable();
+
+ inline for (@typeInfo(terminal.Parser.Action.Tag).@"enum".fields) |field| {
+ const tag = @field(terminal.Parser.Action.Tag, field.name);
+ if (tag == .apc_put or tag == .dcs_put) continue;
+
+ _ = cimgui.c.ImGui_TableNextColumn();
+ var value = !handler.filter_exclude.contains(tag);
+ if (cimgui.c.ImGui_Checkbox(@tagName(tag).ptr, &value)) {
+ if (value) {
+ handler.filter_exclude.remove(tag);
+ } else {
+ handler.filter_exclude.insert(tag);
+ }
+ }
+ }
+ } // Filter kind table
+
+ cimgui.c.ImGui_Separator();
+
+ cimgui.c.ImGui_Text(
+ "Filter by string. Empty displays all, \"abc\" finds lines\n" ++
+ "containing \"abc\", \"abc,xyz\" finds lines containing \"abc\"\n" ++
+ "or \"xyz\", \"-abc\" excludes lines containing \"abc\".",
+ );
+ _ = cimgui.c.ImGuiTextFilter_Draw(
+ &handler.filter_text,
+ "##filter_text",
+ 0,
+ );
+
+ cimgui.c.ImGui_Separator();
+ if (cimgui.c.ImGui_Button("Close")) {
+ cimgui.c.ImGui_CloseCurrentPopup();
+ }
+ } // filter popup
+ }
+};
+
+/// Helper function to check keyboard state and determine navigation action.
+fn getKeyAction() KeyAction {
+ const keys = .{
+ .{ .key = cimgui.c.ImGuiKey_J, .action = KeyAction.down },
+ .{ .key = cimgui.c.ImGuiKey_DownArrow, .action = KeyAction.down },
+ .{ .key = cimgui.c.ImGuiKey_K, .action = KeyAction.up },
+ .{ .key = cimgui.c.ImGuiKey_UpArrow, .action = KeyAction.up },
+ };
+
+ inline for (keys) |k| {
+ if (cimgui.c.ImGui_IsKeyPressed(k.key)) {
+ return k.action;
+ }
+ }
+ return .none;
+}
+
+pub fn eventTable(events: *const VTEvent.Ring) void {
+ if (!cimgui.c.ImGui_BeginTable(
+ "events",
+ 3,
+ cimgui.c.ImGuiTableFlags_RowBg |
+ cimgui.c.ImGuiTableFlags_Borders,
+ )) return;
+ defer cimgui.c.ImGui_EndTable();
+
+ cimgui.c.ImGui_TableSetupColumn(
+ "Seq",
+ cimgui.c.ImGuiTableColumnFlags_WidthFixed,
+ );
+ cimgui.c.ImGui_TableSetupColumn(
+ "Kind",
+ cimgui.c.ImGuiTableColumnFlags_WidthFixed,
+ );
+ cimgui.c.ImGui_TableSetupColumn(
+ "Description",
+ cimgui.c.ImGuiTableColumnFlags_WidthStretch,
+ );
+
+ var it = events.iterator(.reverse);
+ while (it.next()) |ev| {
+ // Need to push an ID so that our selectable is unique.
+ cimgui.c.ImGui_PushIDPtr(ev);
+ defer cimgui.c.ImGui_PopID();
+
+ cimgui.c.ImGui_TableNextRow();
+ _ = cimgui.c.ImGui_TableNextColumn();
+
+ cimgui.c.ImGui_SameLine();
+ cimgui.c.ImGui_Text("%d", ev.seq);
+ _ = cimgui.c.ImGui_TableNextColumn();
+ cimgui.c.ImGui_Text("%s", @tagName(ev.kind).ptr);
+ _ = cimgui.c.ImGui_TableNextColumn();
+ cimgui.c.ImGui_Text("%s", ev.raw_description.ptr);
+ }
+}
+
+/// VT event. This isn't public because this is just how we store internal
+/// events.
+const VTEvent = struct {
+ /// The arena that all allocated memory for this event is stored.
+ arena_state: ArenaAllocator.State,
+
+ /// Sequence number, just monotonically increasing and wrapping if
+ /// it ever overflows. It gives us a nice way to visualize progress.
+ seq: usize = 1,
+
+ /// Kind of event, for filtering
+ kind: Kind,
+
+ /// The description of the raw event in a more human-friendly format.
+ /// For example for control sequences this is the full sequence but
+ /// control characters are replaced with human-readable names, e.g.
+ /// 0x07 (bell) becomes BEL.
+ raw_description: [: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);
+
+ /// Circular buffer of VT events.
+ pub const Ring = CircBuf(VTEvent, undefined);
+
+ /// Initialize the event information for the given parser action.
+ pub fn init(
+ alloc_gpa: Allocator,
+ t: *const terminal.Terminal,
+ action: terminal.Parser.Action,
+ ) !VTEvent {
+ var arena: ArenaAllocator = .init(alloc_gpa);
+ errdefer arena.deinit();
+ const alloc = arena.allocator();
+
+ var md = Metadata.init(alloc);
+ var buf: std.Io.Writer.Allocating = .init(alloc);
+ try encodeAction(alloc, &buf.writer, &md, action);
+ const desc = try buf.toOwnedSliceSentinel(0);
+
+ 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,
+ };
+
+ return .{
+ .arena_state = arena.state,
+ .kind = kind,
+ .raw_description = desc,
+ .cursor = t.screens.active.cursor,
+ .scrolling_region = t.scrolling_region,
+ .metadata = md.unmanaged,
+ };
+ }
+
+ pub fn deinit(self: *VTEvent, alloc_gpa: Allocator) void {
+ var arena = self.arena_state.promote(alloc_gpa);
+ arena.deinit();
+ }
+
+ /// Returns true if the event passes the given filter.
+ pub fn passFilter(
+ self: *const VTEvent,
+ filter: *const cimgui.c.ImGuiTextFilter,
+ ) bool {
+ // Check our main string
+ if (cimgui.c.ImGuiTextFilter_PassFilter(
+ filter,
+ self.raw_description.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: *std.Io.Writer,
+ 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("{f}", .{action}),
+ }
+ }
+
+ fn encodePrint(writer: *std.Io.Writer, action: terminal.Parser.Action) !void {
+ const ch = action.print;
+ try writer.print("'{u}' (U+{X})", .{ ch, ch });
+ }
+
+ fn encodeExecute(writer: *std.Io.Writer, 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: *std.Io.Writer, 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: *std.Io.Writer, 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: *std.Io.Writer,
+ 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,
+ [:0]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 if (field.type == [:0]const u8 or field.type == []const u8)
+ try std.fmt.allocPrintSentinel(alloc, "{s}={s}", .{
+ tag_name,
+ @field(value, field.name),
+ }, 0)
+ else
+ try std.fmt.allocPrintSentinel(alloc, "{s}={}", .{
+ tag_name,
+ @field(value, field.name),
+ }, 0);
+
+ try md.put(key, s);
+ }
+ }
+ },
+
+ .@"struct" => try md.put(
+ key,
+ try alloc.dupeZ(u8, @typeName(Value)),
+ ),
+
+ else => switch (Value) {
+ []const u8,
+ [:0]const u8,
+ => try md.put(key, try alloc.dupeZ(u8, value)),
+
+ else => |T| switch (@typeInfo(T)) {
+ .int => try md.put(
+ key,
+ try std.fmt.allocPrintSentinel(alloc, "{}", .{value}, 0),
+ ),
+ else => {
+ @compileLog(T);
+ @compileError("unsupported type, see log");
+ },
+ },
+ },
+ }
+ }
+};
+
+/// Our VT stream handler for the Stream widget. This isn't public
+/// because there is no reason to use this directly.
+const VTHandler = struct {
+ /// The capture state, must be set before use. If null, then
+ /// events are dropped.
+ state: ?State,
+
+ /// True to pause this artificially.
+ paused: bool,
+
+ /// Current sequence number
+ current_seq: usize,
+
+ /// Exclude certain actions by tag.
+ filter_exclude: ActionTagSet,
+ filter_text: cimgui.c.ImGuiTextFilter,
+
+ const Stream = terminal.Stream(VTHandler);
+
+ pub const ActionTagSet = std.EnumSet(terminal.Parser.Action.Tag);
+
+ pub const State = struct {
+ /// The allocator to use for the events.
+ alloc: Allocator,
+
+ /// The terminal state at the time of the event.
+ terminal: *const terminal.Terminal,
+
+ /// The event ring to write events to.
+ events: *VTEvent.Ring,
+ };
+
+ pub const init: VTHandler = .{
+ .state = null,
+ .paused = false,
+ .current_seq = 1,
+ .filter_exclude = .initMany(&.{.print}),
+ .filter_text = .{},
+ };
+
+ pub fn deinit(self: *VTHandler) void {
+ // Required for the parser stream interface
+ _ = self;
+ }
+
+ pub fn vt(
+ self: *VTHandler,
+ comptime action: VTHandler.Stream.Action.Tag,
+ value: VTHandler.Stream.Action.Value(action),
+ ) void {
+ _ = self;
+ _ = value;
+ }
+
+ /// This is called with every single terminal action.
+ pub fn vtRaw(self: *VTHandler, action: terminal.Parser.Action) !bool {
+ const state: *State = if (self.state) |*s| s else return true;
+ const alloc = state.alloc;
+ const vt_events = state.events;
+
+ // 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 manually paused, we ignore all events.
+ if (self.paused) 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
+ var ev: VTEvent = try .init(
+ alloc,
+ state.terminal,
+ 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;
+ vt_events.append(ev) catch |err| switch (err) {
+ error.OutOfMemory => if (vt_events.capacity() < max_capacity) {
+ // We're out of memory, but we can allocate to our capacity.
+ const new_capacity = @min(vt_events.capacity() * 2, max_capacity);
+ try vt_events.resize(alloc, new_capacity);
+ try vt_events.append(ev);
+ } else {
+ var it = vt_events.iterator(.forward);
+ if (it.next()) |old_ev| old_ev.deinit(alloc);
+ vt_events.deleteOldest(1);
+ try vt_events.append(ev);
+ },
+
+ else => return err,
+ };
+
+ // Do NOT skip it, because we want to record more information
+ // about this event.
+ return false;
+ }
+};
+
+/// Enum representing keyboard navigation actions
+const KeyAction = enum {
+ down,
+ none,
+ up,
+};