mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-22 15:25:20 +00:00
inspector: move termio to dedicated widget
This commit is contained in:
@@ -33,29 +33,9 @@ mouse: widgets.surface.Mouse = .{},
|
||||
/// A selected cell.
|
||||
cell: CellInspect = .{ .idle = {} },
|
||||
|
||||
/// The VT stream
|
||||
vt_events: inspector.termio.VTEventRing,
|
||||
vt_stream: inspector.termio.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,
|
||||
|
||||
// ImGui state
|
||||
gui: widgets.surface.Inspector,
|
||||
|
||||
/// Enum representing keyboard navigation actions
|
||||
const KeyAction = enum {
|
||||
down,
|
||||
none,
|
||||
up,
|
||||
};
|
||||
|
||||
const CellInspect = union(enum) {
|
||||
/// Idle, no cell inspection is requested
|
||||
idle: void,
|
||||
@@ -146,34 +126,18 @@ pub fn setup() void {
|
||||
}
|
||||
|
||||
pub fn init(surface: *Surface) !Inspector {
|
||||
var vt_events = try inspector.termio.VTEventRing.init(surface.alloc, 2);
|
||||
errdefer vt_events.deinit(surface.alloc);
|
||||
|
||||
var vt_handler = inspector.termio.VTHandler.init(surface);
|
||||
errdefer vt_handler.deinit();
|
||||
|
||||
var gui: widgets.surface.Inspector = try .init(surface.alloc);
|
||||
var gui: widgets.surface.Inspector = try .init(surface.alloc, surface);
|
||||
errdefer gui.deinit(surface.alloc);
|
||||
|
||||
return .{
|
||||
.surface = surface,
|
||||
.gui = gui,
|
||||
.vt_events = vt_events,
|
||||
.vt_stream = .initAlloc(surface.alloc, vt_handler),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Inspector) void {
|
||||
self.gui.deinit(self.surface.alloc);
|
||||
self.cell.deinit();
|
||||
|
||||
{
|
||||
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.
|
||||
@@ -200,7 +164,7 @@ 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);
|
||||
try self.gui.vt_stream.parser_stream.nextSlice(data);
|
||||
}
|
||||
|
||||
/// Render the frame.
|
||||
@@ -322,295 +286,3 @@ fn renderCellWindow(self: *Inspector) void {
|
||||
selected.row,
|
||||
);
|
||||
}
|
||||
|
||||
/// Helper function to check keyboard state and determine navigation action.
|
||||
fn getKeyAction(self: *Inspector) KeyAction {
|
||||
_ = self;
|
||||
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;
|
||||
}
|
||||
|
||||
fn renderTermioWindow(self: *Inspector) void {
|
||||
// Start our window. If we're collapsed we do nothing.
|
||||
defer cimgui.c.ImGui_End();
|
||||
if (!cimgui.c.ImGui_Begin(
|
||||
window_termio,
|
||||
null,
|
||||
cimgui.c.ImGuiWindowFlags_NoFocusOnAppearing,
|
||||
)) return;
|
||||
|
||||
const popup_filter = "Filter";
|
||||
|
||||
list: {
|
||||
const pause_play: [:0]const u8 = if (self.vt_stream.handler.active)
|
||||
"Pause##pause_play"
|
||||
else
|
||||
"Resume##pause_play";
|
||||
if (cimgui.c.ImGui_Button(pause_play.ptr)) {
|
||||
self.vt_stream.handler.active = !self.vt_stream.handler.active;
|
||||
}
|
||||
|
||||
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 (!self.vt_events.empty()) {
|
||||
cimgui.c.ImGui_SameLineEx(0, cimgui.c.ImGui_GetStyle().*.ItemInnerSpacing.x);
|
||||
if (cimgui.c.ImGui_Button("Clear")) {
|
||||
var it = self.vt_events.iterator(.forward);
|
||||
while (it.next()) |v| v.deinit(self.surface.alloc);
|
||||
self.vt_events.clear();
|
||||
|
||||
// We also reset the sequence number.
|
||||
self.vt_stream.handler.current_seq = 1;
|
||||
}
|
||||
}
|
||||
|
||||
cimgui.c.ImGui_Separator();
|
||||
|
||||
if (self.vt_events.empty()) {
|
||||
cimgui.c.ImGui_Text("Waiting for events...");
|
||||
break :list;
|
||||
}
|
||||
|
||||
_ = 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 = self.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 (!self.vt_events.empty()) {
|
||||
var it = self.vt_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 = self.vt_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 inspector.termio.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 = self.vt_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.str.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) {
|
||||
{
|
||||
widgets.screen.cursorTable(&ev.cursor);
|
||||
widgets.screen.cursorStyle(
|
||||
&ev.cursor,
|
||||
&self.surface.renderer_state.terminal.colors.palette.current,
|
||||
);
|
||||
|
||||
_ = 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 = !self.vt_stream.handler.filter_exclude.contains(tag);
|
||||
if (cimgui.c.ImGui_Checkbox(@tagName(tag).ptr, &value)) {
|
||||
if (value) {
|
||||
self.vt_stream.handler.filter_exclude.remove(tag);
|
||||
} else {
|
||||
self.vt_stream.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(
|
||||
&self.vt_stream.handler.filter_text,
|
||||
"##filter_text",
|
||||
0,
|
||||
);
|
||||
|
||||
cimgui.c.ImGui_Separator();
|
||||
if (cimgui.c.ImGui_Button("Close")) {
|
||||
cimgui.c.ImGui_CloseCurrentPopup();
|
||||
}
|
||||
} // filter popup
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@ const std = @import("std");
|
||||
pub const cell = @import("cell.zig");
|
||||
pub const key = @import("widgets/key.zig");
|
||||
|
||||
pub const termio = @import("termio.zig");
|
||||
|
||||
pub const Cell = cell.Cell;
|
||||
pub const Inspector = @import("Inspector.zig");
|
||||
|
||||
|
||||
@@ -1,398 +0,0 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const cimgui = @import("dcimgui");
|
||||
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.Io.Writer.Allocating = .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.screens.active.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: *const 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: *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.
|
||||
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 = .initMany(&.{.print}),
|
||||
filter_text: cimgui.c.ImGuiTextFilter = .{},
|
||||
|
||||
const ActionTagSet = std.EnumSet(terminal.Parser.Action.Tag);
|
||||
|
||||
pub fn init(surface: *Surface) VTHandler {
|
||||
return .{
|
||||
.surface = surface,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *VTHandler) void {
|
||||
_ = self;
|
||||
}
|
||||
|
||||
pub fn vt(
|
||||
self: *VTHandler,
|
||||
comptime action: Stream.Action.Tag,
|
||||
value: Stream.Action.Value(action),
|
||||
) !void {
|
||||
_ = self;
|
||||
_ = value;
|
||||
}
|
||||
|
||||
/// 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;
|
||||
}
|
||||
};
|
||||
@@ -7,6 +7,7 @@ pub const screen = @import("widgets/screen.zig");
|
||||
pub const style = @import("widgets/style.zig");
|
||||
pub const surface = @import("widgets/surface.zig");
|
||||
pub const terminal = @import("widgets/terminal.zig");
|
||||
pub const termio = @import("widgets/termio.zig");
|
||||
|
||||
/// Draws a "(?)" disabled text marker that shows some help text
|
||||
/// on hover.
|
||||
|
||||
@@ -15,23 +15,27 @@ const window_imgui_demo = "Dear ImGui Demo";
|
||||
const window_keyboard = "Keyboard";
|
||||
const window_terminal = "Terminal";
|
||||
const window_surface = "Surface";
|
||||
const window_termio = "Terminal IO";
|
||||
|
||||
pub const Inspector = struct {
|
||||
/// Internal GUI state
|
||||
surface_info: Info,
|
||||
key_stream: widgets.key.Stream,
|
||||
terminal_info: widgets.terminal.Info,
|
||||
vt_stream: widgets.termio.Stream,
|
||||
|
||||
pub fn init(alloc: Allocator) !Inspector {
|
||||
pub fn init(alloc: Allocator, surface: *Surface) !Inspector {
|
||||
return .{
|
||||
.surface_info = .empty,
|
||||
.key_stream = try .init(alloc),
|
||||
.terminal_info = .empty,
|
||||
.vt_stream = try .init(alloc, surface),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Inspector, alloc: Allocator) void {
|
||||
self.key_stream.deinit(alloc);
|
||||
self.vt_stream.deinit(alloc);
|
||||
}
|
||||
|
||||
pub fn draw(
|
||||
@@ -96,6 +100,22 @@ pub const Inspector = struct {
|
||||
surface.alloc,
|
||||
);
|
||||
}
|
||||
|
||||
// Terminal IO window
|
||||
{
|
||||
const open = cimgui.c.ImGui_Begin(
|
||||
window_termio,
|
||||
null,
|
||||
cimgui.c.ImGuiWindowFlags_NoFocusOnAppearing,
|
||||
);
|
||||
defer cimgui.c.ImGui_End();
|
||||
if (open) {
|
||||
self.vt_stream.draw(
|
||||
surface.alloc,
|
||||
&t.colors.palette.current,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (first_render) {
|
||||
@@ -136,6 +156,7 @@ pub const Inspector = struct {
|
||||
cimgui.ImGui_DockBuilderDockWindow(window_terminal, dock_id_main);
|
||||
cimgui.ImGui_DockBuilderDockWindow(window_surface, dock_id_main);
|
||||
cimgui.ImGui_DockBuilderDockWindow(window_keyboard, dock_id_main);
|
||||
cimgui.ImGui_DockBuilderDockWindow(window_termio, dock_id_main);
|
||||
cimgui.ImGui_DockBuilderDockWindow(window_imgui_demo, dock_id_main);
|
||||
cimgui.ImGui_DockBuilderFinish(dockspace_id);
|
||||
}
|
||||
|
||||
728
src/inspector/widgets/termio.zig
Normal file
728
src/inspector/widgets/termio.zig
Normal file
@@ -0,0 +1,728 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
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");
|
||||
|
||||
/// The stream handler for our inspector.
|
||||
pub const ParserStream = 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.Io.Writer.Allocating = .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.screens.active.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: *const 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: *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.
|
||||
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 = .initMany(&.{.print}),
|
||||
filter_text: cimgui.c.ImGuiTextFilter = .{},
|
||||
|
||||
const ActionTagSet = std.EnumSet(terminal.Parser.Action.Tag);
|
||||
|
||||
pub fn init(surface: *Surface) VTHandler {
|
||||
return .{
|
||||
.surface = surface,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *VTHandler) void {
|
||||
_ = self;
|
||||
}
|
||||
|
||||
pub fn vt(
|
||||
self: *VTHandler,
|
||||
comptime action: ParserStream.Action.Tag,
|
||||
value: ParserStream.Action.Value(action),
|
||||
) !void {
|
||||
_ = self;
|
||||
_ = value;
|
||||
}
|
||||
|
||||
/// 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;
|
||||
const vt_events = &insp.gui.vt_stream.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 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;
|
||||
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(self.surface.alloc, new_capacity);
|
||||
try vt_events.append(ev);
|
||||
} else {
|
||||
var it = vt_events.iterator(.forward);
|
||||
if (it.next()) |old_ev| old_ev.deinit(self.surface.alloc);
|
||||
vt_events.deleteOldest(1);
|
||||
try vt_events.append(ev);
|
||||
},
|
||||
|
||||
else => return err,
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
/// Enum representing keyboard navigation actions
|
||||
const KeyAction = enum {
|
||||
down,
|
||||
none,
|
||||
up,
|
||||
};
|
||||
|
||||
/// VT event stream inspector widget.
|
||||
pub const Stream = struct {
|
||||
events: VTEventRing,
|
||||
parser_stream: ParserStream,
|
||||
|
||||
/// 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, surface: *Surface) !Stream {
|
||||
var events: VTEventRing = try .init(alloc, 2);
|
||||
errdefer events.deinit(alloc);
|
||||
|
||||
var handler = VTHandler.init(surface);
|
||||
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 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";
|
||||
|
||||
list: {
|
||||
const pause_play: [:0]const u8 = if (handler.active)
|
||||
"Pause##pause_play"
|
||||
else
|
||||
"Resume##pause_play";
|
||||
if (cimgui.c.ImGui_Button(pause_play.ptr)) {
|
||||
handler.active = !handler.active;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
cimgui.c.ImGui_Separator();
|
||||
|
||||
if (events.empty()) {
|
||||
cimgui.c.ImGui_Text("Waiting for events...");
|
||||
break :list;
|
||||
}
|
||||
|
||||
_ = 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.str.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;
|
||||
}
|
||||
Reference in New Issue
Block a user