inspector: key events

This commit is contained in:
Mitchell Hashimoto
2026-01-29 13:48:44 -08:00
parent 9056fa7fd1
commit b2c8cdbc90
6 changed files with 490 additions and 331 deletions

View File

@@ -10,15 +10,12 @@ const builtin = @import("builtin");
const cimgui = @import("dcimgui");
const Surface = @import("../Surface.zig");
const font = @import("../font/main.zig");
const input = @import("../input.zig");
const renderer = @import("../renderer.zig");
const terminal = @import("../terminal/main.zig");
const inspector = @import("main.zig");
const widgets = @import("widgets.zig");
/// The window names. These are used with docking so we need to have access.
const window_cell = "Cell";
const window_keyboard = "Keyboard";
const window_termio = "Terminal IO";
const window_imgui_demo = "Dear ImGui Demo";
@@ -36,9 +33,6 @@ mouse: widgets.surface.Mouse = .{},
/// A selected cell.
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,
@@ -53,7 +47,7 @@ need_scroll_to_selected: bool = false,
is_keyboard_selection: bool = false,
// ImGui state
gui: widgets.surface.Inspector = .empty,
gui: widgets.surface.Inspector,
/// Enum representing keyboard navigation actions
const KeyAction = enum {
@@ -152,32 +146,27 @@ pub fn setup() void {
}
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);
var vt_handler = inspector.termio.VTHandler.init(surface);
errdefer vt_handler.deinit();
var gui: widgets.surface.Inspector = try .init(surface.alloc);
errdefer gui.deinit(surface.alloc);
return .{
.surface = surface,
.key_events = key_buf,
.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.key_events.iterator(.forward);
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);
@@ -190,17 +179,19 @@ pub fn deinit(self: *Inspector) void {
/// Record a keyboard event.
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) {
const events: *widgets.key.EventRing = &self.gui.key_stream.events;
events.append(ev) catch |err| switch (err) {
error.OutOfMemory => if (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);
const new_capacity = @min(events.capacity() * 2, max_capacity);
try events.resize(self.surface.alloc, new_capacity);
try events.append(ev);
} else {
var it = self.key_events.iterator(.forward);
var it = events.iterator(.forward);
if (it.next()) |old_ev| old_ev.deinit(self.surface.alloc);
self.key_events.deleteOldest(1);
try self.key_events.append(ev);
events.deleteOldest(1);
try events.append(ev);
},
else => return err,
@@ -214,7 +205,10 @@ pub fn recordPtyRead(self: *Inspector, data: []const u8) !void {
/// Render the frame.
pub fn render(self: *Inspector) void {
self.gui.draw(self.surface, self.mouse);
self.gui.draw(
self.surface,
self.mouse,
);
if (true) return;
const dock_id = cimgui.c.ImGui_DockSpaceOverViewport();
@@ -231,7 +225,6 @@ pub fn render(self: *Inspector) void {
.surface = self.surface,
.mouse = self.mouse,
});
self.renderKeyboardWindow();
self.renderTermioWindow();
self.renderCellWindow();
}
@@ -262,7 +255,6 @@ fn setupLayout(self: *Inspector, dock_id_main: cimgui.c.ImGuiID) void {
// Surface is docked first so it appears as the first tab.
cimgui.ImGui_DockBuilderDockWindow(inspector.surface.Window.name, dock_id_main);
cimgui.ImGui_DockBuilderDockWindow(inspector.terminal.Window.name, dock_id_main);
cimgui.ImGui_DockBuilderDockWindow(window_keyboard, dock_id_main);
cimgui.ImGui_DockBuilderDockWindow(window_termio, dock_id_main);
cimgui.ImGui_DockBuilderDockWindow(window_cell, dock_id_main);
cimgui.ImGui_DockBuilderDockWindow(window_imgui_demo, dock_id_main);
@@ -331,63 +323,6 @@ fn renderCellWindow(self: *Inspector) void {
);
}
fn renderKeyboardWindow(self: *Inspector) void {
// Start our window. If we're collapsed we do nothing.
defer cimgui.c.ImGui_End();
if (!cimgui.c.ImGui_Begin(
window_keyboard,
null,
cimgui.c.ImGuiWindowFlags_NoFocusOnAppearing,
)) return;
list: {
if (self.key_events.empty()) {
cimgui.c.ImGui_Text("No recorded key events. Press a key with the " ++
"terminal focused to record it.");
break :list;
}
if (cimgui.c.ImGui_Button("Clear")) {
var it = self.key_events.iterator(.forward);
while (it.next()) |v| v.deinit(self.surface.alloc);
self.key_events.clear();
self.vt_stream.handler.current_seq = 1;
}
cimgui.c.ImGui_Separator();
_ = cimgui.c.ImGui_BeginTable(
"table_key_events",
1,
//cimgui.c.ImGuiTableFlags_ScrollY |
cimgui.c.ImGuiTableFlags_RowBg |
cimgui.c.ImGuiTableFlags_Borders,
);
defer cimgui.c.ImGui_EndTable();
var it = self.key_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_TableSetColumnIndex(0);
var buf: [1024]u8 = undefined;
const label = ev.label(&buf) catch "Key Event";
_ = cimgui.c.ImGui_SelectableBoolPtr(
label.ptr,
&ev.imgui_state.selected,
cimgui.c.ImGuiSelectableFlags_None,
);
if (!ev.imgui_state.selected) continue;
ev.render();
}
} // table
}
/// Helper function to check keyboard state and determine navigation action.
fn getKeyAction(self: *Inspector) KeyAction {
_ = self;

View File

@@ -1,240 +0,0 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const input = @import("../input.zig");
const CircBuf = @import("../datastruct/main.zig").CircBuf;
const cimgui = @import("dcimgui");
/// 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.
/// Multiple bindings are possible if they are chained.
binding: []const input.Binding.Action = &.{},
/// 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 {
alloc.free(self.binding);
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+");
// Write our key. If we have an invalid key we attempt to write
// the utf8 associated with it if we have it to handle non-ascii.
try writer.writeAll(switch (self.event.key) {
.unidentified => if (self.event.utf8.len > 0) self.event.utf8 else @tagName(self.event.key),
else => @tagName(self.event.key),
});
// Deadkey
if (self.event.composing) try writer.writeAll(" (composing)");
// Null-terminator
try writer.writeByte(0);
return buf[0..(buf_stream.getWritten().len - 1) :0];
}
/// Render this event in the inspector GUI.
pub fn render(self: *const Event) void {
_ = cimgui.c.ImGui_BeginTable(
"##event",
2,
cimgui.c.ImGuiTableFlags_None,
);
defer cimgui.c.ImGui_EndTable();
if (self.binding.len > 0) {
cimgui.c.ImGui_TableNextRow();
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
cimgui.c.ImGui_Text("Triggered Binding");
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
const height: f32 = height: {
const item_count: f32 = @floatFromInt(@min(self.binding.len, 5));
const padding = cimgui.c.ImGui_GetStyle().*.FramePadding.y * 2;
break :height cimgui.c.ImGui_GetTextLineHeightWithSpacing() * item_count + padding;
};
if (cimgui.c.ImGui_BeginListBox("##bindings", .{ .x = 0, .y = height })) {
defer cimgui.c.ImGui_EndListBox();
for (self.binding) |action| {
_ = cimgui.c.ImGui_SelectableEx(
@tagName(action).ptr,
false,
cimgui.c.ImGuiSelectableFlags_None,
.{ .x = 0, .y = 0 },
);
}
}
}
pty: {
cimgui.c.ImGui_TableNextRow();
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
cimgui.c.ImGui_Text("Encoding to Pty");
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
if (self.pty.len == 0) {
cimgui.c.ImGui_TextDisabled("(no data)");
break :pty;
}
self.renderPty() catch {
cimgui.c.ImGui_TextDisabled("(error rendering pty data)");
break :pty;
};
}
{
cimgui.c.ImGui_TableNextRow();
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
cimgui.c.ImGui_Text("Action");
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
cimgui.c.ImGui_Text("%s", @tagName(self.event.action).ptr);
}
{
cimgui.c.ImGui_TableNextRow();
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
cimgui.c.ImGui_Text("Key");
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
cimgui.c.ImGui_Text("%s", @tagName(self.event.key).ptr);
}
if (!self.event.mods.empty()) {
cimgui.c.ImGui_TableNextRow();
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
cimgui.c.ImGui_Text("Mods");
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
if (self.event.mods.shift) cimgui.c.ImGui_Text("shift ");
if (self.event.mods.ctrl) cimgui.c.ImGui_Text("ctrl ");
if (self.event.mods.alt) cimgui.c.ImGui_Text("alt ");
if (self.event.mods.super) cimgui.c.ImGui_Text("super ");
}
if (self.event.composing) {
cimgui.c.ImGui_TableNextRow();
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
cimgui.c.ImGui_Text("Composing");
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
cimgui.c.ImGui_Text("true");
}
utf8: {
cimgui.c.ImGui_TableNextRow();
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
cimgui.c.ImGui_Text("UTF-8");
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
if (self.event.utf8.len == 0) {
cimgui.c.ImGui_TextDisabled("(empty)");
break :utf8;
}
self.renderUtf8(self.event.utf8) catch {
cimgui.c.ImGui_TextDisabled("(error rendering utf-8)");
break :utf8;
};
}
}
fn renderUtf8(self: *const Event, utf8: []const u8) !void {
_ = self;
// Format the codepoint sequence
var buf: [1024]u8 = undefined;
var buf_stream = std.io.fixedBufferStream(&buf);
const writer = buf_stream.writer();
if (std.unicode.Utf8View.init(utf8)) |view| {
var it = view.iterator();
while (it.nextCodepoint()) |cp| {
try writer.print("U+{X} ", .{cp});
}
} else |_| {
try writer.writeAll("(invalid utf-8)");
}
try writer.writeByte(0);
// Render as a textbox
_ = cimgui.c.ImGui_InputText(
"##utf8",
&buf,
buf_stream.getWritten().len - 1,
cimgui.c.ImGuiInputTextFlags_ReadOnly,
);
}
fn renderPty(self: *const Event) !void {
// Format the codepoint sequence
var buf: [1024]u8 = undefined;
var buf_stream = std.io.fixedBufferStream(&buf);
const writer = buf_stream.writer();
for (self.pty) |byte| {
// Print ESC special because its so common
if (byte == 0x1B) {
try writer.writeAll("ESC ");
continue;
}
// Print ASCII as-is
if (byte > 0x20 and byte < 0x7F) {
try writer.writeByte(byte);
continue;
}
// Everything else as a hex byte
try writer.print("0x{X} ", .{byte});
}
try writer.writeByte(0);
// Render as a textbox
_ = cimgui.c.ImGui_InputText(
"##pty",
&buf,
buf_stream.getWritten().len - 1,
cimgui.c.ImGuiInputTextFlags_ReadOnly,
);
}
};
test "event string" {
const testing = std.testing;
const alloc = testing.allocator;
var event = try Event.init(alloc, .{ .key = .key_a });
defer event.deinit(alloc);
var buf: [1024]u8 = undefined;
try testing.expectEqualStrings("Press: key_a", try event.label(&buf));
}

