inspector: start termio log

This commit is contained in:
Mitchell Hashimoto
2023-10-23 17:16:01 -07:00
parent 5da0e0dab6
commit 7b2af3a039
5 changed files with 173 additions and 1 deletions

View File

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

View File

@ -1,4 +1,5 @@
pub const key = @import("key.zig");
pub const termio = @import("termio.zig");
pub const Inspector = @import("Inspector.zig");
test {

61
src/inspector/termio.zig Normal file
View File

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

View File

@ -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),

View File

@ -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.