From 7b2af3a039f7a79fe6d500d5c695fda295f57ec8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 23 Oct 2023 17:16:01 -0700 Subject: [PATCH] inspector: start termio log --- src/inspector/Inspector.zig | 81 +++++++++++++++++++++++++++++++++++++ src/inspector/main.zig | 1 + src/inspector/termio.zig | 61 ++++++++++++++++++++++++++++ src/terminal/stream.zig | 20 ++++++++- src/termio/Exec.zig | 11 +++++ 5 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 src/inspector/termio.zig diff --git a/src/inspector/Inspector.zig b/src/inspector/Inspector.zig index 18f0bb865..f52a07438 100644 --- a/src/inspector/Inspector.zig +++ b/src/inspector/Inspector.zig @@ -16,6 +16,7 @@ const inspector = @import("main.zig"); const window_cell = "Cell"; const window_modes = "Modes"; const window_keyboard = "Keyboard"; +const window_termio = "Terminal IO"; const window_screen = "Screen"; const window_size = "Surface Info"; const window_imgui_demo = "Dear ImGui Demo"; @@ -44,6 +45,10 @@ cell: CellInspect = .{ .idle = {} }, /// The list of keyboard events key_events: inspector.key.EventRing, +/// The VT stream +vt_events: inspector.termio.VTEventRing, +vt_stream: inspector.termio.Stream, + const CellInspect = union(enum) { /// Idle, no cell inspection is requested idle: void, @@ -107,9 +112,24 @@ pub fn init(surface: *Surface) !Inspector { var key_buf = try inspector.key.EventRing.init(surface.alloc, 2); errdefer key_buf.deinit(surface.alloc); + var vt_events = try inspector.termio.VTEventRing.init(surface.alloc, 2); + errdefer vt_events.deinit(surface.alloc); + return .{ .surface = surface, .key_events = key_buf, + .vt_events = vt_events, + .vt_stream = .{ + .handler = .{ + .surface = surface, + }, + + .parser = .{ + .osc_parser = .{ + .alloc = surface.alloc, + }, + }, + }, }; } @@ -119,6 +139,13 @@ pub fn deinit(self: *Inspector) void { while (it.next()) |v| v.deinit(self.surface.alloc); self.key_events.deinit(self.surface.alloc); } + + { + var it = self.vt_events.iterator(.forward); + while (it.next()) |v| v.deinit(self.surface.alloc); + self.vt_events.deinit(self.surface.alloc); + self.vt_stream.deinit(); + } } /// Record a keyboard event. @@ -141,6 +168,11 @@ pub fn recordKeyEvent(self: *Inspector, ev: inspector.key.Event) !void { }; } +/// Record data read from the pty. +pub fn recordPtyRead(self: *Inspector, data: []const u8) !void { + try self.vt_stream.nextSlice(data); +} + /// Render the frame. pub fn render(self: *Inspector) void { const dock_id = cimgui.c.igDockSpaceOverViewport( @@ -158,6 +190,7 @@ pub fn render(self: *Inspector) void { self.renderScreenWindow(); self.renderModesWindow(); self.renderKeyboardWindow(); + self.renderTermioWindow(); self.renderCellWindow(); self.renderSizeWindow(); } @@ -208,6 +241,7 @@ fn setupLayout(self: *Inspector, dock_id_main: cimgui.c.ImGuiID) void { cimgui.c.igDockBuilderDockWindow(window_cell, dock_id.left); cimgui.c.igDockBuilderDockWindow(window_modes, dock_id.left); cimgui.c.igDockBuilderDockWindow(window_keyboard, dock_id.left); + cimgui.c.igDockBuilderDockWindow(window_termio, dock_id.left); cimgui.c.igDockBuilderDockWindow(window_screen, dock_id.left); cimgui.c.igDockBuilderDockWindow(window_imgui_demo, dock_id.left); cimgui.c.igDockBuilderDockWindow(window_size, dock_id.right); @@ -1048,3 +1082,50 @@ fn renderKeyboardWindow(self: *Inspector) void { } } // table } + +fn renderTermioWindow(self: *Inspector) void { + // Start our window. If we're collapsed we do nothing. + defer cimgui.c.igEnd(); + if (!cimgui.c.igBegin( + window_termio, + null, + cimgui.c.ImGuiWindowFlags_NoFocusOnAppearing, + )) return; + + list: { + if (self.vt_events.empty()) { + cimgui.c.igText("Waiting for events..."); + break :list; + } + + if (cimgui.c.igButton("Clear", .{ .x = 0, .y = 0 })) { + var it = self.vt_events.iterator(.forward); + while (it.next()) |v| v.deinit(self.surface.alloc); + self.vt_events.clear(); + } + + cimgui.c.igSeparator(); + + _ = cimgui.c.igBeginTable( + "table_vt_events", + 1, + cimgui.c.ImGuiTableFlags_RowBg | + cimgui.c.ImGuiTableFlags_Borders, + .{ .x = 0, .y = 0 }, + 0, + ); + defer cimgui.c.igEndTable(); + + var it = self.vt_events.iterator(.reverse); + while (it.next()) |ev| { + // Need to push an ID so that our selectable is unique. + cimgui.c.igPushID_Ptr(ev); + defer cimgui.c.igPopID(); + + cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + _ = cimgui.c.igTableSetColumnIndex(0); + + cimgui.c.igText("%s", ev.str.ptr); + } + } // table +} diff --git a/src/inspector/main.zig b/src/inspector/main.zig index 91192f62c..db061088d 100644 --- a/src/inspector/main.zig +++ b/src/inspector/main.zig @@ -1,4 +1,5 @@ pub const key = @import("key.zig"); +pub const termio = @import("termio.zig"); pub const Inspector = @import("Inspector.zig"); test { diff --git a/src/inspector/termio.zig b/src/inspector/termio.zig new file mode 100644 index 000000000..0e41bd337 --- /dev/null +++ b/src/inspector/termio.zig @@ -0,0 +1,61 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +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(Handler); + +/// VT event circular buffer. +pub const VTEventRing = CircBuf(VTEvent, undefined); + +/// VT event +pub const VTEvent = struct { + /// 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, + + pub fn deinit(self: *VTEvent, alloc: Allocator) void { + alloc.free(self.str); + } +}; + +/// Our VT stream handler. +const Handler = struct { + /// The surface that the inspector is attached to. We use this instead + /// of the inspector because this is pointer-stable. + surface: *Surface, + + /// This is called with every single terminal action. + pub fn handleManually(self: *Handler, action: terminal.Parser.Action) !bool { + const insp = self.surface.inspector orelse return false; + const alloc = self.surface.alloc; + const formatted = try std.fmt.allocPrintZ(alloc, "{}", .{action}); + errdefer alloc.free(formatted); + + const ev: VTEvent = .{ + .str = formatted, + }; + + 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; + } +}; diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index 1fda8c514..467a15177 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -58,11 +58,29 @@ pub fn Stream(comptime Handler: type) type { // log.debug("char: {c}", .{c}); const actions = self.parser.next(c); for (actions) |action_opt| { + const action = action_opt orelse continue; + + // If this handler handles everything manually then we do nothing + // if it can be processed. + if (@hasDecl(T, "handleManually")) { + const processed = self.handler.handleManually(action) catch |err| err: { + log.warn("error handling action manually err={} action={}", .{ + err, + action, + }); + + break :err false; + }; + + if (processed) continue; + } + // if (action_opt) |action| { // if (action != .print) // log.info("action: {}", .{action}); // } - switch (action_opt orelse continue) { + + switch (action) { .print => |p| if (@hasDecl(T, "print")) try self.handler.print(p), .execute => |code| try self.execute(code), .csi_dispatch => |csi_action| try self.csiDispatch(csi_action), diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 242b2e574..229f69bec 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -1133,6 +1133,17 @@ const ReadThread = struct { // Schedule a render ev.queueRender() catch unreachable; + // If we have an inspector, send the data to it too. The inspector + // will keep track of the VT sequences that we are processing. + if (ev.renderer_state.inspector) |insp| { + // This forces all VT sequences to be parsed twice, meaning our + // vt processing always slows down by roughly 50% with the inspector + // active. That's fine for now but room for improvement. + insp.recordPtyRead(buf) catch |err| { + log.err("error recording pty read in inspector err={}", .{err}); + }; + } + // Process the terminal data. This is an extremely hot part of the // terminal emulator, so we do some abstraction leakage to avoid // function calls and unnecessary logic.