View File

@@ -1,6 +1,6 @@
const std = @import("std");
pub const cell = @import("cell.zig");
pub const key = @import("key.zig");
pub const key = @import("widgets/key.zig");
pub const termio = @import("termio.zig");

View File

@@ -2,6 +2,7 @@ const cimgui = @import("dcimgui");
pub const page = @import("widgets/page.zig");
pub const pagelist = @import("widgets/pagelist.zig");
pub const key = @import("widgets/key.zig");
pub const screen = @import("widgets/screen.zig");
pub const style = @import("widgets/style.zig");
pub const surface = @import("widgets/surface.zig");

View File

@@ -0,0 +1,434 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const input = @import("../../input.zig");
const CircBuf = @import("../../datastruct/main.zig").CircBuf;
const cimgui = @import("dcimgui");
/// 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.
/// Multiple bindings are possible if they are chained.
binding: []const input.Binding.Action = &.{},
/// 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, ev: input.KeyEvent) !Event {
var copy = ev;
copy.utf8 = "";
if (ev.utf8.len > 0) copy.utf8 = try alloc.dupe(u8, ev.utf8);
return .{ .event = copy };
}
pub fn deinit(self: *const Event, alloc: Allocator) void {
alloc.free(self.binding);
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+");
// Write our key. If we have an invalid key we attempt to write
// the utf8 associated with it if we have it to handle non-ascii.
try writer.writeAll(switch (self.event.key) {
.unidentified => if (self.event.utf8.len > 0) self.event.utf8 else @tagName(self.event.key),
else => @tagName(self.event.key),
});
// Deadkey
if (self.event.composing) try writer.writeAll(" (composing)");
// Null-terminator
try writer.writeByte(0);
return buf[0..(buf_stream.getWritten().len - 1) :0];
}
/// Render this event in the inspector GUI.
pub fn render(self: *const Event) void {
_ = cimgui.c.ImGui_BeginTable(
"##event",
2,
cimgui.c.ImGuiTableFlags_None,
);
defer cimgui.c.ImGui_EndTable();
if (self.binding.len > 0) {
cimgui.c.ImGui_TableNextRow();
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
cimgui.c.ImGui_Text("Triggered Binding");
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
const height: f32 = height: {
const item_count: f32 = @floatFromInt(@min(self.binding.len, 5));
const padding = cimgui.c.ImGui_GetStyle().*.FramePadding.y * 2;
break :height cimgui.c.ImGui_GetTextLineHeightWithSpacing() * item_count + padding;
};
if (cimgui.c.ImGui_BeginListBox("##bindings", .{ .x = 0, .y = height })) {
defer cimgui.c.ImGui_EndListBox();
for (self.binding) |action| {
_ = cimgui.c.ImGui_SelectableEx(
@tagName(action).ptr,
false,
cimgui.c.ImGuiSelectableFlags_None,
.{ .x = 0, .y = 0 },
);
}
}
}
pty: {
cimgui.c.ImGui_TableNextRow();
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
cimgui.c.ImGui_Text("Encoding to Pty");
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
if (self.pty.len == 0) {
cimgui.c.ImGui_TextDisabled("(no data)");
break :pty;
}
self.renderPty() catch {
cimgui.c.ImGui_TextDisabled("(error rendering pty data)");
break :pty;
};
}
{
cimgui.c.ImGui_TableNextRow();
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
cimgui.c.ImGui_Text("Action");
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
cimgui.c.ImGui_Text("%s", @tagName(self.event.action).ptr);
}
{
cimgui.c.ImGui_TableNextRow();
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
cimgui.c.ImGui_Text("Key");
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
cimgui.c.ImGui_Text("%s", @tagName(self.event.key).ptr);
}
if (!self.event.mods.empty()) {
cimgui.c.ImGui_TableNextRow();
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
cimgui.c.ImGui_Text("Mods");
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
if (self.event.mods.shift) cimgui.c.ImGui_Text("shift ");
if (self.event.mods.ctrl) cimgui.c.ImGui_Text("ctrl ");
if (self.event.mods.alt) cimgui.c.ImGui_Text("alt ");
if (self.event.mods.super) cimgui.c.ImGui_Text("super ");
}
if (self.event.composing) {
cimgui.c.ImGui_TableNextRow();
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
cimgui.c.ImGui_Text("Composing");
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
cimgui.c.ImGui_Text("true");
}
utf8: {
cimgui.c.ImGui_TableNextRow();
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
cimgui.c.ImGui_Text("UTF-8");
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
if (self.event.utf8.len == 0) {
cimgui.c.ImGui_TextDisabled("(empty)");
break :utf8;
}
self.renderUtf8(self.event.utf8) catch {
cimgui.c.ImGui_TextDisabled("(error rendering utf-8)");
break :utf8;
};
}
}
fn renderUtf8(self: *const Event, utf8: []const u8) !void {
_ = self;
// Format the codepoint sequence
var buf: [1024]u8 = undefined;
var buf_stream = std.io.fixedBufferStream(&buf);
const writer = buf_stream.writer();
if (std.unicode.Utf8View.init(utf8)) |view| {
var it = view.iterator();
while (it.nextCodepoint()) |cp| {
try writer.print("U+{X} ", .{cp});
}
} else |_| {
try writer.writeAll("(invalid utf-8)");
}
try writer.writeByte(0);
// Render as a textbox
_ = cimgui.c.ImGui_InputText(
"##utf8",
&buf,
buf_stream.getWritten().len - 1,
cimgui.c.ImGuiInputTextFlags_ReadOnly,
);
}
fn renderPty(self: *const Event) !void {
// Format the codepoint sequence
var buf: [1024]u8 = undefined;
var buf_stream = std.io.fixedBufferStream(&buf);
const writer = buf_stream.writer();
for (self.pty) |byte| {
// Print ESC special because its so common
if (byte == 0x1B) {
try writer.writeAll("ESC ");
continue;
}
// Print ASCII as-is
if (byte > 0x20 and byte < 0x7F) {
try writer.writeByte(byte);
continue;
}
// Everything else as a hex byte
try writer.print("0x{X} ", .{byte});
}
try writer.writeByte(0);
// Render as a textbox
_ = cimgui.c.ImGui_InputText(
"##pty",
&buf,
buf_stream.getWritten().len - 1,
cimgui.c.ImGuiInputTextFlags_ReadOnly,
);
}
};
fn modsTooltip(
mods: *const input.Mods,
buf: []u8,
) ![:0]const u8 {
var stream = std.io.fixedBufferStream(buf);
const writer = stream.writer();
var first = true;
if (mods.shift) {
try writer.writeAll("Shift");
first = false;
}
if (mods.ctrl) {
if (!first) try writer.writeAll("+");
try writer.writeAll("Ctrl");
first = false;
}
if (mods.alt) {
if (!first) try writer.writeAll("+");
try writer.writeAll("Alt");
first = false;
}
if (mods.super) {
if (!first) try writer.writeAll("+");
try writer.writeAll("Super");
}
try writer.writeByte(0);
const written = stream.getWritten();
return written[0 .. written.len - 1 :0];
}
/// Keyboard event stream inspector widget.
pub const Stream = struct {
events: EventRing,
pub fn init(alloc: Allocator) !Stream {
var events: EventRing = try .init(alloc, 2);
errdefer events.deinit(alloc);
return .{ .events = events };
}
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);
}
pub fn draw(
self: *Stream,
open: bool,
alloc: Allocator,
) void {
if (!open) return;
if (self.events.empty()) {
cimgui.c.ImGui_Text("No recorded key events. Press a key with the " ++
"terminal focused to record it.");
return;
}
if (cimgui.c.ImGui_Button("Clear")) {
var it = self.events.iterator(.forward);
while (it.next()) |v| v.deinit(alloc);
self.events.clear();
}
cimgui.c.ImGui_Separator();
const table_flags = cimgui.c.ImGuiTableFlags_RowBg |
cimgui.c.ImGuiTableFlags_Borders |
cimgui.c.ImGuiTableFlags_Resizable |
cimgui.c.ImGuiTableFlags_ScrollY |
cimgui.c.ImGuiTableFlags_SizingFixedFit;
if (!cimgui.c.ImGui_BeginTable("table_key_events", 6, table_flags)) return;
defer cimgui.c.ImGui_EndTable();
cimgui.c.ImGui_TableSetupScrollFreeze(0, 1);
cimgui.c.ImGui_TableSetupColumnEx("Action", cimgui.c.ImGuiTableColumnFlags_WidthFixed, 60, 0);
cimgui.c.ImGui_TableSetupColumnEx("Key", cimgui.c.ImGuiTableColumnFlags_WidthFixed, 100, 0);
cimgui.c.ImGui_TableSetupColumnEx("Mods", cimgui.c.ImGuiTableColumnFlags_WidthFixed, 150, 0);
cimgui.c.ImGui_TableSetupColumnEx("UTF-8", cimgui.c.ImGuiTableColumnFlags_WidthFixed, 80, 0);
cimgui.c.ImGui_TableSetupColumnEx("PTY Encoding", cimgui.c.ImGuiTableColumnFlags_WidthStretch, 0, 0);
cimgui.c.ImGui_TableSetupColumnEx("Binding", cimgui.c.ImGuiTableColumnFlags_WidthStretch, 0, 0);
cimgui.c.ImGui_TableHeadersRow();
var it = self.events.iterator(.reverse);
while (it.next()) |ev| {
cimgui.c.ImGui_PushIDPtr(ev);
defer cimgui.c.ImGui_PopID();
cimgui.c.ImGui_TableNextRow();
// Action
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
cimgui.c.ImGui_Text("%s", @tagName(ev.event.action).ptr);
// Key
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
const key_name = switch (ev.event.key) {
.unidentified => if (ev.event.utf8.len > 0) ev.event.utf8 else @tagName(ev.event.key),
else => @tagName(ev.event.key),
};
cimgui.c.ImGui_Text("%s", key_name.ptr);
// Mods
_ = cimgui.c.ImGui_TableSetColumnIndex(2);
mods: {
if (ev.event.mods.empty()) {
cimgui.c.ImGui_TextDisabled("-");
break :mods;
}
var any_hovered = false;
if (ev.event.mods.shift) {
_ = cimgui.c.ImGui_SmallButton("S");
any_hovered = any_hovered or cimgui.c.ImGui_IsItemHovered(cimgui.c.ImGuiHoveredFlags_None);
cimgui.c.ImGui_SameLine();
}
if (ev.event.mods.ctrl) {
_ = cimgui.c.ImGui_SmallButton("C");
any_hovered = any_hovered or cimgui.c.ImGui_IsItemHovered(cimgui.c.ImGuiHoveredFlags_None);
cimgui.c.ImGui_SameLine();
}
if (ev.event.mods.alt) {
_ = cimgui.c.ImGui_SmallButton("A");
any_hovered = any_hovered or cimgui.c.ImGui_IsItemHovered(cimgui.c.ImGuiHoveredFlags_None);
cimgui.c.ImGui_SameLine();
}
if (ev.event.mods.super) {
_ = cimgui.c.ImGui_SmallButton("M");
any_hovered = any_hovered or cimgui.c.ImGui_IsItemHovered(cimgui.c.ImGuiHoveredFlags_None);
cimgui.c.ImGui_SameLine();
}
cimgui.c.ImGui_NewLine();
if (any_hovered) tooltip: {
var tooltip_buf: [64]u8 = undefined;
const tooltip = modsTooltip(
&ev.event.mods,
&tooltip_buf,
) catch break :tooltip;
cimgui.c.ImGui_SetTooltip("%s", tooltip.ptr);
}
}
// UTF-8
_ = cimgui.c.ImGui_TableSetColumnIndex(3);
if (ev.event.utf8.len == 0) {
cimgui.c.ImGui_TextDisabled("-");
} else {
var utf8_buf: [128]u8 = undefined;
var utf8_stream = std.io.fixedBufferStream(&utf8_buf);
const utf8_writer = utf8_stream.writer();
if (std.unicode.Utf8View.init(ev.event.utf8)) |view| {
var utf8_it = view.iterator();
while (utf8_it.nextCodepoint()) |cp| {
utf8_writer.print("U+{X} ", .{cp}) catch break;
}
} else |_| {
utf8_writer.writeAll("?") catch {};
}
utf8_writer.writeByte(0) catch {};
cimgui.c.ImGui_Text("%s", &utf8_buf);
}
// PTY
_ = cimgui.c.ImGui_TableSetColumnIndex(4);
if (ev.pty.len == 0) {
cimgui.c.ImGui_TextDisabled("-");
} else {
var pty_buf: [256]u8 = undefined;
var pty_stream = std.io.fixedBufferStream(&pty_buf);
const pty_writer = pty_stream.writer();
for (ev.pty) |byte| {
if (byte == 0x1B) {
pty_writer.writeAll("ESC ") catch break;
} else if (byte > 0x20 and byte < 0x7F) {
pty_writer.writeByte(byte) catch break;
} else {
pty_writer.print("0x{X} ", .{byte}) catch break;
}
}
pty_writer.writeByte(0) catch {};
cimgui.c.ImGui_Text("%s", &pty_buf);
}
// Binding
_ = cimgui.c.ImGui_TableSetColumnIndex(5);
if (ev.binding.len == 0) {
cimgui.c.ImGui_TextDisabled("-");
} else {
var binding_buf: [256]u8 = undefined;
var binding_stream = std.io.fixedBufferStream(&binding_buf);
const binding_writer = binding_stream.writer();
for (ev.binding, 0..) |action, i| {
if (i > 0) binding_writer.writeAll(", ") catch break;
binding_writer.writeAll(@tagName(action)) catch break;
}
binding_writer.writeByte(0) catch {};
cimgui.c.ImGui_Text("%s", &binding_buf);
}
}
}
};

