From 00ed6069f68fe48963051e4fb0c8c056e5665946 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 23 Oct 2023 14:46:10 -0700 Subject: [PATCH] inspector: render basic key inspector --- src/Inspector.zig | 120 +++++++++++++++++++++++++++++++++-------- src/Surface.zig | 9 ++-- src/inspector/key.zig | 71 ++++++++++++++++++++++++ src/inspector/main.zig | 5 ++ src/main.zig | 1 + 5 files changed, 180 insertions(+), 26 deletions(-) create mode 100644 src/inspector/key.zig create mode 100644 src/inspector/main.zig diff --git a/src/Inspector.zig b/src/Inspector.zig index 618a8d610..ffd87876d 100644 --- a/src/Inspector.zig +++ b/src/Inspector.zig @@ -11,14 +11,21 @@ const CircBuf = @import("circ_buf.zig").CircBuf; const Surface = @import("Surface.zig"); const input = @import("input.zig"); const terminal = @import("terminal/main.zig"); +const inspector = @import("inspector/main.zig"); /// The window names. These are used with docking so we need to have access. const window_cell = "Cell"; const window_modes = "Modes"; +const window_keyboard = "Keyboard"; const window_screen = "Screen"; const window_size = "Surface Info"; const window_imgui_demo = "Dear ImGui Demo"; +/// Unique ID system. This is used to generate unique IDs for Dear ImGui +/// widgets. Overflow to reset to 0 is fine. IDs should still be prefixed +/// by type to avoid collisions but its never going to happen. +next_id: usize = 123456789, + /// The surface that we're inspecting. surface: *Surface, @@ -41,7 +48,7 @@ mouse: struct { cell: CellInspect = .{ .idle = {} }, /// The list of keyboard events -key_events: CircBuf(KeyEvent, undefined), +key_events: inspector.key.EventRing, const CellInspect = union(enum) { /// Idle, no cell inspection is requested @@ -68,23 +75,6 @@ const CellInspect = union(enum) { } }; -pub const KeyEvent = struct { - /// The input event. - event: input.KeyEvent, - - /// The binding that was triggered as a result of this event. - binding: ?input.Binding.Action = null, - - /// The data sent to the pty as a result of this keyboard event. - /// This is allocated using the inspector allocator. - pty: []const u8 = "", - - pub fn deinit(self: *const KeyEvent, alloc: Allocator) void { - if (self.event.utf8.len > 0) alloc.free(self.event.utf8); - if (self.pty.len > 0) alloc.free(self.pty); - } -}; - /// Setup the ImGui state. This requires an ImGui context to be set. pub fn setup() void { const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO(); @@ -120,7 +110,7 @@ pub fn setup() void { } pub fn init(surface: *Surface) !Inspector { - var key_buf = try CircBuf(KeyEvent, undefined).init(surface.alloc, 2); + var key_buf = try inspector.key.EventRing.init(surface.alloc, 2); errdefer key_buf.deinit(surface.alloc); return .{ @@ -138,15 +128,18 @@ pub fn deinit(self: *Inspector) void { } /// Record a keyboard event. -pub fn recordKeyEvent(self: *Inspector, ev: KeyEvent) !void { - const max_capacity = 1024; +pub fn recordKeyEvent(self: *Inspector, ev: inspector.key.Event) !void { + const max_capacity = 50; self.key_events.append(ev) catch |err| switch (err) { error.OutOfMemory => if (self.key_events.capacity() < max_capacity) { // We're out of memory, but we can allocate to our capacity. const new_capacity = @min(self.key_events.capacity() * 2, max_capacity); try self.key_events.resize(self.surface.alloc, new_capacity); try self.key_events.append(ev); - } else return err, + } else { + self.key_events.deleteOldest(1); + try self.key_events.append(ev); + }, else => return err, }; @@ -168,6 +161,7 @@ pub fn render(self: *Inspector) void { defer self.surface.renderer_state.mutex.unlock(); self.renderScreenWindow(); self.renderModesWindow(); + self.renderKeyboardWindow(); self.renderCellWindow(); self.renderSizeWindow(); } @@ -217,6 +211,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_screen, dock_id.left); cimgui.c.igDockBuilderDockWindow(window_imgui_demo, dock_id.left); cimgui.c.igDockBuilderDockWindow(window_size, dock_id.right); @@ -915,3 +910,84 @@ fn renderCellWindow(self: *Inspector) void { cimgui.c.igTextDisabled("(Any styles not shown are not currently set)"); } + +fn renderKeyboardWindow(self: *Inspector) void { + // Start our window. If we're collapsed we do nothing. + defer cimgui.c.igEnd(); + if (!cimgui.c.igBegin( + window_keyboard, + null, + cimgui.c.ImGuiWindowFlags_NoFocusOnAppearing, + )) return; + + list: { + if (self.key_events.empty()) { + cimgui.c.igText("No recorded key events. Press a key with the " ++ + "terminal focused to record it."); + break :list; + } + + _ = cimgui.c.igBeginTable( + "table_key_events", + 1, + //cimgui.c.ImGuiTableFlags_ScrollY | + cimgui.c.ImGuiTableFlags_RowBg | + cimgui.c.ImGuiTableFlags_Borders, + .{ .x = 0, .y = 0 }, + 0, + ); + defer cimgui.c.igEndTable(); + + var it = self.key_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); + + var buf: [1024]u8 = undefined; + const label = ev.label(&buf) catch "Key Event"; + _ = cimgui.c.igSelectable_BoolPtr( + label.ptr, + &ev.imgui_state.selected, + cimgui.c.ImGuiSelectableFlags_None, + .{ .x = 0, .y = 0 }, + ); + + if (!ev.imgui_state.selected) continue; + + _ = cimgui.c.igBeginTable( + "##event", + 2, + cimgui.c.ImGuiTableFlags_None, + .{ .x = 0, .y = 0 }, + 0, + ); + defer cimgui.c.igEndTable(); + + { + cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + _ = cimgui.c.igTableSetColumnIndex(0); + cimgui.c.igText("Action"); + _ = cimgui.c.igTableSetColumnIndex(1); + cimgui.c.igText("%s", @tagName(ev.event.action).ptr); + } + { + cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + _ = cimgui.c.igTableSetColumnIndex(0); + cimgui.c.igText("Key"); + _ = cimgui.c.igTableSetColumnIndex(1); + cimgui.c.igText("%s", @tagName(ev.event.key).ptr); + } + { + cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + _ = cimgui.c.igTableSetColumnIndex(0); + cimgui.c.igText("Physical Key"); + _ = cimgui.c.igTableSetColumnIndex(1); + cimgui.c.igText("%s", @tagName(ev.event.physical_key).ptr); + } + } + } // table +} diff --git a/src/Surface.zig b/src/Surface.zig index f92b34663..466cfffe4 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -34,6 +34,7 @@ const configpkg = @import("config.zig"); const input = @import("input.zig"); const App = @import("App.zig"); const internal_os = @import("os/main.zig"); +const inspector = @import("inspector/main.zig"); const log = std.log.scoped(.surface); @@ -598,7 +599,7 @@ pub fn activateInspector(self: *Surface) !void { /// Deactivate the inspector and stop collecting any information. pub fn deactivateInspector(self: *Surface) void { - const inspector = self.inspector orelse return; + const insp = self.inspector orelse return; // Remove the inspector from the render state { @@ -613,8 +614,8 @@ pub fn deactivateInspector(self: *Surface) void { _ = self.io_thread.mailbox.push(.{ .inspector = false }, .{ .forever = {} }); // Deinit the inspector - inspector.deinit(); - self.alloc.destroy(inspector); + insp.deinit(); + self.alloc.destroy(insp); self.inspector = null; } @@ -1005,7 +1006,7 @@ pub fn keyCallback( // log.debug("keyCallback event={}", .{event}); // Setup our inspector event if we have an inspector. - var insp_ev: ?Inspector.KeyEvent = if (self.inspector != null) ev: { + var insp_ev: ?inspector.key.Event = if (self.inspector != null) ev: { var copy = event; copy.utf8 = ""; if (event.utf8.len > 0) copy.utf8 = try self.alloc.dupe(u8, event.utf8); diff --git a/src/inspector/key.zig b/src/inspector/key.zig new file mode 100644 index 000000000..97596bddb --- /dev/null +++ b/src/inspector/key.zig @@ -0,0 +1,71 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const input = @import("../input.zig"); +const CircBuf = @import("../circ_buf.zig").CircBuf; + +/// Circular buffer of key events. +pub const EventRing = CircBuf(Event, undefined); + +/// Represents a recorded keyboard event. +pub const Event = struct { + /// The input event. + event: input.KeyEvent, + + /// The binding that was triggered as a result of this event. + binding: ?input.Binding.Action = null, + + /// The data sent to the pty as a result of this keyboard event. + /// This is allocated using the inspector allocator. + pty: []const u8 = "", + + /// State for the inspector GUI. Do not set this unless you're the inspector. + imgui_state: struct { + selected: bool = false, + } = .{}, + + pub fn init(alloc: Allocator, event: input.KeyEvent) !Event { + var copy = event; + copy.utf8 = ""; + if (event.utf8.len > 0) copy.utf8 = try alloc.dupe(u8, event.utf8); + return .{ .event = copy }; + } + + pub fn deinit(self: *const Event, alloc: Allocator) void { + if (self.event.utf8.len > 0) alloc.free(self.event.utf8); + if (self.pty.len > 0) alloc.free(self.pty); + } + + /// Returns a label that can be used for this event. This is null-terminated + /// so it can be easily used with C APIs. + pub fn label(self: *const Event, buf: []u8) ![:0]const u8 { + var buf_stream = std.io.fixedBufferStream(buf); + const writer = buf_stream.writer(); + + switch (self.event.action) { + .press => try writer.writeAll("Press: "), + .release => try writer.writeAll("Release: "), + .repeat => try writer.writeAll("Repeat: "), + } + + if (self.event.mods.shift) try writer.writeAll("Shift+"); + if (self.event.mods.ctrl) try writer.writeAll("Ctrl+"); + if (self.event.mods.alt) try writer.writeAll("Alt+"); + if (self.event.mods.super) try writer.writeAll("Super+"); + try writer.writeAll(@tagName(self.event.key)); + + // Null-terminator + try writer.writeByte(0); + return buf[0..(buf_stream.getWritten().len - 1) :0]; + } +}; + +test "event string" { + const testing = std.testing; + const alloc = testing.allocator; + + var event = try Event.init(alloc, .{ .key = .a }); + defer event.deinit(alloc); + + var buf: [1024]u8 = undefined; + try testing.expectEqualStrings("Press: a", try event.label(&buf)); +} diff --git a/src/inspector/main.zig b/src/inspector/main.zig new file mode 100644 index 000000000..628f543e1 --- /dev/null +++ b/src/inspector/main.zig @@ -0,0 +1,5 @@ +pub const key = @import("key.zig"); + +test { + @import("std").testing.refAllDecls(@This()); +} diff --git a/src/main.zig b/src/main.zig index 4eadcefad..af669de92 100644 --- a/src/main.zig +++ b/src/main.zig @@ -295,6 +295,7 @@ test { // Libraries _ = @import("segmented_pool.zig"); + _ = @import("inspector/main.zig"); _ = @import("terminal/main.zig"); _ = @import("terminfo/main.zig");