mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-15 00:06:09 +03:00
inspector: render basic key inspector
This commit is contained in:
@ -11,14 +11,21 @@ const CircBuf = @import("circ_buf.zig").CircBuf;
|
|||||||
const Surface = @import("Surface.zig");
|
const Surface = @import("Surface.zig");
|
||||||
const input = @import("input.zig");
|
const input = @import("input.zig");
|
||||||
const terminal = @import("terminal/main.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.
|
/// The window names. These are used with docking so we need to have access.
|
||||||
const window_cell = "Cell";
|
const window_cell = "Cell";
|
||||||
const window_modes = "Modes";
|
const window_modes = "Modes";
|
||||||
|
const window_keyboard = "Keyboard";
|
||||||
const window_screen = "Screen";
|
const window_screen = "Screen";
|
||||||
const window_size = "Surface Info";
|
const window_size = "Surface Info";
|
||||||
const window_imgui_demo = "Dear ImGui Demo";
|
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.
|
/// The surface that we're inspecting.
|
||||||
surface: *Surface,
|
surface: *Surface,
|
||||||
|
|
||||||
@ -41,7 +48,7 @@ mouse: struct {
|
|||||||
cell: CellInspect = .{ .idle = {} },
|
cell: CellInspect = .{ .idle = {} },
|
||||||
|
|
||||||
/// The list of keyboard events
|
/// The list of keyboard events
|
||||||
key_events: CircBuf(KeyEvent, undefined),
|
key_events: inspector.key.EventRing,
|
||||||
|
|
||||||
const CellInspect = union(enum) {
|
const CellInspect = union(enum) {
|
||||||
/// Idle, no cell inspection is requested
|
/// 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.
|
/// Setup the ImGui state. This requires an ImGui context to be set.
|
||||||
pub fn setup() void {
|
pub fn setup() void {
|
||||||
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
|
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
|
||||||
@ -120,7 +110,7 @@ pub fn setup() void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn init(surface: *Surface) !Inspector {
|
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);
|
errdefer key_buf.deinit(surface.alloc);
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
@ -138,15 +128,18 @@ pub fn deinit(self: *Inspector) void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Record a keyboard event.
|
/// Record a keyboard event.
|
||||||
pub fn recordKeyEvent(self: *Inspector, ev: KeyEvent) !void {
|
pub fn recordKeyEvent(self: *Inspector, ev: inspector.key.Event) !void {
|
||||||
const max_capacity = 1024;
|
const max_capacity = 50;
|
||||||
self.key_events.append(ev) catch |err| switch (err) {
|
self.key_events.append(ev) catch |err| switch (err) {
|
||||||
error.OutOfMemory => if (self.key_events.capacity() < max_capacity) {
|
error.OutOfMemory => if (self.key_events.capacity() < max_capacity) {
|
||||||
// We're out of memory, but we can allocate to our capacity.
|
// We're out of memory, but we can allocate to our capacity.
|
||||||
const new_capacity = @min(self.key_events.capacity() * 2, max_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.resize(self.surface.alloc, new_capacity);
|
||||||
try self.key_events.append(ev);
|
try self.key_events.append(ev);
|
||||||
} else return err,
|
} else {
|
||||||
|
self.key_events.deleteOldest(1);
|
||||||
|
try self.key_events.append(ev);
|
||||||
|
},
|
||||||
|
|
||||||
else => return err,
|
else => return err,
|
||||||
};
|
};
|
||||||
@ -168,6 +161,7 @@ pub fn render(self: *Inspector) void {
|
|||||||
defer self.surface.renderer_state.mutex.unlock();
|
defer self.surface.renderer_state.mutex.unlock();
|
||||||
self.renderScreenWindow();
|
self.renderScreenWindow();
|
||||||
self.renderModesWindow();
|
self.renderModesWindow();
|
||||||
|
self.renderKeyboardWindow();
|
||||||
self.renderCellWindow();
|
self.renderCellWindow();
|
||||||
self.renderSizeWindow();
|
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_cell, dock_id.left);
|
||||||
cimgui.c.igDockBuilderDockWindow(window_modes, 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_screen, dock_id.left);
|
||||||
cimgui.c.igDockBuilderDockWindow(window_imgui_demo, dock_id.left);
|
cimgui.c.igDockBuilderDockWindow(window_imgui_demo, dock_id.left);
|
||||||
cimgui.c.igDockBuilderDockWindow(window_size, dock_id.right);
|
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)");
|
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
|
||||||
|
}
|
||||||
|
@ -34,6 +34,7 @@ const configpkg = @import("config.zig");
|
|||||||
const input = @import("input.zig");
|
const input = @import("input.zig");
|
||||||
const App = @import("App.zig");
|
const App = @import("App.zig");
|
||||||
const internal_os = @import("os/main.zig");
|
const internal_os = @import("os/main.zig");
|
||||||
|
const inspector = @import("inspector/main.zig");
|
||||||
|
|
||||||
const log = std.log.scoped(.surface);
|
const log = std.log.scoped(.surface);
|
||||||
|
|
||||||
@ -598,7 +599,7 @@ pub fn activateInspector(self: *Surface) !void {
|
|||||||
|
|
||||||
/// Deactivate the inspector and stop collecting any information.
|
/// Deactivate the inspector and stop collecting any information.
|
||||||
pub fn deactivateInspector(self: *Surface) void {
|
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
|
// 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 = {} });
|
_ = self.io_thread.mailbox.push(.{ .inspector = false }, .{ .forever = {} });
|
||||||
|
|
||||||
// Deinit the inspector
|
// Deinit the inspector
|
||||||
inspector.deinit();
|
insp.deinit();
|
||||||
self.alloc.destroy(inspector);
|
self.alloc.destroy(insp);
|
||||||
self.inspector = null;
|
self.inspector = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1005,7 +1006,7 @@ pub fn keyCallback(
|
|||||||
// log.debug("keyCallback event={}", .{event});
|
// log.debug("keyCallback event={}", .{event});
|
||||||
|
|
||||||
// Setup our inspector event if we have an inspector.
|
// 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;
|
var copy = event;
|
||||||
copy.utf8 = "";
|
copy.utf8 = "";
|
||||||
if (event.utf8.len > 0) copy.utf8 = try self.alloc.dupe(u8, event.utf8);
|
if (event.utf8.len > 0) copy.utf8 = try self.alloc.dupe(u8, event.utf8);
|
||||||
|
71
src/inspector/key.zig
Normal file
71
src/inspector/key.zig
Normal file
@ -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));
|
||||||
|
}
|
5
src/inspector/main.zig
Normal file
5
src/inspector/main.zig
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
pub const key = @import("key.zig");
|
||||||
|
|
||||||
|
test {
|
||||||
|
@import("std").testing.refAllDecls(@This());
|
||||||
|
}
|
@ -295,6 +295,7 @@ test {
|
|||||||
|
|
||||||
// Libraries
|
// Libraries
|
||||||
_ = @import("segmented_pool.zig");
|
_ = @import("segmented_pool.zig");
|
||||||
|
_ = @import("inspector/main.zig");
|
||||||
_ = @import("terminal/main.zig");
|
_ = @import("terminal/main.zig");
|
||||||
_ = @import("terminfo/main.zig");
|
_ = @import("terminfo/main.zig");
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user