const std = @import("std"); const Allocator = std.mem.Allocator; const input = @import("../input.zig"); const CircBuf = @import("../datastruct/main.zig").CircBuf; const cimgui = @import("cimgui"); /// 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+"); // 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) { .invalid => if (self.event.utf8.len > 0) self.event.utf8 else @tagName(.invalid), 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.igBeginTable( "##event", 2, cimgui.c.ImGuiTableFlags_None, .{ .x = 0, .y = 0 }, 0, ); defer cimgui.c.igEndTable(); if (self.binding) |binding| { cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); _ = cimgui.c.igTableSetColumnIndex(0); cimgui.c.igText("Triggered Binding"); _ = cimgui.c.igTableSetColumnIndex(1); cimgui.c.igText("%s", @tagName(binding).ptr); } pty: { cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); _ = cimgui.c.igTableSetColumnIndex(0); cimgui.c.igText("Encoding to Pty"); _ = cimgui.c.igTableSetColumnIndex(1); if (self.pty.len == 0) { cimgui.c.igTextDisabled("(no data)"); break :pty; } self.renderPty() catch { cimgui.c.igTextDisabled("(error rendering pty data)"); break :pty; }; } { 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(self.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(self.event.key).ptr); } if (self.event.physical_key != self.event.key) { 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(self.event.physical_key).ptr); } if (!self.event.mods.empty()) { cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); _ = cimgui.c.igTableSetColumnIndex(0); cimgui.c.igText("Mods"); _ = cimgui.c.igTableSetColumnIndex(1); if (self.event.mods.shift) cimgui.c.igText("shift "); if (self.event.mods.ctrl) cimgui.c.igText("ctrl "); if (self.event.mods.alt) cimgui.c.igText("alt "); if (self.event.mods.super) cimgui.c.igText("super "); } if (self.event.composing) { cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); _ = cimgui.c.igTableSetColumnIndex(0); cimgui.c.igText("Composing"); _ = cimgui.c.igTableSetColumnIndex(1); cimgui.c.igText("true"); } utf8: { cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); _ = cimgui.c.igTableSetColumnIndex(0); cimgui.c.igText("UTF-8"); _ = cimgui.c.igTableSetColumnIndex(1); if (self.event.utf8.len == 0) { cimgui.c.igTextDisabled("(empty)"); break :utf8; } self.renderUtf8(self.event.utf8) catch { cimgui.c.igTextDisabled("(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.igInputText( "##utf8", &buf, buf_stream.getWritten().len - 1, cimgui.c.ImGuiInputTextFlags_ReadOnly, null, null, ); } 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.igInputText( "##pty", &buf, buf_stream.getWritten().len - 1, cimgui.c.ImGuiInputTextFlags_ReadOnly, null, null, ); } }; 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)); }