View File

@@ -3,6 +3,7 @@ const builtin = @import("builtin");
const assert = @import("../../quirks.zig").inlineAssert;
const Allocator = std.mem.Allocator;
const cimgui = @import("dcimgui");
const inspector = @import("../main.zig");
const widgets = @import("../widgets.zig");
const input = @import("../../input.zig");
const renderer = @import("../../renderer.zig");
@@ -11,20 +12,33 @@ const Surface = @import("../../Surface.zig");
/// This is discovered via the hardcoded string in the ImGui demo window.
const window_imgui_demo = "Dear ImGui Demo";
const window_keyboard = "Keyboard";
const window_terminal = "Terminal";
const window_surface = "Surface";
pub const Inspector = struct {
/// Internal GUI state
surface_info: Info,
key_stream: widgets.key.Stream,
terminal_info: widgets.terminal.Info,
pub const empty: Inspector = .{
.surface_info = .empty,
.terminal_info = .empty,
};
pub fn init(alloc: Allocator) !Inspector {
return .{
.surface_info = .empty,
.key_stream = try .init(alloc),
.terminal_info = .empty,
};
}
pub fn draw(self: *Inspector, surface: *const Surface, mouse: Mouse) void {
pub fn deinit(self: *Inspector, alloc: Allocator) void {
self.key_stream.deinit(alloc);
}
pub fn draw(
self: *Inspector,
surface: *const Surface,
mouse: Mouse,
) void {
// Create our dockspace first. If we had to setup our dockspace,
// then it is a first render.
const dockspace_id = cimgui.c.ImGui_GetID("Main Dockspace");
@@ -68,6 +82,20 @@ pub const Inspector = struct {
mouse,
);
}
// Keyboard info window
{
const open = cimgui.c.ImGui_Begin(
window_keyboard,
null,
cimgui.c.ImGuiWindowFlags_NoFocusOnAppearing,
);
defer cimgui.c.ImGui_End();
self.key_stream.draw(
open,
surface.alloc,
);
}
}
if (first_render) {
@@ -107,6 +135,7 @@ pub const Inspector = struct {
const dock_id_main: cimgui.c.ImGuiID = dockspace_id;
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_imgui_demo, dock_id_main);
cimgui.ImGui_DockBuilderFinish(dockspace_id);
}