aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/inspector
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-04-04 19:22:04 +0300
committerCan Celik <ogulcancelik@gmail.com>2026-04-05 01:23:06 +0300
commit41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch)
tree3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/inspector
parent55c138263e190a1e79d4b997bfb9397306c0ec1f (diff)
feat: finish ghostty pane backend integration
Diffstat (limited to 'vendor/libghostty-vt/src/inspector')
-rw-r--r--vendor/libghostty-vt/src/inspector/AGENTS.md12
-rw-r--r--vendor/libghostty-vt/src/inspector/Inspector.zig131
-rw-r--r--vendor/libghostty-vt/src/inspector/main.zig8
-rw-r--r--vendor/libghostty-vt/src/inspector/units.zig3
-rw-r--r--vendor/libghostty-vt/src/inspector/widgets.zig227
-rw-r--r--vendor/libghostty-vt/src/inspector/widgets/key.zig535
-rw-r--r--vendor/libghostty-vt/src/inspector/widgets/page.zig428
-rw-r--r--vendor/libghostty-vt/src/inspector/widgets/pagelist.zig852
-rw-r--r--vendor/libghostty-vt/src/inspector/widgets/renderer.zig123
-rw-r--r--vendor/libghostty-vt/src/inspector/widgets/screen.zig400
-rw-r--r--vendor/libghostty-vt/src/inspector/widgets/style.zig125
-rw-r--r--vendor/libghostty-vt/src/inspector/widgets/surface.zig503
-rw-r--r--vendor/libghostty-vt/src/inspector/widgets/terminal.zig726
-rw-r--r--vendor/libghostty-vt/src/inspector/widgets/termio.zig811
14 files changed, 4884 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/inspector/AGENTS.md b/vendor/libghostty-vt/src/inspector/AGENTS.md
new file mode 100644
index 00000000..dafc81e0
--- /dev/null
+++ b/vendor/libghostty-vt/src/inspector/AGENTS.md
@@ -0,0 +1,12 @@
+# Inspector Subsystem
+
+The inspector is a feature of Ghostty that works similar to a
+browser's developer tools. It allows the user to inspect and modify the
+terminal state.
+
+- See the full C API by finding `dcimgui.h` in the `.zig-cache` folder
+ in the root: `find . -type f -name dcimgui.h`. Use the newest version.
+- See full examples of how to use every widget by loading this file:
+ <https://raw.githubusercontent.com/ocornut/imgui/refs/heads/master/imgui_demo.cpp>
+- On macOS, run builds with `-Demit-macos-app=false` to verify API usage.
+- There are no unit tests in this package.
diff --git a/vendor/libghostty-vt/src/inspector/Inspector.zig b/vendor/libghostty-vt/src/inspector/Inspector.zig
new file mode 100644
index 00000000..22d763f7
--- /dev/null
+++ b/vendor/libghostty-vt/src/inspector/Inspector.zig
@@ -0,0 +1,131 @@
+//! The Inspector is a development tool to debug the terminal. This is
+//! useful for terminal application developers as well as people potentially
+//! debugging issues in Ghostty itself.
+const Inspector = @This();
+
+const std = @import("std");
+const assert = @import("../quirks.zig").inlineAssert;
+const Allocator = std.mem.Allocator;
+const builtin = @import("builtin");
+const cimgui = @import("dcimgui");
+const Surface = @import("../Surface.zig");
+const font = @import("../font/main.zig");
+const terminal = @import("../terminal/main.zig");
+const inspector = @import("main.zig");
+const widgets = @import("widgets.zig");
+
+/// Mouse state that we track in addition to normal mouse states that
+/// Ghostty always knows about.
+mouse: widgets.surface.Mouse = .{},
+
+// ImGui state
+gui: widgets.surface.Inspector,
+
+/// Setup the ImGui state. This requires an ImGui context to be set.
+pub fn setup() void {
+ const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO();
+
+ // Enable docking, which we use heavily for the UI.
+ io.ConfigFlags |= cimgui.c.ImGuiConfigFlags_DockingEnable;
+
+ // Our colorspace is sRGB.
+ io.ConfigFlags |= cimgui.c.ImGuiConfigFlags_IsSRGB;
+
+ // Disable the ini file to save layout
+ io.IniFilename = null;
+ io.LogFilename = null;
+
+ // Use our own embedded font
+ {
+ // TODO: This will have to be recalculated for different screen DPIs.
+ // This is currently hardcoded to a 2x content scale.
+ const font_size = 16 * 2;
+
+ var font_config: cimgui.c.ImFontConfig = undefined;
+ cimgui.ext.ImFontConfig_ImFontConfig(&font_config);
+ font_config.FontDataOwnedByAtlas = false;
+ _ = cimgui.c.ImFontAtlas_AddFontFromMemoryTTF(
+ io.Fonts,
+ @ptrCast(@constCast(font.embedded.regular.ptr)),
+ @intCast(font.embedded.regular.len),
+ font_size,
+ &font_config,
+ null,
+ );
+ }
+}
+
+pub fn init(alloc: Allocator) !Inspector {
+ var gui: widgets.surface.Inspector = try .init(alloc);
+ errdefer gui.deinit(alloc);
+ return .{ .gui = gui };
+}
+
+pub fn deinit(self: *Inspector, alloc: Allocator) void {
+ self.gui.deinit(alloc);
+}
+
+/// Returns the renderer info panel. This is a convenience function
+/// to access and find this state to read and modify.
+pub fn rendererInfo(self: *Inspector) *widgets.renderer.Info {
+ return &self.gui.renderer_info;
+}
+
+/// Record a keyboard event.
+pub fn recordKeyEvent(
+ self: *Inspector,
+ alloc: Allocator,
+ ev: inspector.KeyEvent,
+) Allocator.Error!void {
+ const max_capacity = 50;
+
+ 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(events.capacity() * 2, max_capacity);
+ try events.resize(alloc, new_capacity);
+ try events.append(ev);
+ } else {
+ var it = events.iterator(.forward);
+ if (it.next()) |old_ev| old_ev.deinit(alloc);
+ events.deleteOldest(1);
+ try events.append(ev);
+ },
+
+ else => return err,
+ };
+}
+
+/// Record data read from the pty.
+pub fn recordPtyRead(
+ self: *Inspector,
+ alloc: Allocator,
+ t: *terminal.Terminal,
+ data: []const u8,
+) !void {
+ try self.gui.vt_stream.recordPtyRead(
+ alloc,
+ t,
+ data,
+ );
+}
+
+/// Render the frame.
+pub fn render(
+ self: *Inspector,
+ surface: *Surface,
+) void {
+ // Draw the UI
+ self.gui.draw(
+ surface,
+ self.mouse,
+ );
+
+ // We always trigger a rebuild of the surface when the inspector
+ // is focused because modifying the inspector can change the terminal
+ // state. This is KIND OF expensive (wasted CPU if nothing was done)
+ // but the inspector is a development tool and it expressly costs
+ // more resources while open so its okay.
+ surface.renderer_thread.wakeup.notify() catch {};
+}
diff --git a/vendor/libghostty-vt/src/inspector/main.zig b/vendor/libghostty-vt/src/inspector/main.zig
new file mode 100644
index 00000000..ae185f47
--- /dev/null
+++ b/vendor/libghostty-vt/src/inspector/main.zig
@@ -0,0 +1,8 @@
+pub const widgets = @import("widgets.zig");
+pub const Inspector = @import("Inspector.zig");
+
+pub const KeyEvent = widgets.key.Event;
+
+test {
+ @import("std").testing.refAllDecls(@This());
+}
diff --git a/vendor/libghostty-vt/src/inspector/units.zig b/vendor/libghostty-vt/src/inspector/units.zig
new file mode 100644
index 00000000..28f92823
--- /dev/null
+++ b/vendor/libghostty-vt/src/inspector/units.zig
@@ -0,0 +1,3 @@
+pub fn toKibiBytes(bytes: usize) usize {
+ return bytes / 1024;
+}
diff --git a/vendor/libghostty-vt/src/inspector/widgets.zig b/vendor/libghostty-vt/src/inspector/widgets.zig
new file mode 100644
index 00000000..dd8ebc00
--- /dev/null
+++ b/vendor/libghostty-vt/src/inspector/widgets.zig
@@ -0,0 +1,227 @@
+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 renderer = @import("widgets/renderer.zig");
+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.
+pub fn helpMarker(text: [:0]const u8) void {
+ cimgui.c.ImGui_TextDisabled("(?)");
+ if (!cimgui.c.ImGui_BeginItemTooltip()) return;
+ defer cimgui.c.ImGui_EndTooltip();
+
+ cimgui.c.ImGui_PushTextWrapPos(cimgui.c.ImGui_GetFontSize() * 35.0);
+ defer cimgui.c.ImGui_PopTextWrapPos();
+
+ cimgui.c.ImGui_TextUnformatted(text.ptr);
+}
+
+/// DetachableHeader allows rendering a collapsing header that can be
+/// detached into its own window.
+pub const DetachableHeader = struct {
+ /// Set whether the window is detached.
+ detached: bool = false,
+
+ /// If true, detaching will move the item into a docking position
+ /// to the right.
+ dock: bool = true,
+
+ // Internal state do not touch.
+ window_first: bool = true,
+
+ pub fn windowEnd(self: *DetachableHeader) void {
+ _ = self;
+
+ // If we started the window, we need to end it.
+ cimgui.c.ImGui_End();
+ }
+
+ /// Returns null if there is no window created (not detached).
+ /// Otherwise returns whether the window is open.
+ pub fn window(
+ self: *DetachableHeader,
+ label: [:0]const u8,
+ ) ?bool {
+ // If we're not detached, we don't create a window.
+ if (!self.detached) {
+ self.window_first = true;
+ return null;
+ }
+
+ // If this is our first time showing the window then we need to
+ // setup docking. We only do this on the first time because we
+ // don't want to reset a user's docking behavior later.
+ if (self.window_first) dock: {
+ self.window_first = false;
+ if (!self.dock) break :dock;
+ const dock_id = cimgui.c.ImGui_GetWindowDockID();
+ if (dock_id == 0) break :dock;
+ var dock_id_right: cimgui.c.ImGuiID = 0;
+ var dock_id_left: cimgui.c.ImGuiID = 0;
+ _ = cimgui.ImGui_DockBuilderSplitNode(
+ dock_id,
+ cimgui.c.ImGuiDir_Right,
+ 0.4,
+ &dock_id_right,
+ &dock_id_left,
+ );
+ cimgui.ImGui_DockBuilderDockWindow(label, dock_id_right);
+ cimgui.ImGui_DockBuilderFinish(dock_id);
+ }
+
+ return cimgui.c.ImGui_Begin(
+ label,
+ &self.detached,
+ cimgui.c.ImGuiWindowFlags_NoFocusOnAppearing,
+ );
+ }
+
+ pub fn header(
+ self: *DetachableHeader,
+ label: [:0]const u8,
+ ) bool {
+ // If we're detached, create a separate window.
+ if (self.detached) return false;
+
+ // Make sure all headers have a unique ID in the stack. We only
+ // need to do this for the header side because creating a window
+ // automatically creates an ID.
+ cimgui.c.ImGui_PushID(label);
+ defer cimgui.c.ImGui_PopID();
+
+ // Create the collapsing header with the pop out button overlaid.
+ cimgui.c.ImGui_SetNextItemAllowOverlap();
+ const is_open = cimgui.c.ImGui_CollapsingHeader(
+ label,
+ cimgui.c.ImGuiTreeNodeFlags_None,
+ );
+
+ // Place pop-out button inside the header bar
+ const header_max = cimgui.c.ImGui_GetItemRectMax();
+ const header_min = cimgui.c.ImGui_GetItemRectMin();
+ const frame_height = cimgui.c.ImGui_GetFrameHeight();
+ const button_size = frame_height - 4;
+ const padding = 4;
+
+ cimgui.c.ImGui_SameLine();
+ cimgui.c.ImGui_SetCursorScreenPos(.{
+ .x = header_max.x - button_size - padding,
+ .y = header_min.y + 2,
+ });
+ {
+ cimgui.c.ImGui_PushStyleVarImVec2(
+ cimgui.c.ImGuiStyleVar_FramePadding,
+ .{ .x = 0, .y = 0 },
+ );
+ defer cimgui.c.ImGui_PopStyleVar();
+ if (cimgui.c.ImGui_ButtonEx(
+ ">>##detach",
+ .{ .x = button_size, .y = button_size },
+ )) {
+ self.detached = true;
+ }
+ }
+
+ if (cimgui.c.ImGui_IsItemHovered(cimgui.c.ImGuiHoveredFlags_DelayShort)) {
+ cimgui.c.ImGui_SetTooltip("Detach into separate window");
+ }
+
+ return is_open;
+ }
+};
+
+pub const DetachableHeaderState = struct {
+ show_window: bool = false,
+
+ /// Internal state. Don't touch.
+ first_show: bool = false,
+};
+
+/// Render a collapsing header that can be detached into its own window.
+/// When detached, renders as a separate window with a close button.
+/// When attached, renders as a collapsing header with a pop-out button.
+pub fn detachableHeader(
+ label: [:0]const u8,
+ state: *DetachableHeaderState,
+ ctx: anytype,
+ comptime contentFn: fn (@TypeOf(ctx)) void,
+) void {
+ cimgui.c.ImGui_PushID(label);
+ defer cimgui.c.ImGui_PopID();
+
+ if (state.show_window) {
+ // On first show, dock this window to the right of the parent window's dock.
+ // We only do this once so the user can freely reposition the window afterward
+ // without it snapping back to the right on every frame.
+ if (!state.first_show) {
+ state.first_show = true;
+ const current_dock_id = cimgui.c.ImGui_GetWindowDockID();
+ if (current_dock_id != 0) {
+ var dock_id_right: cimgui.c.ImGuiID = 0;
+ var dock_id_left: cimgui.c.ImGuiID = 0;
+ _ = cimgui.ImGui_DockBuilderSplitNode(
+ current_dock_id,
+ cimgui.c.ImGuiDir_Right,
+ 0.3,
+ &dock_id_right,
+ &dock_id_left,
+ );
+ cimgui.ImGui_DockBuilderDockWindow(label, dock_id_right);
+ cimgui.ImGui_DockBuilderFinish(current_dock_id);
+ }
+ }
+
+ defer cimgui.c.ImGui_End();
+ if (cimgui.c.ImGui_Begin(
+ label,
+ &state.show_window,
+ cimgui.c.ImGuiWindowFlags_NoFocusOnAppearing,
+ )) contentFn(ctx);
+ return;
+ }
+
+ // Reset first_show when window is closed so next open docks again
+ state.first_show = false;
+
+ cimgui.c.ImGui_SetNextItemAllowOverlap();
+ const is_open = cimgui.c.ImGui_CollapsingHeader(
+ label,
+ cimgui.c.ImGuiTreeNodeFlags_None,
+ );
+
+ // Place pop-out button inside the header bar
+ const header_max = cimgui.c.ImGui_GetItemRectMax();
+ const header_min = cimgui.c.ImGui_GetItemRectMin();
+ const frame_height = cimgui.c.ImGui_GetFrameHeight();
+ const button_size = frame_height - 4;
+ const padding = 4;
+
+ cimgui.c.ImGui_SameLine();
+ cimgui.c.ImGui_SetCursorScreenPos(.{
+ .x = header_max.x - button_size - padding,
+ .y = header_min.y + 2,
+ });
+ cimgui.c.ImGui_PushStyleVarImVec2(
+ cimgui.c.ImGuiStyleVar_FramePadding,
+ .{ .x = 0, .y = 0 },
+ );
+ if (cimgui.c.ImGui_ButtonEx(
+ ">>##detach",
+ .{ .x = button_size, .y = button_size },
+ )) {
+ state.show_window = true;
+ }
+ cimgui.c.ImGui_PopStyleVar();
+ if (cimgui.c.ImGui_IsItemHovered(cimgui.c.ImGuiHoveredFlags_DelayShort)) {
+ cimgui.c.ImGui_SetTooltip("Pop out into separate window");
+ }
+
+ if (is_open) contentFn(ctx);
+}
diff --git a/vendor/libghostty-vt/src/inspector/widgets/key.zig b/vendor/libghostty-vt/src/inspector/widgets/key.zig
new file mode 100644
index 00000000..7d7188a6
--- /dev/null
+++ b/vendor/libghostty-vt/src/inspector/widgets/key.zig
@@ -0,0 +1,535 @@
+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_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, 80, 0);
+ cimgui.c.ImGui_TableSetupColumnEx("Key", cimgui.c.ImGuiTableColumnFlags_WidthFixed, 160, 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();
+ const row_min_y = cimgui.c.ImGui_GetCursorScreenPos().y;
+
+ // Set row background color based on action
+ cimgui.c.ImGui_TableSetBgColor(cimgui.c.ImGuiTableBgTarget_RowBg0, actionColor(ev.event.action), -1);
+
+ // Action column with colored text
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ const action_text_color: cimgui.c.ImVec4 = switch (ev.event.action) {
+ .press => .{ .x = 0.4, .y = 1.0, .z = 0.4, .w = 1.0 }, // Green
+ .release => .{ .x = 0.6, .y = 0.6, .z = 1.0, .w = 1.0 }, // Blue
+ .repeat => .{ .x = 1.0, .y = 1.0, .z = 0.4, .w = 1.0 }, // Yellow
+ };
+ cimgui.c.ImGui_TextColored(action_text_color, "%s", @tagName(ev.event.action).ptr);
+
+ // Key column with consistent key coloring
+ _ = 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),
+ };
+ const key_rgba = keyColor(ev.event.key);
+ const key_color: cimgui.c.ImVec4 = .{
+ .x = @as(f32, @floatFromInt(key_rgba & 0xFF)) / 255.0,
+ .y = @as(f32, @floatFromInt((key_rgba >> 8) & 0xFF)) / 255.0,
+ .z = @as(f32, @floatFromInt((key_rgba >> 16) & 0xFF)) / 255.0,
+ .w = 1.0,
+ };
+ cimgui.c.ImGui_TextColored(key_color, "%s", key_name.ptr);
+
+ // Composing indicator
+ if (ev.event.composing) {
+ cimgui.c.ImGui_SameLine();
+ cimgui.c.ImGui_TextColored(.{ .x = 1.0, .y = 0.6, .z = 0.0, .w = 1.0 }, "*");
+ if (cimgui.c.ImGui_IsItemHovered(cimgui.c.ImGuiHoveredFlags_None)) {
+ cimgui.c.ImGui_SetTooltip("Composing (dead key)");
+ }
+ }
+
+ // 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);
+ }
+
+ // Row hover highlight
+ const row_max_y = cimgui.c.ImGui_GetCursorScreenPos().y;
+ const mouse_pos = cimgui.c.ImGui_GetMousePos();
+ if (mouse_pos.y >= row_min_y and mouse_pos.y < row_max_y) {
+ cimgui.c.ImGui_TableSetBgColor(cimgui.c.ImGuiTableBgTarget_RowBg1, 0x1AFFFFFF, -1);
+ }
+ }
+ }
+};
+
+/// Returns row background color for an action (ABGR format for ImGui)
+fn actionColor(action: input.Action) u32 {
+ return switch (action) {
+ .press => 0x1A4A6F4A, // Muted sage green
+ .release => 0x1A6A5A5A, // Muted slate gray
+ .repeat => 0x1A4A5A6F, // Muted warm brown
+ };
+}
+
+/// Generate a consistent color for a key based on its enum value.
+/// Uses HSV color space with fixed saturation and value for pleasing colors.
+fn keyColor(key: input.Key) u32 {
+ const key_int: u32 = @intCast(@intFromEnum(key));
+ const hue: f32 = @as(f32, @floatFromInt(key_int *% 47)) / 256.0;
+ return hsvToRgba(hue, 0.5, 0.9, 1.0);
+}
+
+/// Convert HSV (hue 0-1, saturation 0-1, value 0-1) to RGBA u32.
+fn hsvToRgba(h: f32, s: f32, v: f32, a: f32) u32 {
+ var r: f32 = undefined;
+ var g: f32 = undefined;
+ var b: f32 = undefined;
+
+ const i: u32 = @intFromFloat(h * 6.0);
+ const f = h * 6.0 - @as(f32, @floatFromInt(i));
+ const p = v * (1.0 - s);
+ const q = v * (1.0 - f * s);
+ const t = v * (1.0 - (1.0 - f) * s);
+
+ switch (i % 6) {
+ 0 => {
+ r = v;
+ g = t;
+ b = p;
+ },
+ 1 => {
+ r = q;
+ g = v;
+ b = p;
+ },
+ 2 => {
+ r = p;
+ g = v;
+ b = t;
+ },
+ 3 => {
+ r = p;
+ g = q;
+ b = v;
+ },
+ 4 => {
+ r = t;
+ g = p;
+ b = v;
+ },
+ else => {
+ r = v;
+ g = p;
+ b = q;
+ },
+ }
+
+ const ri: u32 = @intFromFloat(r * 255.0);
+ const gi: u32 = @intFromFloat(g * 255.0);
+ const bi: u32 = @intFromFloat(b * 255.0);
+ const ai: u32 = @intFromFloat(a * 255.0);
+
+ return (ai << 24) | (bi << 16) | (gi << 8) | ri;
+}
diff --git a/vendor/libghostty-vt/src/inspector/widgets/page.zig b/vendor/libghostty-vt/src/inspector/widgets/page.zig
new file mode 100644
index 00000000..844abc35
--- /dev/null
+++ b/vendor/libghostty-vt/src/inspector/widgets/page.zig
@@ -0,0 +1,428 @@
+const std = @import("std");
+const cimgui = @import("dcimgui");
+const terminal = @import("../../terminal/main.zig");
+const units = @import("../units.zig");
+const widgets = @import("../widgets.zig");
+
+const PageList = terminal.PageList;
+const Page = terminal.Page;
+
+pub fn inspector(page: *const terminal.Page) void {
+ cimgui.c.ImGui_SeparatorText("Managed Memory");
+ managedMemory(page);
+
+ cimgui.c.ImGui_SeparatorText("Styles");
+ stylesList(page);
+
+ cimgui.c.ImGui_SeparatorText("Hyperlinks");
+ hyperlinksList(page);
+
+ cimgui.c.ImGui_SeparatorText("Rows");
+ rowsTable(page);
+}
+
+/// Draw a tree node header with metadata about this page. Returns if
+/// the tree node is open or not. If it is open you must close it with
+/// TreePop.
+pub fn treeNode(state: struct {
+ /// The page
+ page: *const terminal.Page,
+ /// The index of the page in a page list, used for headers.
+ index: usize,
+ /// The range of rows this page covers, inclusive.
+ row_range: [2]usize,
+ /// Whether this page is the active or viewport node.
+ active: bool,
+ viewport: bool,
+}) bool {
+ // Setup our node.
+ const open = open: {
+ var label_buf: [160]u8 = undefined;
+ const label = std.fmt.bufPrintZ(
+ &label_buf,
+ "Page {d}",
+ .{state.index},
+ ) catch "Page";
+
+ const flags = cimgui.c.ImGuiTreeNodeFlags_AllowOverlap |
+ cimgui.c.ImGuiTreeNodeFlags_SpanFullWidth |
+ cimgui.c.ImGuiTreeNodeFlags_FramePadding;
+ break :open cimgui.c.ImGui_TreeNodeEx(label.ptr, flags);
+ };
+
+ // Move our cursor into the tree header so we can add extra info.
+ const header_min = cimgui.c.ImGui_GetItemRectMin();
+ const header_max = cimgui.c.ImGui_GetItemRectMax();
+ const header_height = header_max.y - header_min.y;
+ const text_line = cimgui.c.ImGui_GetTextLineHeight();
+ const y_center = header_min.y + (header_height - text_line) * 0.5;
+ cimgui.c.ImGui_SetCursorScreenPos(.{ .x = header_min.x + 170, .y = y_center });
+
+ // Metadata
+ cimgui.c.ImGui_TextDisabled(
+ "%dc x %dr",
+ state.page.size.cols,
+ state.page.size.rows,
+ );
+ cimgui.c.ImGui_SameLine();
+ cimgui.c.ImGui_Text("rows %d..%d", state.row_range[0], state.row_range[1]);
+
+ // Labels
+ if (state.active) {
+ cimgui.c.ImGui_SameLine();
+ cimgui.c.ImGui_TextColored(.{ .x = 0.4, .y = 0.9, .z = 0.4, .w = 1.0 }, "active");
+ }
+ if (state.viewport) {
+ cimgui.c.ImGui_SameLine();
+ cimgui.c.ImGui_TextColored(.{ .x = 0.4, .y = 0.8, .z = 1.0, .w = 1.0 }, "viewport");
+ }
+ if (state.page.isDirty()) {
+ cimgui.c.ImGui_SameLine();
+ cimgui.c.ImGui_TextColored(.{ .x = 1.0, .y = 0.4, .z = 0.4, .w = 1.0 }, "dirty");
+ }
+
+ return open;
+}
+
+pub fn managedMemory(page: *const Page) void {
+ if (cimgui.c.ImGui_BeginTable(
+ "##overview",
+ 3,
+ cimgui.c.ImGuiTableFlags_BordersInnerV |
+ cimgui.c.ImGuiTableFlags_RowBg |
+ cimgui.c.ImGuiTableFlags_SizingFixedFit,
+ )) {
+ defer cimgui.c.ImGui_EndTable();
+
+ cimgui.c.ImGui_TableNextRow();
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Memory Size");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ widgets.helpMarker(
+ "Memory allocated for this page. Note the backing memory " ++
+ "may be a larger allocation from which this page " ++
+ "uses a portion.",
+ );
+ _ = cimgui.c.ImGui_TableSetColumnIndex(2);
+ cimgui.c.ImGui_Text(
+ "%d KiB",
+ units.toKibiBytes(page.memory.len),
+ );
+ }
+
+ if (cimgui.c.ImGui_BeginTable(
+ "##managed",
+ 4,
+ cimgui.c.ImGuiTableFlags_BordersInnerV |
+ cimgui.c.ImGuiTableFlags_RowBg |
+ cimgui.c.ImGuiTableFlags_SizingFixedFit,
+ )) {
+ defer cimgui.c.ImGui_EndTable();
+
+ cimgui.c.ImGui_TableSetupColumn("Resource", cimgui.c.ImGuiTableColumnFlags_WidthFixed);
+ cimgui.c.ImGui_TableSetupColumn("", cimgui.c.ImGuiTableColumnFlags_WidthFixed);
+ cimgui.c.ImGui_TableSetupColumn("Used", cimgui.c.ImGuiTableColumnFlags_WidthFixed);
+ cimgui.c.ImGui_TableSetupColumn("Capacity", cimgui.c.ImGuiTableColumnFlags_WidthFixed);
+ cimgui.c.ImGui_TableHeadersRow();
+
+ const size = page.size;
+ const cap = page.capacity;
+ cimgui.c.ImGui_TableNextRow();
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Columns");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ widgets.helpMarker("Number of columns in the terminal grid.");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(2);
+ cimgui.c.ImGui_Text("%d", size.cols);
+ _ = cimgui.c.ImGui_TableSetColumnIndex(3);
+ cimgui.c.ImGui_Text("%d", cap.cols);
+
+ cimgui.c.ImGui_TableNextRow();
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Rows");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ widgets.helpMarker("Number of rows in this page.");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(2);
+ cimgui.c.ImGui_Text("%d", size.rows);
+ _ = cimgui.c.ImGui_TableSetColumnIndex(3);
+ cimgui.c.ImGui_Text("%d", cap.rows);
+
+ cimgui.c.ImGui_TableNextRow();
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Styles");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ widgets.helpMarker("Unique text styles (colors, attributes) currently in use.");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(2);
+ cimgui.c.ImGui_Text("%d", page.styles.count());
+ _ = cimgui.c.ImGui_TableSetColumnIndex(3);
+ cimgui.c.ImGui_Text("%d", page.styles.layout.cap);
+
+ cimgui.c.ImGui_TableNextRow();
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Graphemes");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ widgets.helpMarker("Extended grapheme clusters for multi-codepoint characters.");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(2);
+ cimgui.c.ImGui_Text("%d", page.graphemeCount());
+ _ = cimgui.c.ImGui_TableSetColumnIndex(3);
+ cimgui.c.ImGui_Text("%d", page.graphemeCapacity());
+
+ cimgui.c.ImGui_TableNextRow();
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Strings (bytes)");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ widgets.helpMarker("String storage for hyperlink URIs and other text data.");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(2);
+ cimgui.c.ImGui_Text("%d", page.string_alloc.usedBytes(page.memory));
+ _ = cimgui.c.ImGui_TableSetColumnIndex(3);
+ cimgui.c.ImGui_Text("%d", page.string_alloc.capacityBytes());
+
+ const hyperlink_map = page.hyperlink_map.map(page.memory);
+ cimgui.c.ImGui_TableNextRow();
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Hyperlink Map");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ widgets.helpMarker("Maps cell positions to hyperlink IDs.");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(2);
+ cimgui.c.ImGui_Text("%d", hyperlink_map.count());
+ _ = cimgui.c.ImGui_TableSetColumnIndex(3);
+ cimgui.c.ImGui_Text("%d", hyperlink_map.capacity());
+
+ cimgui.c.ImGui_TableNextRow();
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Hyperlink IDs");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ widgets.helpMarker("Unique hyperlink definitions (URI + optional ID).");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(2);
+ cimgui.c.ImGui_Text("%d", page.hyperlink_set.count());
+ _ = cimgui.c.ImGui_TableSetColumnIndex(3);
+ cimgui.c.ImGui_Text("%d", page.hyperlink_set.layout.cap);
+ }
+}
+
+fn rowsTable(page: *const terminal.Page) void {
+ const visible_rows: usize = @min(page.size.rows, 12);
+ const row_height: f32 = cimgui.c.ImGui_GetTextLineHeightWithSpacing();
+ const child_height: f32 = row_height * (@as(f32, @floatFromInt(visible_rows)) + 2.0);
+
+ // Child window so scrolling is separate.
+ // This defer first is not a bug, EndChild always needs to be called.
+ defer cimgui.c.ImGui_EndChild();
+ if (!cimgui.c.ImGui_BeginChild(
+ "##page_rows",
+ .{ .x = 0.0, .y = child_height },
+ cimgui.c.ImGuiChildFlags_Borders,
+ cimgui.c.ImGuiWindowFlags_None,
+ )) return;
+
+ if (!cimgui.c.ImGui_BeginTable(
+ "##page_rows_table",
+ 10,
+ cimgui.c.ImGuiTableFlags_BordersInnerV |
+ cimgui.c.ImGuiTableFlags_RowBg |
+ cimgui.c.ImGuiTableFlags_SizingFixedFit,
+ )) return;
+ defer cimgui.c.ImGui_EndTable();
+
+ cimgui.c.ImGui_TableSetupScrollFreeze(0, 1);
+ cimgui.c.ImGui_TableSetupColumn("Row", cimgui.c.ImGuiTableColumnFlags_WidthFixed);
+ cimgui.c.ImGui_TableSetupColumn("Text", cimgui.c.ImGuiTableColumnFlags_WidthFixed);
+ cimgui.c.ImGui_TableSetupColumn("Dirty", cimgui.c.ImGuiTableColumnFlags_WidthFixed);
+ cimgui.c.ImGui_TableSetupColumn("Wrap", cimgui.c.ImGuiTableColumnFlags_WidthFixed);
+ cimgui.c.ImGui_TableSetupColumn("Cont", cimgui.c.ImGuiTableColumnFlags_WidthFixed);
+ cimgui.c.ImGui_TableSetupColumn("Styled", cimgui.c.ImGuiTableColumnFlags_WidthFixed);
+ cimgui.c.ImGui_TableSetupColumn("Grapheme", cimgui.c.ImGuiTableColumnFlags_WidthFixed);
+ cimgui.c.ImGui_TableSetupColumn("Link", cimgui.c.ImGuiTableColumnFlags_WidthFixed);
+ cimgui.c.ImGui_TableSetupColumn("Prompt", cimgui.c.ImGuiTableColumnFlags_WidthFixed);
+ cimgui.c.ImGui_TableSetupColumn("Kitty", cimgui.c.ImGuiTableColumnFlags_WidthFixed);
+ cimgui.c.ImGui_TableHeadersRow();
+
+ const rows = page.rows.ptr(page.memory)[0..page.size.rows];
+ for (rows, 0..) |*row, row_index| {
+ var text_cells: usize = 0;
+ const cells = page.getCells(row);
+ for (cells) |cell| {
+ if (cell.hasText()) {
+ text_cells += 1;
+ }
+ }
+
+ cimgui.c.ImGui_TableNextRow();
+
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("%d", row_index);
+
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ if (text_cells == 0) {
+ cimgui.c.ImGui_TextDisabled("0");
+ } else {
+ cimgui.c.ImGui_Text("%d", text_cells);
+ }
+
+ _ = cimgui.c.ImGui_TableSetColumnIndex(2);
+ flagCell(row.dirty);
+
+ _ = cimgui.c.ImGui_TableSetColumnIndex(3);
+ flagCell(row.wrap);
+
+ _ = cimgui.c.ImGui_TableSetColumnIndex(4);
+ flagCell(row.wrap_continuation);
+
+ _ = cimgui.c.ImGui_TableSetColumnIndex(5);
+ flagCell(row.styled);
+
+ _ = cimgui.c.ImGui_TableSetColumnIndex(6);
+ flagCell(row.grapheme);
+
+ _ = cimgui.c.ImGui_TableSetColumnIndex(7);
+ flagCell(row.hyperlink);
+
+ _ = cimgui.c.ImGui_TableSetColumnIndex(8);
+ cimgui.c.ImGui_Text("%s", @tagName(row.semantic_prompt).ptr);
+
+ _ = cimgui.c.ImGui_TableSetColumnIndex(9);
+ flagCell(row.kitty_virtual_placeholder);
+ }
+}
+
+fn stylesList(page: *const Page) void {
+ const items = page.styles.items.ptr(page.memory)[0..page.styles.layout.cap];
+
+ var count: usize = 0;
+ for (items, 0..) |item, index| {
+ if (index == 0) continue;
+ if (item.meta.ref == 0) continue;
+ count += 1;
+ }
+
+ if (count == 0) {
+ cimgui.c.ImGui_TextDisabled("(no styles in use)");
+ return;
+ }
+
+ const visible_rows: usize = @min(count, 8);
+ const row_height: f32 = cimgui.c.ImGui_GetTextLineHeightWithSpacing();
+ const child_height: f32 = row_height * (@as(f32, @floatFromInt(visible_rows)) + 2.0);
+
+ defer cimgui.c.ImGui_EndChild();
+ if (!cimgui.c.ImGui_BeginChild(
+ "##page_styles",
+ .{ .x = 0.0, .y = child_height },
+ cimgui.c.ImGuiChildFlags_Borders,
+ cimgui.c.ImGuiWindowFlags_None,
+ )) return;
+
+ if (!cimgui.c.ImGui_BeginTable(
+ "##page_styles_table",
+ 3,
+ cimgui.c.ImGuiTableFlags_BordersInnerV |
+ cimgui.c.ImGuiTableFlags_RowBg |
+ cimgui.c.ImGuiTableFlags_SizingFixedFit,
+ )) return;
+ defer cimgui.c.ImGui_EndTable();
+
+ cimgui.c.ImGui_TableSetupScrollFreeze(0, 1);
+ cimgui.c.ImGui_TableSetupColumn("ID", cimgui.c.ImGuiTableColumnFlags_WidthFixed);
+ cimgui.c.ImGui_TableSetupColumn("Refs", cimgui.c.ImGuiTableColumnFlags_WidthFixed);
+ cimgui.c.ImGui_TableSetupColumn("Style", cimgui.c.ImGuiTableColumnFlags_WidthStretch);
+ cimgui.c.ImGui_TableHeadersRow();
+
+ for (items, 0..) |item, index| {
+ if (index == 0) continue;
+ if (item.meta.ref == 0) continue;
+
+ cimgui.c.ImGui_TableNextRow();
+ cimgui.c.ImGui_PushIDInt(@intCast(index));
+ defer cimgui.c.ImGui_PopID();
+
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("%d", index);
+
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ cimgui.c.ImGui_Text("%d", item.meta.ref);
+
+ _ = cimgui.c.ImGui_TableSetColumnIndex(2);
+ if (cimgui.c.ImGui_TreeNodeEx("Details", cimgui.c.ImGuiTreeNodeFlags_None)) {
+ defer cimgui.c.ImGui_TreePop();
+ widgets.style.table(item.value, null);
+ }
+ }
+}
+
+fn hyperlinksList(page: *const Page) void {
+ const items = page.hyperlink_set.items.ptr(page.memory)[0..page.hyperlink_set.layout.cap];
+
+ var count: usize = 0;
+ for (items, 0..) |item, index| {
+ if (index == 0) continue;
+ if (item.meta.ref == 0) continue;
+ count += 1;
+ }
+
+ if (count == 0) {
+ cimgui.c.ImGui_TextDisabled("(no hyperlinks in use)");
+ return;
+ }
+
+ const visible_rows: usize = @min(count, 8);
+ const row_height: f32 = cimgui.c.ImGui_GetTextLineHeightWithSpacing();
+ const child_height: f32 = row_height * (@as(f32, @floatFromInt(visible_rows)) + 2.0);
+
+ defer cimgui.c.ImGui_EndChild();
+ if (!cimgui.c.ImGui_BeginChild(
+ "##page_hyperlinks",
+ .{ .x = 0.0, .y = child_height },
+ cimgui.c.ImGuiChildFlags_Borders,
+ cimgui.c.ImGuiWindowFlags_None,
+ )) return;
+
+ if (!cimgui.c.ImGui_BeginTable(
+ "##page_hyperlinks_table",
+ 4,
+ cimgui.c.ImGuiTableFlags_BordersInnerV |
+ cimgui.c.ImGuiTableFlags_RowBg |
+ cimgui.c.ImGuiTableFlags_SizingFixedFit,
+ )) return;
+ defer cimgui.c.ImGui_EndTable();
+
+ cimgui.c.ImGui_TableSetupScrollFreeze(0, 1);
+ cimgui.c.ImGui_TableSetupColumn("ID", cimgui.c.ImGuiTableColumnFlags_WidthFixed);
+ cimgui.c.ImGui_TableSetupColumn("Refs", cimgui.c.ImGuiTableColumnFlags_WidthFixed);
+ cimgui.c.ImGui_TableSetupColumn("Explicit ID", cimgui.c.ImGuiTableColumnFlags_WidthFixed);
+ cimgui.c.ImGui_TableSetupColumn("URI", cimgui.c.ImGuiTableColumnFlags_WidthStretch);
+ cimgui.c.ImGui_TableHeadersRow();
+
+ for (items, 0..) |item, index| {
+ if (index == 0) continue;
+ if (item.meta.ref == 0) continue;
+
+ cimgui.c.ImGui_TableNextRow();
+
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("%d", index);
+
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ cimgui.c.ImGui_Text("%d", item.meta.ref);
+
+ _ = cimgui.c.ImGui_TableSetColumnIndex(2);
+ switch (item.value.id) {
+ .explicit => |slice| {
+ const explicit_id = slice.slice(page.memory);
+ cimgui.c.ImGui_Text("%.*s", explicit_id.len, explicit_id.ptr);
+ },
+ .implicit => cimgui.c.ImGui_TextDisabled("-"),
+ }
+
+ _ = cimgui.c.ImGui_TableSetColumnIndex(3);
+ const uri = item.value.uri.slice(page.memory);
+ cimgui.c.ImGui_Text("%.*s", uri.len, uri.ptr);
+ }
+}
+
+fn flagCell(value: bool) void {
+ if (value) {
+ cimgui.c.ImGui_TextColored(.{ .x = 0.4, .y = 0.9, .z = 0.4, .w = 1.0 }, "yes");
+ } else {
+ cimgui.c.ImGui_TextDisabled("-");
+ }
+}
diff --git a/vendor/libghostty-vt/src/inspector/widgets/pagelist.zig b/vendor/libghostty-vt/src/inspector/widgets/pagelist.zig
new file mode 100644
index 00000000..bedbfb59
--- /dev/null
+++ b/vendor/libghostty-vt/src/inspector/widgets/pagelist.zig
@@ -0,0 +1,852 @@
+const std = @import("std");
+const cimgui = @import("dcimgui");
+const terminal = @import("../../terminal/main.zig");
+const stylepkg = @import("../../terminal/style.zig");
+const widgets = @import("../widgets.zig");
+const units = @import("../units.zig");
+
+const PageList = terminal.PageList;
+
+/// PageList inspector widget.
+pub const Inspector = struct {
+ pub const empty: Inspector = .{};
+
+ pub fn draw(_: *const Inspector, pages: *PageList) void {
+ cimgui.c.ImGui_TextWrapped(
+ "PageList manages the backing pages that hold scrollback and the active " ++
+ "terminal grid. Each page is a contiguous memory buffer with its " ++
+ "own rows, cells, style set, grapheme map, and hyperlink storage.",
+ );
+
+ if (cimgui.c.ImGui_CollapsingHeader(
+ "Overview",
+ cimgui.c.ImGuiTreeNodeFlags_DefaultOpen,
+ )) {
+ summaryTable(pages);
+ }
+
+ if (cimgui.c.ImGui_CollapsingHeader(
+ "Scrollbar & Regions",
+ cimgui.c.ImGuiTreeNodeFlags_DefaultOpen,
+ )) {
+ cimgui.c.ImGui_SeparatorText("Scrollbar");
+ scrollbarInfo(pages);
+ cimgui.c.ImGui_SeparatorText("Regions");
+ regionsTable(pages);
+ }
+
+ if (cimgui.c.ImGui_CollapsingHeader(
+ "Tracked Pins",
+ cimgui.c.ImGuiTreeNodeFlags_DefaultOpen,
+ )) {
+ trackedPinsTable(pages);
+ }
+
+ if (cimgui.c.ImGui_CollapsingHeader(
+ "Pages",
+ cimgui.c.ImGuiTreeNodeFlags_DefaultOpen,
+ )) {
+ widgets.helpMarker(
+ "Pages are shown most-recent first. Each page holds a grid of rows/cells " ++
+ "plus metadata tables for styles, graphemes, strings, and hyperlinks.",
+ );
+
+ const active_pin = pages.getTopLeft(.active);
+ const viewport_pin = pages.getTopLeft(.viewport);
+
+ var row_offset = pages.total_rows;
+ var index: usize = pages.totalPages();
+ var node = pages.pages.last;
+ while (node) |page_node| : (node = page_node.prev) {
+ const page = &page_node.data;
+ row_offset -= page.size.rows;
+ index -= 1;
+
+ // We use our location as the ID so that even if reallocations
+ // happen we remain open if we're open already.
+ cimgui.c.ImGui_PushIDInt(@intCast(index));
+ defer cimgui.c.ImGui_PopID();
+
+ // Open up the tree node.
+ if (!widgets.page.treeNode(.{
+ .page = page,
+ .index = index,
+ .row_range = .{ row_offset, row_offset + page.size.rows - 1 },
+ .active = node == active_pin.node,
+ .viewport = node == viewport_pin.node,
+ })) continue;
+ defer cimgui.c.ImGui_TreePop();
+ widgets.page.inspector(page);
+ }
+ }
+ }
+};
+
+fn summaryTable(pages: *const PageList) void {
+ if (!cimgui.c.ImGui_BeginTable(
+ "pagelist_summary",
+ 3,
+ cimgui.c.ImGuiTableFlags_BordersInnerV |
+ cimgui.c.ImGuiTableFlags_RowBg |
+ cimgui.c.ImGuiTableFlags_SizingFixedFit,
+ )) return;
+ defer cimgui.c.ImGui_EndTable();
+
+ cimgui.c.ImGui_TableNextRow();
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Active Grid");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ widgets.helpMarker("Active viewport size in columns x rows.");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(2);
+ cimgui.c.ImGui_Text("%dc x %dr", pages.cols, pages.rows);
+
+ cimgui.c.ImGui_TableNextRow();
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Pages");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ widgets.helpMarker("Total number of pages in the linked list.");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(2);
+ cimgui.c.ImGui_Text("%d", pages.totalPages());
+
+ cimgui.c.ImGui_TableNextRow();
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Total Rows");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ widgets.helpMarker("Total rows represented by scrollback + active area.");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(2);
+ cimgui.c.ImGui_Text("%d", pages.total_rows);
+
+ cimgui.c.ImGui_TableNextRow();
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Page Bytes");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ widgets.helpMarker("Total bytes allocated for active pages.");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(2);
+ cimgui.c.ImGui_Text(
+ "%d KiB",
+ units.toKibiBytes(pages.page_size),
+ );
+
+ cimgui.c.ImGui_TableNextRow();
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Max Size");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ widgets.helpMarker(
+ \\Maximum bytes before pages must be evicated. The total
+ \\used bytes may be higher due to minimum individual page
+ \\sizes but the next allocation that would exceed this limit
+ \\will evict pages from the front of the list to free up space.
+ );
+ _ = cimgui.c.ImGui_TableSetColumnIndex(2);
+ cimgui.c.ImGui_Text(
+ "%d KiB",
+ units.toKibiBytes(pages.maxSize()),
+ );
+
+ cimgui.c.ImGui_TableNextRow();
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Viewport");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ widgets.helpMarker("Current viewport anchoring mode.");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(2);
+ cimgui.c.ImGui_Text("%s", @tagName(pages.viewport).ptr);
+
+ cimgui.c.ImGui_TableNextRow();
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Tracked Pins");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ widgets.helpMarker("Number of pins tracked for automatic updates.");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(2);
+ cimgui.c.ImGui_Text("%d", pages.countTrackedPins());
+}
+
+fn scrollbarInfo(pages: *PageList) void {
+ const scrollbar = pages.scrollbar();
+
+ // If we have a scrollbar, show it.
+ if (scrollbar.total > 0) {
+ var delta_row: isize = 0;
+ scrollbarWidget(&scrollbar, &delta_row);
+ if (delta_row != 0) {
+ pages.scroll(.{ .delta_row = delta_row });
+ }
+ }
+
+ if (!cimgui.c.ImGui_BeginTable(
+ "scrollbar_info",
+ 3,
+ cimgui.c.ImGuiTableFlags_BordersInnerV |
+ cimgui.c.ImGuiTableFlags_RowBg |
+ cimgui.c.ImGuiTableFlags_SizingFixedFit,
+ )) return;
+ defer cimgui.c.ImGui_EndTable();
+
+ cimgui.c.ImGui_TableNextRow();
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Total");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ widgets.helpMarker("Total number of scrollable rows including scrollback and active area.");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(2);
+ cimgui.c.ImGui_Text("%d", scrollbar.total);
+
+ cimgui.c.ImGui_TableNextRow();
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Offset");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ widgets.helpMarker("Current scroll position as row offset from the top of scrollback.");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(2);
+ cimgui.c.ImGui_Text("%d", scrollbar.offset);
+
+ cimgui.c.ImGui_TableNextRow();
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Length");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ widgets.helpMarker("Number of rows visible in the viewport.");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(2);
+ cimgui.c.ImGui_Text("%d", scrollbar.len);
+}
+
+fn regionsTable(pages: *PageList) void {
+ if (!cimgui.c.ImGui_BeginTable(
+ "pagelist_regions",
+ 4,
+ cimgui.c.ImGuiTableFlags_BordersInnerV |
+ cimgui.c.ImGuiTableFlags_RowBg |
+ cimgui.c.ImGuiTableFlags_SizingFixedFit,
+ )) return;
+ defer cimgui.c.ImGui_EndTable();
+
+ cimgui.c.ImGui_TableSetupColumn("Region", cimgui.c.ImGuiTableColumnFlags_WidthFixed);
+ cimgui.c.ImGui_TableSetupColumn("", cimgui.c.ImGuiTableColumnFlags_WidthFixed);
+ cimgui.c.ImGui_TableSetupColumn("Top-Left", cimgui.c.ImGuiTableColumnFlags_WidthFixed);
+ cimgui.c.ImGui_TableSetupColumn("Bottom-Right", cimgui.c.ImGuiTableColumnFlags_WidthStretch);
+ cimgui.c.ImGui_TableHeadersRow();
+
+ inline for (comptime std.meta.tags(terminal.point.Tag)) |tag| {
+ regionRow(pages, tag);
+ }
+}
+
+fn regionRow(pages: *const PageList, comptime tag: terminal.point.Tag) void {
+ const tl_pin = pages.getTopLeft(tag);
+ const br_pin = pages.getBottomRight(tag);
+
+ cimgui.c.ImGui_TableNextRow();
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("%s", @tagName(tag).ptr);
+
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ widgets.helpMarker(comptime regionHelpText(tag));
+
+ _ = cimgui.c.ImGui_TableSetColumnIndex(2);
+ if (pages.pointFromPin(tag, tl_pin)) |pt| {
+ const coord = pt.coord();
+ cimgui.c.ImGui_Text("(%d, %d)", coord.x, coord.y);
+ } else {
+ cimgui.c.ImGui_TextDisabled("(n/a)");
+ }
+
+ _ = cimgui.c.ImGui_TableSetColumnIndex(3);
+ if (br_pin) |br| {
+ if (pages.pointFromPin(tag, br)) |pt| {
+ const coord = pt.coord();
+ cimgui.c.ImGui_Text("(%d, %d)", coord.x, coord.y);
+ } else {
+ cimgui.c.ImGui_TextDisabled("(n/a)");
+ }
+ } else {
+ cimgui.c.ImGui_TextDisabled("(empty)");
+ }
+}
+
+fn regionHelpText(comptime tag: terminal.point.Tag) [:0]const u8 {
+ return switch (tag) {
+ .active => "The active area where a running program can jump the cursor " ++
+ "and make changes. This is the 'editable' part of the screen. " ++
+ "Bottom-right includes the full height of the screen, including " ++
+ "rows that may not be written yet.",
+ .viewport => "The visible viewport. If the user has scrolled, top-left changes. " ++
+ "Bottom-right is the last written row from the top-left.",
+ .screen => "Top-left is the furthest back in scrollback history. Bottom-right " ++
+ "is the last written row. Unlike 'active', this only contains " ++
+ "written rows.",
+ .history => "Same top-left as 'screen' but bottom-right is the line just before " ++
+ "the top of 'active'. Contains only the scrollback history.",
+ };
+}
+
+fn trackedPinsTable(pages: *const PageList) void {
+ if (!cimgui.c.ImGui_BeginTable(
+ "tracked_pins",
+ 5,
+ cimgui.c.ImGuiTableFlags_Borders |
+ cimgui.c.ImGuiTableFlags_RowBg |
+ cimgui.c.ImGuiTableFlags_SizingFixedFit,
+ )) return;
+ defer cimgui.c.ImGui_EndTable();
+
+ cimgui.c.ImGui_TableSetupColumn("Index", cimgui.c.ImGuiTableColumnFlags_WidthFixed);
+ cimgui.c.ImGui_TableSetupColumn("Pin", cimgui.c.ImGuiTableColumnFlags_WidthFixed);
+ cimgui.c.ImGui_TableSetupColumn("Context", cimgui.c.ImGuiTableColumnFlags_WidthFixed);
+ cimgui.c.ImGui_TableSetupColumn("Dirty", cimgui.c.ImGuiTableColumnFlags_WidthFixed);
+ cimgui.c.ImGui_TableSetupColumn("State", cimgui.c.ImGuiTableColumnFlags_WidthFixed);
+ cimgui.c.ImGui_TableHeadersRow();
+
+ const active_pin = pages.getTopLeft(.active);
+ const viewport_pin = pages.getTopLeft(.viewport);
+
+ for (pages.trackedPins(), 0..) |tracked, idx| {
+ const pin = tracked.*;
+ cimgui.c.ImGui_TableNextRow();
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("%d", idx);
+
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ if (pin.garbage) {
+ cimgui.c.ImGui_TextColored(.{ .x = 1.0, .y = 0.5, .z = 0.3, .w = 1.0 }, "(%d, %d)", pin.x, pin.y);
+ } else {
+ cimgui.c.ImGui_Text("(%d, %d)", pin.x, pin.y);
+ }
+
+ _ = cimgui.c.ImGui_TableSetColumnIndex(2);
+ if (pages.pointFromPin(.screen, pin)) |pt| {
+ const coord = pt.coord();
+ cimgui.c.ImGui_Text(
+ "screen (%d, %d)",
+ coord.x,
+ coord.y,
+ );
+ } else {
+ cimgui.c.ImGui_TextDisabled("screen (out of range)");
+ }
+
+ _ = cimgui.c.ImGui_TableSetColumnIndex(3);
+ const dirty = pin.isDirty();
+ if (dirty) {
+ cimgui.c.ImGui_TextColored(.{ .x = 1.0, .y = 0.4, .z = 0.4, .w = 1.0 }, "dirty");
+ } else {
+ cimgui.c.ImGui_TextDisabled("clean");
+ }
+
+ _ = cimgui.c.ImGui_TableSetColumnIndex(4);
+ if (pin.eql(active_pin)) {
+ cimgui.c.ImGui_TextColored(.{ .x = 0.4, .y = 0.9, .z = 0.4, .w = 1.0 }, "active top");
+ } else if (pin.eql(viewport_pin)) {
+ cimgui.c.ImGui_TextColored(.{ .x = 0.4, .y = 0.8, .z = 1.0, .w = 1.0 }, "viewport top");
+ } else if (pin.garbage) {
+ cimgui.c.ImGui_TextColored(.{ .x = 1.0, .y = 0.5, .z = 0.3, .w = 1.0 }, "garbage");
+ } else if (tracked == pages.viewport_pin) {
+ cimgui.c.ImGui_Text("viewport pin");
+ } else {
+ cimgui.c.ImGui_TextDisabled("tracked");
+ }
+ }
+}
+
+fn scrollbarWidget(
+ scrollbar: *const PageList.Scrollbar,
+ delta_row: *isize,
+) void {
+ delta_row.* = 0;
+
+ const avail_width = cimgui.c.ImGui_GetContentRegionAvail().x;
+ const bar_height: f32 = cimgui.c.ImGui_GetFrameHeight();
+ const cursor_pos = cimgui.c.ImGui_GetCursorScreenPos();
+
+ const total_f: f32 = @floatFromInt(scrollbar.total);
+ const offset_f: f32 = @floatFromInt(scrollbar.offset);
+ const len_f: f32 = @floatFromInt(scrollbar.len);
+
+ const grab_start = (offset_f / total_f) * avail_width;
+ const grab_width = @max((len_f / total_f) * avail_width, 4.0);
+
+ const draw_list = cimgui.c.ImGui_GetWindowDrawList();
+ const bg_color = cimgui.c.ImGui_GetColorU32(cimgui.c.ImGuiCol_ScrollbarBg);
+ const grab_color = cimgui.c.ImGui_GetColorU32(cimgui.c.ImGuiCol_ScrollbarGrab);
+
+ const bg_min: cimgui.c.ImVec2 = cursor_pos;
+ const bg_max: cimgui.c.ImVec2 = .{ .x = cursor_pos.x + avail_width, .y = cursor_pos.y + bar_height };
+ cimgui.c.ImDrawList_AddRectFilledEx(
+ draw_list,
+ bg_min,
+ bg_max,
+ bg_color,
+ 0,
+ 0,
+ );
+
+ const grab_min: cimgui.c.ImVec2 = .{
+ .x = cursor_pos.x + grab_start,
+ .y = cursor_pos.y,
+ };
+ const grab_max: cimgui.c.ImVec2 = .{
+ .x = cursor_pos.x + grab_start + grab_width,
+ .y = cursor_pos.y + bar_height,
+ };
+ cimgui.c.ImDrawList_AddRectFilledEx(
+ draw_list,
+ grab_min,
+ grab_max,
+ grab_color,
+ 0,
+ 0,
+ );
+ _ = cimgui.c.ImGui_InvisibleButton(
+ "scrollbar_drag",
+ .{ .x = avail_width, .y = bar_height },
+ 0,
+ );
+ if (cimgui.c.ImGui_IsItemActive()) {
+ const drag_delta = cimgui.c.ImGui_GetMouseDragDelta(
+ cimgui.c.ImGuiMouseButton_Left,
+ 0.0,
+ );
+ if (drag_delta.x != 0) {
+ const row_delta = (drag_delta.x / avail_width) * total_f;
+ delta_row.* = @intFromFloat(row_delta);
+ cimgui.c.ImGui_ResetMouseDragDelta();
+ }
+ }
+
+ if (cimgui.c.ImGui_IsItemHovered(cimgui.c.ImGuiHoveredFlags_DelayShort)) {
+ cimgui.c.ImGui_SetTooltip(
+ "offset=%d len=%d total=%d",
+ scrollbar.offset,
+ scrollbar.len,
+ scrollbar.total,
+ );
+ }
+}
+
+/// Grid inspector widget for choosing and inspecting a specific cell.
+pub const CellChooser = struct {
+ lookup_region: terminal.point.Tag,
+ lookup_coord: terminal.point.Coordinate,
+ cell_info: CellInfo,
+
+ pub const empty: CellChooser = .{
+ .lookup_region = .viewport,
+ .lookup_coord = .{ .x = 0, .y = 0 },
+ .cell_info = .empty,
+ };
+
+ pub fn draw(
+ self: *CellChooser,
+ pages: *const PageList,
+ ) void {
+ cimgui.c.ImGui_TextWrapped(
+ "Inspect a cell by choosing a coordinate space and entering the X/Y position. " ++
+ "The inspector resolves the point into the page list and displays the cell contents.",
+ );
+
+ cimgui.c.ImGui_SeparatorText("Cell Inspector");
+
+ const region_max = maxCoord(pages, self.lookup_region);
+ if (region_max) |coord| {
+ self.lookup_coord.x = @min(self.lookup_coord.x, coord.x);
+ self.lookup_coord.y = @min(self.lookup_coord.y, coord.y);
+ } else {
+ self.lookup_coord = .{ .x = 0, .y = 0 };
+ }
+
+ {
+ const disabled = region_max == null;
+ cimgui.c.ImGui_BeginDisabled(disabled);
+ defer cimgui.c.ImGui_EndDisabled();
+
+ const preview = @tagName(self.lookup_region);
+ const combo_width = comptime blk: {
+ var max_len: usize = 0;
+ for (std.meta.tags(terminal.point.Tag)) |tag| {
+ max_len = @max(max_len, @tagName(tag).len);
+ }
+ break :blk max_len + 4;
+ };
+ cimgui.c.ImGui_SetNextItemWidth(cimgui.c.ImGui_CalcTextSize("X" ** combo_width).x);
+ if (cimgui.c.ImGui_BeginCombo(
+ "##grid_region",
+ preview.ptr,
+ cimgui.c.ImGuiComboFlags_HeightSmall,
+ )) {
+ inline for (comptime std.meta.tags(terminal.point.Tag)) |tag| {
+ const selected = tag == self.lookup_region;
+ if (cimgui.c.ImGui_SelectableEx(
+ @tagName(tag).ptr,
+ selected,
+ cimgui.c.ImGuiSelectableFlags_None,
+ .{ .x = 0, .y = 0 },
+ )) {
+ self.lookup_region = tag;
+ }
+ if (selected) cimgui.c.ImGui_SetItemDefaultFocus();
+ }
+ cimgui.c.ImGui_EndCombo();
+ }
+
+ cimgui.c.ImGui_SameLine();
+
+ const width = cimgui.c.ImGui_CalcTextSize("00000").x;
+ var x_value: terminal.size.CellCountInt = self.lookup_coord.x;
+ var y_value: u32 = self.lookup_coord.y;
+ var changed = false;
+
+ cimgui.c.ImGui_AlignTextToFramePadding();
+ cimgui.c.ImGui_Text("x:");
+ cimgui.c.ImGui_SameLine();
+ cimgui.c.ImGui_SetNextItemWidth(width);
+ if (cimgui.c.ImGui_InputScalar(
+ "##grid_x",
+ cimgui.c.ImGuiDataType_U16,
+ &x_value,
+ )) changed = true;
+
+ cimgui.c.ImGui_SameLine();
+ cimgui.c.ImGui_AlignTextToFramePadding();
+ cimgui.c.ImGui_Text("y:");
+ cimgui.c.ImGui_SameLine();
+ cimgui.c.ImGui_SetNextItemWidth(width);
+ if (cimgui.c.ImGui_InputScalar(
+ "##grid_y",
+ cimgui.c.ImGuiDataType_U32,
+ &y_value,
+ )) changed = true;
+
+ cimgui.c.ImGui_SameLine();
+ widgets.helpMarker("Choose the coordinate space and X/Y position (0-indexed).");
+
+ if (changed) {
+ if (region_max) |coord| {
+ self.lookup_coord.x = @min(x_value, coord.x);
+ self.lookup_coord.y = @min(y_value, coord.y);
+ }
+ }
+ }
+
+ if (region_max) |coord| {
+ cimgui.c.ImGui_TextDisabled(
+ "Range: x 0..%d, y 0..%d",
+ coord.x,
+ coord.y,
+ );
+ } else {
+ cimgui.c.ImGui_TextDisabled("(region has no rows)");
+ return;
+ }
+
+ const pt = switch (self.lookup_region) {
+ .active => terminal.Point{ .active = self.lookup_coord },
+ .viewport => terminal.Point{ .viewport = self.lookup_coord },
+ .screen => terminal.Point{ .screen = self.lookup_coord },
+ .history => terminal.Point{ .history = self.lookup_coord },
+ };
+
+ const cell = pages.getCell(pt) orelse {
+ cimgui.c.ImGui_TextDisabled("(cell out of range)");
+ return;
+ };
+
+ self.cell_info.draw(cell, pt);
+
+ if (cell.cell.style_id != stylepkg.default_id) {
+ cimgui.c.ImGui_SeparatorText("Style");
+ const style = cell.node.data.styles.get(
+ cell.node.data.memory,
+ cell.cell.style_id,
+ ).*;
+ widgets.style.table(style, null);
+ }
+
+ if (cell.cell.hyperlink) {
+ cimgui.c.ImGui_SeparatorText("Hyperlink");
+ hyperlinkTable(cell);
+ }
+
+ if (cell.cell.hasGrapheme()) {
+ cimgui.c.ImGui_SeparatorText("Grapheme");
+ graphemeTable(cell);
+ }
+ }
+};
+
+fn maxCoord(
+ pages: *const PageList,
+ tag: terminal.point.Tag,
+) ?terminal.point.Coordinate {
+ const br_pin = pages.getBottomRight(tag) orelse return null;
+ const br_point = pages.pointFromPin(tag, br_pin) orelse return null;
+ return br_point.coord();
+}
+
+fn hyperlinkTable(cell: PageList.Cell) void {
+ if (!cimgui.c.ImGui_BeginTable(
+ "cell_hyperlink",
+ 2,
+ cimgui.c.ImGuiTableFlags_None,
+ )) return;
+ defer cimgui.c.ImGui_EndTable();
+
+ const page = &cell.node.data;
+ const link_id = page.lookupHyperlink(cell.cell) orelse {
+ cimgui.c.ImGui_TableNextRow();
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Status");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ cimgui.c.ImGui_TextDisabled("(missing link data)");
+ return;
+ };
+
+ const entry = page.hyperlink_set.get(page.memory, link_id);
+
+ cimgui.c.ImGui_TableNextRow();
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("ID");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ switch (entry.id) {
+ .implicit => |value| cimgui.c.ImGui_Text("implicit %d", value),
+ .explicit => |slice| {
+ const id = slice.slice(page.memory);
+ if (id.len == 0) {
+ cimgui.c.ImGui_TextDisabled("(empty)");
+ } else {
+ cimgui.c.ImGui_Text("%.*s", id.len, id.ptr);
+ }
+ },
+ }
+
+ cimgui.c.ImGui_TableNextRow();
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("URI");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ const uri = entry.uri.slice(page.memory);
+ if (uri.len == 0) {
+ cimgui.c.ImGui_TextDisabled("(empty)");
+ } else {
+ cimgui.c.ImGui_Text("%.*s", uri.len, uri.ptr);
+ }
+
+ cimgui.c.ImGui_TableNextRow();
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Ref Count");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ const refs = page.hyperlink_set.refCount(page.memory, link_id);
+ cimgui.c.ImGui_Text("%d", refs);
+}
+
+fn graphemeTable(cell: PageList.Cell) void {
+ if (!cimgui.c.ImGui_BeginTable(
+ "cell_grapheme",
+ 2,
+ cimgui.c.ImGuiTableFlags_None,
+ )) return;
+ defer cimgui.c.ImGui_EndTable();
+
+ const page = &cell.node.data;
+ const cps = page.lookupGrapheme(cell.cell) orelse {
+ cimgui.c.ImGui_TableNextRow();
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Status");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ cimgui.c.ImGui_TextDisabled("(missing grapheme data)");
+ return;
+ };
+
+ cimgui.c.ImGui_TableNextRow();
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Extra Codepoints");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ if (cps.len == 0) {
+ cimgui.c.ImGui_TextDisabled("(none)");
+ return;
+ }
+
+ var buf: [96]u8 = undefined;
+ if (cimgui.c.ImGui_BeginListBox("##grapheme_list", .{ .x = 0, .y = 0 })) {
+ defer cimgui.c.ImGui_EndListBox();
+ for (cps) |cp| {
+ const label = std.fmt.bufPrintZ(&buf, "U+{X}", .{cp}) catch "U+?";
+ _ = cimgui.c.ImGui_SelectableEx(
+ label.ptr,
+ false,
+ cimgui.c.ImGuiSelectableFlags_None,
+ .{ .x = 0, .y = 0 },
+ );
+ }
+ }
+}
+
+/// Cell inspector widget.
+pub const CellInfo = struct {
+ pub const empty: CellInfo = .{};
+
+ pub fn draw(
+ _: *const CellInfo,
+ cell: PageList.Cell,
+ point: terminal.Point,
+ ) void {
+ if (!cimgui.c.ImGui_BeginTable(
+ "cell_info",
+ 3,
+ cimgui.c.ImGuiTableFlags_BordersInnerV |
+ cimgui.c.ImGuiTableFlags_RowBg |
+ cimgui.c.ImGuiTableFlags_SizingFixedFit,
+ )) return;
+ defer cimgui.c.ImGui_EndTable();
+
+ {
+ cimgui.c.ImGui_TableNextRow();
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Grid Position");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ widgets.helpMarker("The cell's X/Y coordinates in the selected region.");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(2);
+ const coord = point.coord();
+ cimgui.c.ImGui_Text("(%d, %d)", coord.x, coord.y);
+ }
+
+ {
+ cimgui.c.ImGui_TableNextRow();
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Page Location");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ widgets.helpMarker("Row and column indices within the backing page.");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(2);
+ cimgui.c.ImGui_Text("row=%d col=%d", cell.row_idx, cell.col_idx);
+ }
+
+ {
+ cimgui.c.ImGui_TableNextRow();
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Content");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ widgets.helpMarker("Content tag describing how the cell data is stored.");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(2);
+ cimgui.c.ImGui_Text("%s", @tagName(cell.cell.content_tag).ptr);
+ }
+
+ {
+ cimgui.c.ImGui_TableNextRow();
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Codepoint");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ widgets.helpMarker("Primary Unicode codepoint for the cell.");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(2);
+ const cp = cell.cell.codepoint();
+ if (cp == 0) {
+ cimgui.c.ImGui_TextDisabled("(empty)");
+ } else {
+ cimgui.c.ImGui_Text("U+%04X", @as(u32, cp));
+ }
+ }
+
+ if (cell.cell.hasGrapheme()) {
+ cimgui.c.ImGui_TableNextRow();
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Grapheme");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ widgets.helpMarker("Extra codepoints that combine with the primary codepoint to form the grapheme cluster.");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(2);
+ if (cimgui.c.ImGui_BeginListBox("##cell_grapheme", .{ .x = 0, .y = 0 })) {
+ defer cimgui.c.ImGui_EndListBox();
+ if (cell.node.data.lookupGrapheme(cell.cell)) |cps| {
+ var buf: [96]u8 = undefined;
+ for (cps) |cp| {
+ const label = std.fmt.bufPrintZ(&buf, "U+{X}", .{cp}) catch "U+?";
+ _ = cimgui.c.ImGui_SelectableEx(
+ label.ptr,
+ false,
+ cimgui.c.ImGuiSelectableFlags_None,
+ .{ .x = 0, .y = 0 },
+ );
+ }
+ } else {
+ _ = cimgui.c.ImGui_SelectableEx(
+ "(missing)",
+ false,
+ cimgui.c.ImGuiSelectableFlags_None,
+ .{ .x = 0, .y = 0 },
+ );
+ }
+ }
+ }
+
+ {
+ cimgui.c.ImGui_TableNextRow();
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Width Property");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ widgets.helpMarker("Character width property (narrow, wide, spacer, etc.).");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(2);
+ cimgui.c.ImGui_Text("%s", @tagName(cell.cell.wide).ptr);
+ }
+
+ {
+ cimgui.c.ImGui_TableNextRow();
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Row Flags");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ widgets.helpMarker("Flags set on the row containing this cell.");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(2);
+ const row = cell.row;
+ if (row.wrap or row.wrap_continuation or row.grapheme or row.styled or row.hyperlink) {
+ if (row.wrap) {
+ cimgui.c.ImGui_TextColored(.{ .x = 0.4, .y = 0.8, .z = 1.0, .w = 1.0 }, "wrap");
+ cimgui.c.ImGui_SameLine();
+ }
+ if (row.wrap_continuation) {
+ cimgui.c.ImGui_TextColored(.{ .x = 0.4, .y = 0.8, .z = 1.0, .w = 1.0 }, "cont");
+ cimgui.c.ImGui_SameLine();
+ }
+ if (row.grapheme) {
+ cimgui.c.ImGui_TextColored(.{ .x = 0.9, .y = 0.7, .z = 0.3, .w = 1.0 }, "grapheme");
+ cimgui.c.ImGui_SameLine();
+ }
+ if (row.styled) {
+ cimgui.c.ImGui_TextColored(.{ .x = 0.7, .y = 0.9, .z = 0.5, .w = 1.0 }, "styled");
+ cimgui.c.ImGui_SameLine();
+ }
+ if (row.hyperlink) {
+ cimgui.c.ImGui_TextColored(.{ .x = 0.8, .y = 0.6, .z = 1.0, .w = 1.0 }, "link");
+ cimgui.c.ImGui_SameLine();
+ }
+ } else {
+ cimgui.c.ImGui_TextDisabled("(none)");
+ }
+ }
+
+ {
+ cimgui.c.ImGui_TableNextRow();
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Style ID");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ widgets.helpMarker("Internal style reference ID for this cell.");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(2);
+ cimgui.c.ImGui_Text("%d", cell.cell.style_id);
+ }
+
+ {
+ cimgui.c.ImGui_TableNextRow();
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Style");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ widgets.helpMarker("Resolved style for the cell (colors, attributes, etc.).");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(2);
+ if (cell.cell.style_id == stylepkg.default_id) {
+ cimgui.c.ImGui_TextDisabled("(default)");
+ } else {
+ cimgui.c.ImGui_TextDisabled("(see below)");
+ }
+ }
+
+ if (cell.cell.hyperlink) {
+ cimgui.c.ImGui_TableNextRow();
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Hyperlink");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ widgets.helpMarker("OSC8 hyperlink ID associated with this cell.");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(2);
+
+ const link_id = cell.node.data.lookupHyperlink(cell.cell) orelse 0;
+ cimgui.c.ImGui_Text("id=%d", link_id);
+ }
+ }
+};
diff --git a/vendor/libghostty-vt/src/inspector/widgets/renderer.zig b/vendor/libghostty-vt/src/inspector/widgets/renderer.zig
new file mode 100644
index 00000000..1003b02c
--- /dev/null
+++ b/vendor/libghostty-vt/src/inspector/widgets/renderer.zig
@@ -0,0 +1,123 @@
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const cimgui = @import("dcimgui");
+const widgets = @import("../widgets.zig");
+const renderer = @import("../../renderer.zig");
+
+const log = std.log.scoped(.inspector_renderer);
+
+/// Renderer information inspector widget.
+pub const Info = struct {
+ features: std.AutoArrayHashMapUnmanaged(
+ std.meta.Tag(renderer.Overlay.Feature),
+ renderer.Overlay.Feature,
+ ),
+
+ pub const empty: Info = .{
+ .features = .empty,
+ };
+
+ pub fn deinit(self: *Info, alloc: Allocator) void {
+ self.features.deinit(alloc);
+ }
+
+ /// Grab the features into a new allocated slice. This is used by
+ pub fn overlayFeatures(
+ self: *const Info,
+ alloc: Allocator,
+ ) Allocator.Error![]renderer.Overlay.Feature {
+ // The features from our internal state.
+ const features = self.features.values();
+
+ // For now we do a dumb copy since the features have no managed
+ // memory.
+ const result = try alloc.dupe(
+ renderer.Overlay.Feature,
+ features,
+ );
+ errdefer alloc.free(result);
+
+ return result;
+ }
+
+ /// Draw the renderer info window.
+ pub fn draw(
+ self: *Info,
+ alloc: Allocator,
+ open: bool,
+ ) void {
+ if (!open) return;
+
+ cimgui.c.ImGui_SetNextItemOpen(true, cimgui.c.ImGuiCond_Once);
+ if (!cimgui.c.ImGui_CollapsingHeader("Overlays", cimgui.c.ImGuiTreeNodeFlags_None)) return;
+
+ cimgui.c.ImGui_SeparatorText("Hyperlinks");
+ self.overlayHyperlinks(alloc);
+ cimgui.c.ImGui_SeparatorText("Semantic Prompts");
+ self.overlaySemanticPrompts(alloc);
+ }
+
+ fn overlayHyperlinks(self: *Info, alloc: Allocator) void {
+ var hyperlinks: bool = self.features.contains(.highlight_hyperlinks);
+ _ = cimgui.c.ImGui_Checkbox("Overlay Hyperlinks", &hyperlinks);
+ cimgui.c.ImGui_SameLine();
+ widgets.helpMarker("When enabled, highlights OSC8 hyperlinks.");
+
+ if (!hyperlinks) {
+ _ = self.features.swapRemove(.highlight_hyperlinks);
+ } else {
+ self.features.put(
+ alloc,
+ .highlight_hyperlinks,
+ .highlight_hyperlinks,
+ ) catch log.warn("error enabling hyperlink overlay feature", .{});
+ }
+ }
+
+ fn overlaySemanticPrompts(self: *Info, alloc: Allocator) void {
+ var semantic_prompts: bool = self.features.contains(.semantic_prompts);
+ _ = cimgui.c.ImGui_Checkbox("Overlay Semantic Prompts", &semantic_prompts);
+ cimgui.c.ImGui_SameLine();
+ widgets.helpMarker("When enabled, highlights OSC 133 semantic prompts.");
+
+ // Handle the checkbox results
+ if (!semantic_prompts) {
+ _ = self.features.swapRemove(.semantic_prompts);
+ } else {
+ self.features.put(
+ alloc,
+ .semantic_prompts,
+ .semantic_prompts,
+ ) catch log.warn("error enabling semantic prompt overlay feature", .{});
+ }
+
+ // Help
+ cimgui.c.ImGui_Indent();
+ defer cimgui.c.ImGui_Unindent();
+
+ cimgui.c.ImGui_TextDisabled("Colors:");
+
+ const prompt_rgb = renderer.Overlay.Color.semantic_prompt.rgb();
+ const input_rgb = renderer.Overlay.Color.semantic_input.rgb();
+ const prompt_col: cimgui.c.ImVec4 = .{
+ .x = @as(f32, @floatFromInt(prompt_rgb.r)) / 255.0,
+ .y = @as(f32, @floatFromInt(prompt_rgb.g)) / 255.0,
+ .z = @as(f32, @floatFromInt(prompt_rgb.b)) / 255.0,
+ .w = 1.0,
+ };
+ const input_col: cimgui.c.ImVec4 = .{
+ .x = @as(f32, @floatFromInt(input_rgb.r)) / 255.0,
+ .y = @as(f32, @floatFromInt(input_rgb.g)) / 255.0,
+ .z = @as(f32, @floatFromInt(input_rgb.b)) / 255.0,
+ .w = 1.0,
+ };
+
+ _ = cimgui.c.ImGui_ColorButton("##prompt_color", prompt_col, cimgui.c.ImGuiColorEditFlags_NoTooltip);
+ cimgui.c.ImGui_SameLine();
+ cimgui.c.ImGui_Text("Prompt");
+
+ _ = cimgui.c.ImGui_ColorButton("##input_color", input_col, cimgui.c.ImGuiColorEditFlags_NoTooltip);
+ cimgui.c.ImGui_SameLine();
+ cimgui.c.ImGui_Text("Input");
+ }
+};
diff --git a/vendor/libghostty-vt/src/inspector/widgets/screen.zig b/vendor/libghostty-vt/src/inspector/widgets/screen.zig
new file mode 100644
index 00000000..bfe961f4
--- /dev/null
+++ b/vendor/libghostty-vt/src/inspector/widgets/screen.zig
@@ -0,0 +1,400 @@
+const std = @import("std");
+const builtin = @import("builtin");
+const assert = @import("../../quirks.zig").inlineAssert;
+const Allocator = std.mem.Allocator;
+const cimgui = @import("dcimgui");
+const widgets = @import("../widgets.zig");
+const units = @import("../units.zig");
+const terminal = @import("../../terminal/main.zig");
+const stylepkg = @import("../../terminal/style.zig");
+
+/// Window names for the screen dockspace.
+const window_info = "Info";
+const window_cell = "Cell";
+const window_pagelist = "PageList";
+
+/// Screen information inspector widget.
+pub const Info = struct {
+ pagelist: widgets.pagelist.Inspector,
+ cell_chooser: widgets.pagelist.CellChooser,
+
+ pub const empty: Info = .{
+ .pagelist = .empty,
+ .cell_chooser = .empty,
+ };
+
+ /// Draw the screen info contents.
+ pub fn draw(self: *Info, open: bool, data: struct {
+ /// The screen that we're inspecting.
+ screen: *terminal.Screen,
+
+ /// Which screen key we're viewing.
+ key: terminal.ScreenSet.Key,
+
+ /// Which screen is active (primary or alternate).
+ active_key: terminal.ScreenSet.Key,
+
+ /// Whether xterm modify other keys mode 2 is enabled.
+ modify_other_keys_2: bool,
+
+ /// Color palette for cursor color resolution.
+ color_palette: *const terminal.color.DynamicPalette,
+ }) void {
+ // Create the dockspace for this screen
+ const dockspace_id = cimgui.c.ImGui_GetID("Screen Dockspace");
+ _ = createDockSpace(dockspace_id);
+
+ const screen = data.screen;
+
+ // Info window
+ info: {
+ defer cimgui.c.ImGui_End();
+ if (!cimgui.c.ImGui_Begin(
+ window_info,
+ null,
+ cimgui.c.ImGuiWindowFlags_NoFocusOnAppearing,
+ )) break :info;
+
+ if (cimgui.c.ImGui_CollapsingHeader(
+ "Cursor",
+ cimgui.c.ImGuiTreeNodeFlags_DefaultOpen,
+ )) {
+ cursorTable(&screen.cursor);
+ cimgui.c.ImGui_Separator();
+ cursorStyle(
+ &screen.cursor,
+ &data.color_palette.current,
+ );
+ }
+
+ if (cimgui.c.ImGui_CollapsingHeader(
+ "Keyboard",
+ cimgui.c.ImGuiTreeNodeFlags_DefaultOpen,
+ )) keyboardTable(
+ screen,
+ data.modify_other_keys_2,
+ );
+
+ if (cimgui.c.ImGui_CollapsingHeader(
+ "Semantic Prompt",
+ cimgui.c.ImGuiTreeNodeFlags_DefaultOpen,
+ )) semanticPromptTable(&screen.semantic_prompt);
+
+ if (cimgui.c.ImGui_CollapsingHeader(
+ "Kitty Graphics",
+ cimgui.c.ImGuiTreeNodeFlags_DefaultOpen,
+ )) kittyGraphicsTable(&screen.kitty_images);
+
+ if (cimgui.c.ImGui_CollapsingHeader(
+ "Other Screen State",
+ cimgui.c.ImGuiTreeNodeFlags_DefaultOpen,
+ )) internalStateTable(screen);
+ }
+
+ // Cell window
+ cell: {
+ defer cimgui.c.ImGui_End();
+ if (!cimgui.c.ImGui_Begin(
+ window_cell,
+ null,
+ cimgui.c.ImGuiWindowFlags_NoFocusOnAppearing,
+ )) break :cell;
+ self.cell_chooser.draw(&screen.pages);
+ }
+
+ // PageList window
+ pagelist: {
+ defer cimgui.c.ImGui_End();
+ if (!cimgui.c.ImGui_Begin(
+ window_pagelist,
+ null,
+ cimgui.c.ImGuiWindowFlags_NoFocusOnAppearing,
+ )) break :pagelist;
+ self.pagelist.draw(&screen.pages);
+ }
+
+ // The remainder is the open state
+ if (!open) return;
+
+ // Show warning if viewing an inactive screen
+ if (data.key != data.active_key) {
+ cimgui.c.ImGui_TextColored(
+ .{ .x = 1.0, .y = 0.8, .z = 0.0, .w = 1.0 },
+ "âš  Viewing inactive screen",
+ );
+ cimgui.c.ImGui_Separator();
+ }
+ }
+
+ /// Create the dock space for the screen inspector. This creates
+ /// a dedicated dock space for the screen inspector windows. But they
+ /// can of course be undocked and moved around as desired.
+ fn createDockSpace(dockspace_id: cimgui.c.ImGuiID) bool {
+ // Check if we need to set up the dockspace
+ const setup = cimgui.ImGui_DockBuilderGetNode(dockspace_id) == null;
+
+ if (setup) {
+ // Register our dockspace node
+ assert(cimgui.ImGui_DockBuilderAddNodeEx(
+ dockspace_id,
+ cimgui.ImGuiDockNodeFlagsPrivate.DockSpace,
+ ) == dockspace_id);
+
+ // Dock windows into the space
+ cimgui.ImGui_DockBuilderDockWindow(window_info, dockspace_id);
+ cimgui.ImGui_DockBuilderDockWindow(window_cell, dockspace_id);
+ cimgui.ImGui_DockBuilderDockWindow(window_pagelist, dockspace_id);
+ cimgui.ImGui_DockBuilderFinish(dockspace_id);
+ }
+
+ // Create the dockspace
+ assert(cimgui.c.ImGui_DockSpaceEx(
+ dockspace_id,
+ .{ .x = 0, .y = 0 },
+ cimgui.c.ImGuiDockNodeFlags_None,
+ null,
+ ) == dockspace_id);
+ return setup;
+ }
+};
+
+/// Render cursor state with a table of cursor-specific fields.
+pub fn cursorTable(
+ cursor: *const terminal.Screen.Cursor,
+) void {
+ if (!cimgui.c.ImGui_BeginTable(
+ "table_cursor",
+ 2,
+ cimgui.c.ImGuiTableFlags_None,
+ )) return;
+ defer cimgui.c.ImGui_EndTable();
+
+ cimgui.c.ImGui_TableNextRow();
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Position (x, y)");
+ cimgui.c.ImGui_SameLine();
+ widgets.helpMarker("The current cursor position in the terminal grid (0-indexed).");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ cimgui.c.ImGui_Text("(%d, %d)", cursor.x, cursor.y);
+
+ cimgui.c.ImGui_TableNextRow();
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Hyperlink");
+ cimgui.c.ImGui_SameLine();
+ widgets.helpMarker("The active OSC8 hyperlink for newly printed characters.");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ if (cursor.hyperlink) |link| {
+ cimgui.c.ImGui_Text("%.*s", link.uri.len, link.uri.ptr);
+ } else {
+ cimgui.c.ImGui_TextDisabled("(none)");
+ }
+
+ {
+ cimgui.c.ImGui_TableNextRow();
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Pending Wrap");
+ cimgui.c.ImGui_SameLine();
+ widgets.helpMarker("The 'last column flag' (LCF). If set, the next character will force a soft-wrap to the next line.");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ var value: bool = cursor.pending_wrap;
+ _ = cimgui.c.ImGui_Checkbox("##pending_wrap", &value);
+ }
+
+ {
+ cimgui.c.ImGui_TableNextRow();
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Protected");
+ cimgui.c.ImGui_SameLine();
+ widgets.helpMarker("If enabled, new characters will have the protected attribute set, preventing erasure by certain sequences.");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ var value: bool = cursor.protected;
+ _ = cimgui.c.ImGui_Checkbox("##protected", &value);
+ }
+}
+
+/// Render cursor style information using the shared style table.
+pub fn cursorStyle(cursor: *const terminal.Screen.Cursor, palette: ?*const terminal.color.Palette) void {
+ widgets.style.table(cursor.style, palette);
+}
+
+/// Render keyboard information with a table.
+fn keyboardTable(
+ screen: *const terminal.Screen,
+ modify_other_keys_2: bool,
+) void {
+ if (!cimgui.c.ImGui_BeginTable(
+ "table_keyboard",
+ 2,
+ cimgui.c.ImGuiTableFlags_None,
+ )) return;
+ defer cimgui.c.ImGui_EndTable();
+
+ const kitty_flags = screen.kitty_keyboard.current();
+
+ {
+ cimgui.c.ImGui_TableNextRow();
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Mode");
+ }
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ const mode = if (kitty_flags.int() != 0) "kitty" else "legacy";
+ cimgui.c.ImGui_Text("%s", mode.ptr);
+ }
+ }
+
+ if (kitty_flags.int() != 0) {
+ const Flags = @TypeOf(kitty_flags);
+ inline for (@typeInfo(Flags).@"struct".fields) |field| {
+ {
+ const value = @field(kitty_flags, field.name);
+
+ cimgui.c.ImGui_TableNextRow();
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ const field_name = std.fmt.comptimePrint("{s}", .{field.name});
+ cimgui.c.ImGui_Text("%s", field_name.ptr);
+ }
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ cimgui.c.ImGui_Text(
+ "%s",
+ if (value) "true".ptr else "false".ptr,
+ );
+ }
+ }
+ }
+ } else {
+ {
+ cimgui.c.ImGui_TableNextRow();
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Xterm modify keys");
+ }
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ cimgui.c.ImGui_Text(
+ "%s",
+ if (modify_other_keys_2) "true".ptr else "false".ptr,
+ );
+ }
+ }
+ } // keyboard mode info
+}
+
+/// Render kitty graphics information table.
+pub fn kittyGraphicsTable(
+ kitty_images: *const terminal.kitty.graphics.ImageStorage,
+) void {
+ if (!kitty_images.enabled()) {
+ cimgui.c.ImGui_TextDisabled("(Kitty graphics are disabled)");
+ return;
+ }
+
+ if (!cimgui.c.ImGui_BeginTable(
+ "##kitty_graphics",
+ 2,
+ cimgui.c.ImGuiTableFlags_None,
+ )) return;
+ defer cimgui.c.ImGui_EndTable();
+
+ cimgui.c.ImGui_TableNextRow();
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Memory Usage");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ cimgui.c.ImGui_Text("%d bytes (%d KiB)", kitty_images.total_bytes, units.toKibiBytes(kitty_images.total_bytes));
+
+ cimgui.c.ImGui_TableNextRow();
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Memory Limit");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ cimgui.c.ImGui_Text("%d bytes (%d KiB)", kitty_images.total_limit, units.toKibiBytes(kitty_images.total_limit));
+
+ cimgui.c.ImGui_TableNextRow();
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Image Count");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ cimgui.c.ImGui_Text("%d", kitty_images.images.count());
+
+ cimgui.c.ImGui_TableNextRow();
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Placement Count");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ cimgui.c.ImGui_Text("%d", kitty_images.placements.count());
+
+ cimgui.c.ImGui_TableNextRow();
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Image Loading");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ cimgui.c.ImGui_Text("%s", if (kitty_images.loading != null) "true".ptr else "false".ptr);
+}
+
+/// Render internal terminal state table.
+pub fn internalStateTable(
+ screen: *const terminal.Screen,
+) void {
+ const pages = &screen.pages;
+
+ if (!cimgui.c.ImGui_BeginTable(
+ "##terminal_state",
+ 2,
+ cimgui.c.ImGuiTableFlags_None,
+ )) return;
+ defer cimgui.c.ImGui_EndTable();
+
+ cimgui.c.ImGui_TableNextRow();
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Memory Usage");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ cimgui.c.ImGui_Text("%d bytes (%d KiB)", pages.page_size, units.toKibiBytes(pages.page_size));
+
+ cimgui.c.ImGui_TableNextRow();
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Memory Limit");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ cimgui.c.ImGui_Text("%d bytes (%d KiB)", pages.maxSize(), units.toKibiBytes(pages.maxSize()));
+
+ cimgui.c.ImGui_TableNextRow();
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Viewport Location");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ cimgui.c.ImGui_Text("%s", @tagName(pages.viewport).ptr);
+}
+
+/// Render semantic prompt state table.
+pub fn semanticPromptTable(
+ semantic_prompt: *const terminal.Screen.SemanticPrompt,
+) void {
+ if (!cimgui.c.ImGui_BeginTable(
+ "##semantic_prompt",
+ 2,
+ cimgui.c.ImGuiTableFlags_None,
+ )) return;
+ defer cimgui.c.ImGui_EndTable();
+
+ {
+ cimgui.c.ImGui_TableNextRow();
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Seen");
+ cimgui.c.ImGui_SameLine();
+ widgets.helpMarker("Whether any semantic prompt markers (OSC 133) have been seen in this screen.");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ var value: bool = semantic_prompt.seen;
+ _ = cimgui.c.ImGui_Checkbox("##seen", &value);
+ }
+
+ {
+ cimgui.c.ImGui_TableNextRow();
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Click Handling");
+ cimgui.c.ImGui_SameLine();
+ widgets.helpMarker("How click events are handled in prompts. Set via 'cl' or 'click_events' options in OSC 133.");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ switch (semantic_prompt.click) {
+ .none => cimgui.c.ImGui_TextDisabled("(none)"),
+ .click_events => cimgui.c.ImGui_Text("click_events"),
+ .cl => |cl| cimgui.c.ImGui_Text("cl=%s", @tagName(cl).ptr),
+ }
+ }
+}
diff --git a/vendor/libghostty-vt/src/inspector/widgets/style.zig b/vendor/libghostty-vt/src/inspector/widgets/style.zig
new file mode 100644
index 00000000..bdc1816e
--- /dev/null
+++ b/vendor/libghostty-vt/src/inspector/widgets/style.zig
@@ -0,0 +1,125 @@
+const std = @import("std");
+const cimgui = @import("dcimgui");
+const terminal = @import("../../terminal/main.zig");
+const widgets = @import("../widgets.zig");
+
+/// Render a style as a table.
+pub fn table(
+ st: terminal.Style,
+ palette: ?*const terminal.color.Palette,
+) void {
+ {
+ _ = cimgui.c.ImGui_BeginTable(
+ "style",
+ 2,
+ cimgui.c.ImGuiTableFlags_None,
+ );
+ defer cimgui.c.ImGui_EndTable();
+ {
+ cimgui.c.ImGui_TableNextRow();
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Foreground");
+ cimgui.c.ImGui_SameLine();
+ widgets.helpMarker("The foreground (text) color");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ color("fg", st.fg_color, palette);
+ }
+
+ {
+ cimgui.c.ImGui_TableNextRow();
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Background");
+ cimgui.c.ImGui_SameLine();
+ widgets.helpMarker("The background (cell) color");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ color("bg", st.bg_color, palette);
+ }
+
+ {
+ cimgui.c.ImGui_TableNextRow();
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Underline");
+ cimgui.c.ImGui_SameLine();
+ widgets.helpMarker("The underline color, if underlines are enabled.");
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ color("underline", st.underline_color, palette);
+ }
+
+ const style_flags = .{
+ .{ "bold", "Text will be rendered with bold weight." },
+ .{ "italic", "Text will be rendered in italic style." },
+ .{ "faint", "Text will be rendered with reduced intensity." },
+ .{ "blink", "Text will blink." },
+ .{ "inverse", "Foreground and background colors are swapped." },
+ .{ "invisible", "Text will be invisible (hidden)." },
+ .{ "strikethrough", "Text will have a line through it." },
+ };
+ inline for (style_flags) |entry| entry: {
+ const style = entry[0];
+ const help = entry[1];
+ if (!@field(st.flags, style)) break :entry;
+
+ cimgui.c.ImGui_TableNextRow();
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text(style.ptr);
+ cimgui.c.ImGui_SameLine();
+ widgets.helpMarker(help);
+ }
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ cimgui.c.ImGui_Text("true");
+ }
+ }
+ }
+
+ cimgui.c.ImGui_TextDisabled("(Any styles not shown are not currently set)");
+}
+
+/// Render a style color.
+pub fn color(
+ id: [:0]const u8,
+ c: terminal.Style.Color,
+ palette: ?*const terminal.color.Palette,
+) void {
+ cimgui.c.ImGui_PushID(id);
+ defer cimgui.c.ImGui_PopID();
+
+ switch (c) {
+ .none => cimgui.c.ImGui_Text("default"),
+
+ .palette => |idx| {
+ cimgui.c.ImGui_Text("Palette %d", idx);
+ if (palette) |p| {
+ const rgb = p[idx];
+ var data: [3]f32 = .{
+ @as(f32, @floatFromInt(rgb.r)) / 255,
+ @as(f32, @floatFromInt(rgb.g)) / 255,
+ @as(f32, @floatFromInt(rgb.b)) / 255,
+ };
+ _ = cimgui.c.ImGui_ColorEdit3(
+ "color_fg",
+ &data,
+ cimgui.c.ImGuiColorEditFlags_DisplayHex |
+ cimgui.c.ImGuiColorEditFlags_NoPicker |
+ cimgui.c.ImGuiColorEditFlags_NoLabel,
+ );
+ }
+ },
+
+ .rgb => |rgb| {
+ var data: [3]f32 = .{
+ @as(f32, @floatFromInt(rgb.r)) / 255,
+ @as(f32, @floatFromInt(rgb.g)) / 255,
+ @as(f32, @floatFromInt(rgb.b)) / 255,
+ };
+ _ = cimgui.c.ImGui_ColorEdit3(
+ "color_fg",
+ &data,
+ cimgui.c.ImGuiColorEditFlags_DisplayHex |
+ cimgui.c.ImGuiColorEditFlags_NoPicker |
+ cimgui.c.ImGuiColorEditFlags_NoLabel,
+ );
+ },
+ }
+}
diff --git a/vendor/libghostty-vt/src/inspector/widgets/surface.zig b/vendor/libghostty-vt/src/inspector/widgets/surface.zig
new file mode 100644
index 00000000..d73e784c
--- /dev/null
+++ b/vendor/libghostty-vt/src/inspector/widgets/surface.zig
@@ -0,0 +1,503 @@
+const std = @import("std");
+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");
+const terminal = @import("../../terminal/main.zig");
+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";
+const window_termio = "Terminal IO";
+const window_renderer = "Renderer";
+
+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,
+ renderer_info: widgets.renderer.Info,
+ show_demo_window: bool,
+
+ pub fn init(alloc: Allocator) !Inspector {
+ return .{
+ .surface_info = .empty,
+ .key_stream = try .init(alloc),
+ .terminal_info = .empty,
+ .vt_stream = try .init(alloc),
+ .renderer_info = .empty,
+ .show_demo_window = true,
+ };
+ }
+
+ pub fn deinit(self: *Inspector, alloc: Allocator) void {
+ self.key_stream.deinit(alloc);
+ self.vt_stream.deinit(alloc);
+ self.renderer_info.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");
+ const first_render = createDockSpace(dockspace_id);
+
+ // Draw everything that requires the terminal state mutex.
+ {
+ surface.renderer_state.mutex.lock();
+ defer surface.renderer_state.mutex.unlock();
+ const t = surface.renderer_state.terminal;
+
+ // Terminal info window
+ {
+ const open = cimgui.c.ImGui_Begin(
+ window_terminal,
+ null,
+ cimgui.c.ImGuiWindowFlags_NoFocusOnAppearing,
+ );
+ defer cimgui.c.ImGui_End();
+ self.terminal_info.draw(open, t);
+ }
+
+ // Surface info window
+ {
+ const open = cimgui.c.ImGui_Begin(
+ window_surface,
+ null,
+ cimgui.c.ImGuiWindowFlags_NoFocusOnAppearing,
+ );
+ defer cimgui.c.ImGui_End();
+ self.surface_info.draw(
+ open,
+ surface,
+ 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,
+ );
+ }
+
+ // 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,
+ );
+ }
+ }
+
+ // Renderer info window
+ {
+ const open = cimgui.c.ImGui_Begin(
+ window_renderer,
+ null,
+ cimgui.c.ImGuiWindowFlags_NoFocusOnAppearing,
+ );
+ defer cimgui.c.ImGui_End();
+ self.renderer_info.draw(
+ surface.alloc,
+ open,
+ );
+ }
+ }
+
+ // In debug we show the ImGui demo window so we can easily view
+ // available widgets and such.
+ if (comptime builtin.mode == .Debug) {
+ if (self.show_demo_window) {
+ cimgui.c.ImGui_ShowDemoWindow(&self.show_demo_window);
+ }
+ }
+
+ if (first_render) {
+ // On first render, setup our initial focus state. We only
+ // do this on first render so that we can let the user change
+ // focus afterward without it snapping back.
+ cimgui.c.ImGui_SetWindowFocusStr(window_terminal);
+ }
+ }
+
+ /// Create the global dock space for the inspector. A dock space
+ /// is a special area where windows can be docked into. The global
+ /// dock space fills the entire main viewport.
+ ///
+ /// Returns true if this was the first time the dock space was created.
+ fn createDockSpace(dockspace_id: cimgui.c.ImGuiID) bool {
+ const viewport: *cimgui.c.ImGuiViewport = cimgui.c.ImGui_GetMainViewport();
+
+ // Initial Docking setup
+ const setup = cimgui.ImGui_DockBuilderGetNode(dockspace_id) == null;
+ if (setup) {
+ // Register our dockspace node
+ assert(cimgui.ImGui_DockBuilderAddNodeEx(
+ dockspace_id,
+ cimgui.ImGuiDockNodeFlagsPrivate.DockSpace,
+ ) == dockspace_id);
+
+ // Ensure it is the full size of the viewport
+ cimgui.ImGui_DockBuilderSetNodeSize(
+ dockspace_id,
+ viewport.Size,
+ );
+
+ // We only initialize one central docking point now but
+ // this is the point we'd pre-split and so on for the initial
+ // layout.
+ const dock_id_main: cimgui.c.ImGuiID = dockspace_id;
+ cimgui.ImGui_DockBuilderDockWindow(window_imgui_demo, dock_id_main);
+ 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_renderer, dock_id_main);
+ cimgui.ImGui_DockBuilderFinish(dockspace_id);
+ }
+
+ // Put the dockspace over the viewport.
+ assert(cimgui.c.ImGui_DockSpaceOverViewportEx(
+ dockspace_id,
+ viewport,
+ cimgui.c.ImGuiDockNodeFlags_PassthruCentralNode,
+ null,
+ ) == dockspace_id);
+ return setup;
+ }
+};
+
+pub const Mouse = struct {
+ /// Last hovered x/y
+ last_xpos: f64 = 0,
+ last_ypos: f64 = 0,
+
+ // Last hovered screen point
+ last_point: ?terminal.Pin = null,
+};
+
+/// Surface information inspector widget.
+pub const Info = struct {
+ pub const empty: Info = .{};
+
+ /// Draw the surface info window.
+ pub fn draw(
+ self: *Info,
+ open: bool,
+ surface: *const Surface,
+ mouse: Mouse,
+ ) void {
+ _ = self;
+ if (!open) return;
+
+ if (cimgui.c.ImGui_CollapsingHeader(
+ "Help",
+ cimgui.c.ImGuiTreeNodeFlags_None,
+ )) {
+ cimgui.c.ImGui_TextWrapped(
+ "This window displays information about the surface (window). " ++
+ "A surface is the graphical area that displays the terminal " ++
+ "content. It includes dimensions, font sizing, and mouse state " ++
+ "information specific to this window instance.",
+ );
+ }
+
+ cimgui.c.ImGui_SeparatorText("Dimensions");
+ dimensionsTable(surface);
+
+ cimgui.c.ImGui_SeparatorText("Font");
+ fontTable(surface);
+
+ cimgui.c.ImGui_SeparatorText("Mouse");
+ mouseTable(surface, mouse);
+ }
+};
+
+fn dimensionsTable(surface: *const Surface) void {
+ _ = cimgui.c.ImGui_BeginTable(
+ "table_size",
+ 2,
+ cimgui.c.ImGuiTableFlags_None,
+ );
+ defer cimgui.c.ImGui_EndTable();
+
+ // Screen Size
+ {
+ cimgui.c.ImGui_TableNextRow();
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Screen Size");
+ }
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ cimgui.c.ImGui_Text(
+ "%dpx x %dpx",
+ surface.size.screen.width,
+ surface.size.screen.height,
+ );
+ }
+ }
+
+ // Grid Size
+ {
+ cimgui.c.ImGui_TableNextRow();
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Grid Size");
+ }
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ const grid_size = surface.size.grid();
+ cimgui.c.ImGui_Text(
+ "%dc x %dr",
+ grid_size.columns,
+ grid_size.rows,
+ );
+ }
+ }
+
+ // Cell Size
+ {
+ cimgui.c.ImGui_TableNextRow();
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Cell Size");
+ }
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ cimgui.c.ImGui_Text(
+ "%dpx x %dpx",
+ surface.size.cell.width,
+ surface.size.cell.height,
+ );
+ }
+ }
+
+ // Padding
+ {
+ cimgui.c.ImGui_TableNextRow();
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Window Padding");
+ }
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ cimgui.c.ImGui_Text(
+ "T=%d B=%d L=%d R=%d px",
+ surface.size.padding.top,
+ surface.size.padding.bottom,
+ surface.size.padding.left,
+ surface.size.padding.right,
+ );
+ }
+ }
+}
+
+fn fontTable(surface: *const Surface) void {
+ _ = cimgui.c.ImGui_BeginTable(
+ "table_font",
+ 2,
+ cimgui.c.ImGuiTableFlags_None,
+ );
+ defer cimgui.c.ImGui_EndTable();
+
+ {
+ cimgui.c.ImGui_TableNextRow();
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Size (Points)");
+ }
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ cimgui.c.ImGui_Text(
+ "%.2f pt",
+ surface.font_size.points,
+ );
+ }
+ }
+
+ {
+ cimgui.c.ImGui_TableNextRow();
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Size (Pixels)");
+ }
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ cimgui.c.ImGui_Text(
+ "%.2f px",
+ surface.font_size.pixels(),
+ );
+ }
+ }
+}
+
+fn mouseTable(
+ surface: *const Surface,
+ mouse: Mouse,
+) void {
+ _ = cimgui.c.ImGui_BeginTable(
+ "table_mouse",
+ 2,
+ cimgui.c.ImGuiTableFlags_None,
+ );
+ defer cimgui.c.ImGui_EndTable();
+
+ const surface_mouse = &surface.mouse;
+ const t = surface.renderer_state.terminal;
+
+ {
+ const hover_point: terminal.point.Coordinate = pt: {
+ const p = mouse.last_point orelse break :pt .{};
+ const pt = t.screens.active.pages.pointFromPin(
+ .active,
+ p,
+ ) orelse break :pt .{};
+ break :pt pt.coord();
+ };
+
+ cimgui.c.ImGui_TableNextRow();
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Hover Grid");
+ }
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ cimgui.c.ImGui_Text(
+ "row=%d, col=%d",
+ hover_point.y,
+ hover_point.x,
+ );
+ }
+ }
+
+ {
+ const coord: renderer.Coordinate.Terminal = (renderer.Coordinate{
+ .surface = .{
+ .x = mouse.last_xpos,
+ .y = mouse.last_ypos,
+ },
+ }).convert(.terminal, surface.size).terminal;
+
+ cimgui.c.ImGui_TableNextRow();
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Hover Point");
+ }
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ cimgui.c.ImGui_Text(
+ "(%dpx, %dpx)",
+ @as(i64, @intFromFloat(coord.x)),
+ @as(i64, @intFromFloat(coord.y)),
+ );
+ }
+ }
+
+ const any_click = for (surface_mouse.click_state) |state| {
+ if (state == .press) break true;
+ } else false;
+
+ click: {
+ cimgui.c.ImGui_TableNextRow();
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Click State");
+ }
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ if (!any_click) {
+ cimgui.c.ImGui_Text("none");
+ break :click;
+ }
+
+ for (surface_mouse.click_state, 0..) |state, i| {
+ if (state != .press) continue;
+ const button: input.MouseButton = @enumFromInt(i);
+ cimgui.c.ImGui_SameLine();
+ cimgui.c.ImGui_Text("%s", (switch (button) {
+ .unknown => "?",
+ .left => "L",
+ .middle => "M",
+ .right => "R",
+ .four => "{4}",
+ .five => "{5}",
+ .six => "{6}",
+ .seven => "{7}",
+ .eight => "{8}",
+ .nine => "{9}",
+ .ten => "{10}",
+ .eleven => "{11}",
+ }).ptr);
+ }
+ }
+ }
+
+ {
+ const left_click_point: terminal.point.Coordinate = pt: {
+ const p = surface_mouse.left_click_pin orelse break :pt .{};
+ const pt = t.screens.active.pages.pointFromPin(
+ .active,
+ p.*,
+ ) orelse break :pt .{};
+ break :pt pt.coord();
+ };
+
+ cimgui.c.ImGui_TableNextRow();
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Click Grid");
+ }
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ cimgui.c.ImGui_Text(
+ "row=%d, col=%d",
+ left_click_point.y,
+ left_click_point.x,
+ );
+ }
+ }
+
+ {
+ cimgui.c.ImGui_TableNextRow();
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Click Point");
+ }
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ cimgui.c.ImGui_Text(
+ "(%dpx, %dpx)",
+ @as(u32, @intFromFloat(surface_mouse.left_click_xpos)),
+ @as(u32, @intFromFloat(surface_mouse.left_click_ypos)),
+ );
+ }
+ }
+}
diff --git a/vendor/libghostty-vt/src/inspector/widgets/terminal.zig b/vendor/libghostty-vt/src/inspector/widgets/terminal.zig
new file mode 100644
index 00000000..3dbea388
--- /dev/null
+++ b/vendor/libghostty-vt/src/inspector/widgets/terminal.zig
@@ -0,0 +1,726 @@
+const std = @import("std");
+const builtin = @import("builtin");
+const assert = @import("../../quirks.zig").inlineAssert;
+const Allocator = std.mem.Allocator;
+const cimgui = @import("dcimgui");
+const widgets = @import("../widgets.zig");
+const terminal = @import("../../terminal/main.zig");
+const modes = terminal.modes;
+const Terminal = terminal.Terminal;
+
+/// Terminal information inspector widget.
+pub const Info = struct {
+ /// True if we're showing the 256-color palette window.
+ show_palette: bool,
+
+ /// The various detachable headers.
+ misc_header: widgets.DetachableHeader,
+ layout_header: widgets.DetachableHeader,
+ mouse_header: widgets.DetachableHeader,
+ color_header: widgets.DetachableHeader,
+ modes_header: widgets.DetachableHeader,
+
+ /// Screen detail windows for each screen key.
+ screens: ScreenMap,
+
+ pub const empty: Info = .{
+ .show_palette = false,
+ .misc_header = .{},
+ .layout_header = .{},
+ .mouse_header = .{},
+ .color_header = .{},
+ .modes_header = .{},
+ .screens = .{},
+ };
+
+ /// Draw the terminal info window.
+ pub fn draw(
+ self: *Info,
+ open: bool,
+ t: *Terminal,
+ ) void {
+ // Draw our open state if we're open.
+ if (open) self.drawOpen(t);
+
+ // Draw our detached state that draws regardless of if
+ // we're open or not.
+ if (self.misc_header.window("Terminal Misc")) |visible| {
+ defer self.misc_header.windowEnd();
+ if (visible) miscTable(t);
+ }
+ if (self.layout_header.window("Terminal Layout")) |visible| {
+ defer self.layout_header.windowEnd();
+ if (visible) layoutTable(t);
+ }
+ if (self.mouse_header.window("Terminal Mouse")) |visible| {
+ defer self.mouse_header.windowEnd();
+ if (visible) mouseTable(t);
+ }
+ if (self.color_header.window("Terminal Color")) |visible| {
+ defer self.color_header.windowEnd();
+ if (visible) colorTable(t, &self.show_palette);
+ }
+ if (self.modes_header.window("Terminal Modes")) |visible| {
+ defer self.modes_header.windowEnd();
+ if (visible) modesTable(t);
+ }
+
+ // Palette pop-out window
+ if (self.show_palette) {
+ defer cimgui.c.ImGui_End();
+ if (cimgui.c.ImGui_Begin(
+ "256-Color Palette",
+ &self.show_palette,
+ cimgui.c.ImGuiWindowFlags_NoFocusOnAppearing,
+ )) {
+ palette("palette", &t.colors.palette.current);
+ }
+ }
+
+ // Screen pop-out windows
+ var it = self.screens.iterator();
+ while (it.next()) |entry| {
+ const screen = t.screens.get(entry.key) orelse {
+ // Could happen if we opened up a window for a screen
+ // and that screen was subsequently deinitialized. In
+ // this case, hide the window.
+ self.screens.remove(entry.key);
+ continue;
+ };
+
+ var title_buf: [128]u8 = undefined;
+ const title = std.fmt.bufPrintZ(
+ &title_buf,
+ "Screen: {t}",
+ .{entry.key},
+ ) catch "Screen";
+
+ // Setup our next window so it has some size to it.
+ const viewport = cimgui.c.ImGui_GetMainViewport();
+ cimgui.c.ImGui_SetNextWindowSize(
+ .{
+ .x = @min(400, viewport.*.Size.x),
+ .y = @min(300, viewport.*.Size.y),
+ },
+ cimgui.c.ImGuiCond_FirstUseEver,
+ );
+
+ var screen_open: bool = true;
+ defer cimgui.c.ImGui_End();
+ const screen_draw = cimgui.c.ImGui_Begin(
+ title,
+ &screen_open,
+ cimgui.c.ImGuiWindowFlags_NoFocusOnAppearing,
+ );
+ entry.value.draw(screen_draw, .{
+ .screen = screen,
+ .key = entry.key,
+ .active_key = t.screens.active_key,
+ .modify_other_keys_2 = t.flags.modify_other_keys_2,
+ .color_palette = &t.colors.palette,
+ });
+
+ // If the window was closed, remove it from our map so future
+ // renders don't draw it.
+ if (!screen_open) self.screens.remove(entry.key);
+ }
+ }
+
+ fn drawOpen(self: *Info, t: *Terminal) void {
+ // Show our screens up top.
+ screensTable(t, &self.screens);
+
+ if (self.misc_header.header("Misc")) miscTable(t);
+ if (self.layout_header.header("Layout")) layoutTable(t);
+ if (self.mouse_header.header("Mouse")) mouseTable(t);
+ if (self.color_header.header("Color")) colorTable(t, &self.show_palette);
+ if (self.modes_header.header("Modes")) modesTable(t);
+ }
+};
+
+pub const ScreenMap = std.EnumMap(
+ terminal.ScreenSet.Key,
+ widgets.screen.Info,
+);
+
+/// Render the table of possible screens with various actions.
+fn screensTable(
+ t: *Terminal,
+ map: *ScreenMap,
+) void {
+ if (!cimgui.c.ImGui_BeginTable(
+ "screens",
+ 3,
+ cimgui.c.ImGuiTableFlags_Borders |
+ cimgui.c.ImGuiTableFlags_RowBg |
+ cimgui.c.ImGuiTableFlags_SizingFixedFit,
+ )) return;
+ defer cimgui.c.ImGui_EndTable();
+
+ cimgui.c.ImGui_TableSetupColumn("Screen", cimgui.c.ImGuiTableColumnFlags_WidthFixed);
+ cimgui.c.ImGui_TableSetupColumn("Status", cimgui.c.ImGuiTableColumnFlags_WidthFixed);
+ cimgui.c.ImGui_TableSetupColumn("", cimgui.c.ImGuiTableColumnFlags_WidthFixed);
+
+ // Custom header row to include help marker before "Screen"
+ {
+ cimgui.c.ImGui_TableNextRowEx(cimgui.c.ImGuiTableRowFlags_Headers, 0.0);
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_PushStyleVarImVec2(cimgui.c.ImGuiStyleVar_FramePadding, .{ .x = 0, .y = 0 });
+ widgets.helpMarker(
+ "A terminal can have multiple screens, only one of which is active at " ++
+ "a time. Each screen has its own grid, contents, and other state. " ++
+ "This section allows you to inspect the different screens managed by " ++
+ "the terminal.",
+ );
+ cimgui.c.ImGui_PopStyleVar();
+ cimgui.c.ImGui_SameLineEx(0.0, cimgui.c.ImGui_GetStyle().*.ItemInnerSpacing.x);
+ cimgui.c.ImGui_TableHeader("Screen");
+ }
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ cimgui.c.ImGui_TableHeader("Status");
+ }
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(2);
+ cimgui.c.ImGui_TableHeader("");
+ }
+ }
+
+ for (std.meta.tags(terminal.ScreenSet.Key)) |key| {
+ const is_initialized = t.screens.get(key) != null;
+ const is_active = t.screens.active_key == key;
+
+ cimgui.c.ImGui_TableNextRow();
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("%s", @tagName(key).ptr);
+ }
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ if (is_active) {
+ cimgui.c.ImGui_TextColored(
+ .{ .x = 0.4, .y = 1.0, .z = 0.4, .w = 1.0 },
+ "active",
+ );
+ } else if (is_initialized) {
+ cimgui.c.ImGui_TextColored(
+ .{ .x = 0.6, .y = 0.6, .z = 0.6, .w = 1.0 },
+ "initialized",
+ );
+ } else {
+ cimgui.c.ImGui_TextColored(
+ .{ .x = 0.4, .y = 0.4, .z = 0.4, .w = 1.0 },
+ "(not initialized)",
+ );
+ }
+ }
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(2);
+ cimgui.c.ImGui_PushIDInt(@intFromEnum(key));
+ defer cimgui.c.ImGui_PopID();
+ cimgui.c.ImGui_BeginDisabled(!is_initialized);
+ defer cimgui.c.ImGui_EndDisabled();
+ if (cimgui.c.ImGui_Button("View")) {
+ if (!map.contains(key)) {
+ map.put(key, .empty);
+ }
+ }
+ }
+ }
+}
+
+/// Table of miscellaneous terminal information.
+fn miscTable(t: *Terminal) void {
+ _ = cimgui.c.ImGui_BeginTable(
+ "table_misc",
+ 2,
+ cimgui.c.ImGuiTableFlags_None,
+ );
+ defer cimgui.c.ImGui_EndTable();
+
+ {
+ cimgui.c.ImGui_TableNextRow();
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Working Directory");
+ cimgui.c.ImGui_SameLine();
+ widgets.helpMarker("The current working directory reported by the shell.");
+ }
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ if (t.pwd.items.len > 0) {
+ cimgui.c.ImGui_Text(
+ "%.*s",
+ t.pwd.items.len,
+ t.pwd.items.ptr,
+ );
+ } else {
+ cimgui.c.ImGui_TextDisabled("(none)");
+ }
+ }
+ }
+
+ {
+ cimgui.c.ImGui_TableNextRow();
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Focused");
+ cimgui.c.ImGui_SameLine();
+ widgets.helpMarker("Whether the terminal itself is currently focused.");
+ }
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ var value: bool = t.flags.focused;
+ _ = cimgui.c.ImGui_Checkbox("##focused", &value);
+ }
+ }
+
+ {
+ cimgui.c.ImGui_TableNextRow();
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Previous Char");
+ cimgui.c.ImGui_SameLine();
+ widgets.helpMarker("The previously printed character, used only for the REP sequence.");
+ }
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ if (t.previous_char) |c| {
+ cimgui.c.ImGui_Text("U+%04X", @as(u32, c));
+ } else {
+ cimgui.c.ImGui_TextDisabled("(none)");
+ }
+ }
+ }
+}
+
+/// Table of terminal layout information.
+fn layoutTable(t: *Terminal) void {
+ _ = cimgui.c.ImGui_BeginTable(
+ "table_layout",
+ 2,
+ cimgui.c.ImGuiTableFlags_None,
+ );
+ defer cimgui.c.ImGui_EndTable();
+
+ {
+ cimgui.c.ImGui_TableNextRow();
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Grid");
+ cimgui.c.ImGui_SameLine();
+ widgets.helpMarker("The size of the terminal grid in columns and rows.");
+ }
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ cimgui.c.ImGui_Text(
+ "%dc x %dr",
+ t.cols,
+ t.rows,
+ );
+ }
+ }
+
+ {
+ cimgui.c.ImGui_TableNextRow();
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Pixels");
+ cimgui.c.ImGui_SameLine();
+ widgets.helpMarker("The size of the terminal grid in pixels.");
+ }
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ cimgui.c.ImGui_Text(
+ "%dw x %dh",
+ t.width_px,
+ t.height_px,
+ );
+ }
+ }
+
+ {
+ cimgui.c.ImGui_TableNextRow();
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Scroll Region");
+ cimgui.c.ImGui_SameLine();
+ widgets.helpMarker("The scrolling region boundaries (top, bottom, left, right).");
+ }
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ cimgui.c.ImGui_PushItemWidth(cimgui.c.ImGui_CalcTextSize("00000").x);
+ defer cimgui.c.ImGui_PopItemWidth();
+
+ var override = t.scrolling_region;
+ var changed = false;
+
+ cimgui.c.ImGui_AlignTextToFramePadding();
+ cimgui.c.ImGui_Text("T:");
+ cimgui.c.ImGui_SameLine();
+ if (cimgui.c.ImGui_InputScalar(
+ "##scroll_top",
+ cimgui.c.ImGuiDataType_U16,
+ &override.top,
+ )) {
+ override.top = @min(override.top, t.rows -| 1);
+ changed = true;
+ }
+
+ cimgui.c.ImGui_SameLine();
+ cimgui.c.ImGui_Text("B:");
+ cimgui.c.ImGui_SameLine();
+ if (cimgui.c.ImGui_InputScalar(
+ "##scroll_bottom",
+ cimgui.c.ImGuiDataType_U16,
+ &override.bottom,
+ )) {
+ override.bottom = @min(override.bottom, t.rows -| 1);
+ changed = true;
+ }
+
+ cimgui.c.ImGui_SameLine();
+ cimgui.c.ImGui_Text("L:");
+ cimgui.c.ImGui_SameLine();
+ if (cimgui.c.ImGui_InputScalar(
+ "##scroll_left",
+ cimgui.c.ImGuiDataType_U16,
+ &override.left,
+ )) {
+ override.left = @min(override.left, t.cols -| 1);
+ changed = true;
+ }
+
+ cimgui.c.ImGui_SameLine();
+ cimgui.c.ImGui_Text("R:");
+ cimgui.c.ImGui_SameLine();
+ if (cimgui.c.ImGui_InputScalar(
+ "##scroll_right",
+ cimgui.c.ImGuiDataType_U16,
+ &override.right,
+ )) {
+ override.right = @min(override.right, t.cols -| 1);
+ changed = true;
+ }
+
+ if (changed and
+ override.top < override.bottom and
+ override.left < override.right)
+ {
+ t.scrolling_region = override;
+ }
+ }
+ }
+}
+
+/// Table of mouse-related terminal information.
+fn mouseTable(t: *Terminal) void {
+ _ = cimgui.c.ImGui_BeginTable(
+ "table_mouse",
+ 2,
+ cimgui.c.ImGuiTableFlags_None,
+ );
+ defer cimgui.c.ImGui_EndTable();
+
+ {
+ cimgui.c.ImGui_TableNextRow();
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Event Mode");
+ cimgui.c.ImGui_SameLine();
+ widgets.helpMarker("The mouse event reporting mode set by the application.");
+ }
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ cimgui.c.ImGui_Text("%s", @tagName(t.flags.mouse_event).ptr);
+ }
+ }
+
+ {
+ cimgui.c.ImGui_TableNextRow();
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Format");
+ cimgui.c.ImGui_SameLine();
+ widgets.helpMarker("The mouse event encoding format.");
+ }
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ cimgui.c.ImGui_Text("%s", @tagName(t.flags.mouse_format).ptr);
+ }
+ }
+
+ {
+ cimgui.c.ImGui_TableNextRow();
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Shape");
+ cimgui.c.ImGui_SameLine();
+ widgets.helpMarker("The current mouse cursor shape set by the application.");
+ }
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ cimgui.c.ImGui_Text("%s", @tagName(t.mouse_shape).ptr);
+ }
+ }
+
+ {
+ cimgui.c.ImGui_TableNextRow();
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Shift Capture");
+ cimgui.c.ImGui_SameLine();
+ widgets.helpMarker("XTSHIFTESCAPE state for capturing shift in mouse protocol.");
+ }
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ if (t.flags.mouse_shift_capture == .null) {
+ cimgui.c.ImGui_TextDisabled("(unset)");
+ } else {
+ cimgui.c.ImGui_Text("%s", @tagName(t.flags.mouse_shift_capture).ptr);
+ }
+ }
+ }
+}
+
+/// Table of color-related terminal information.
+fn colorTable(
+ t: *Terminal,
+ show_palette: *bool,
+) void {
+ cimgui.c.ImGui_TextWrapped(
+ "Color state for the terminal. Note these colors only apply " ++
+ "to the palette and unstyled colors. Many modern terminal " ++
+ "applications use direct RGB colors which are not reflected here.",
+ );
+ cimgui.c.ImGui_Separator();
+
+ _ = cimgui.c.ImGui_BeginTable(
+ "table_color",
+ 2,
+ cimgui.c.ImGuiTableFlags_None,
+ );
+ defer cimgui.c.ImGui_EndTable();
+
+ {
+ cimgui.c.ImGui_TableNextRow();
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Background");
+ cimgui.c.ImGui_SameLine();
+ widgets.helpMarker("Unstyled cell background color.");
+ }
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ _ = dynamicRGB(
+ "bg_color",
+ &t.colors.background,
+ );
+ }
+ }
+
+ {
+ cimgui.c.ImGui_TableNextRow();
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Foreground");
+ cimgui.c.ImGui_SameLine();
+ widgets.helpMarker("Unstyled cell foreground color.");
+ }
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ _ = dynamicRGB(
+ "fg_color",
+ &t.colors.foreground,
+ );
+ }
+ }
+
+ {
+ cimgui.c.ImGui_TableNextRow();
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Cursor");
+ cimgui.c.ImGui_SameLine();
+ widgets.helpMarker("Cursor coloring set by escape sequences.");
+ }
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ _ = dynamicRGB(
+ "cursor_color",
+ &t.colors.cursor,
+ );
+ }
+ }
+
+ {
+ cimgui.c.ImGui_TableNextRow();
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("Palette");
+ cimgui.c.ImGui_SameLine();
+ widgets.helpMarker("The 256-color palette.");
+ }
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ if (cimgui.c.ImGui_Button("View")) {
+ show_palette.* = true;
+ }
+ }
+ }
+}
+
+/// Table of terminal modes.
+fn modesTable(t: *Terminal) void {
+ _ = cimgui.c.ImGui_BeginTable(
+ "table_modes",
+ 3,
+ cimgui.c.ImGuiTableFlags_SizingFixedFit |
+ cimgui.c.ImGuiTableFlags_RowBg,
+ );
+ defer cimgui.c.ImGui_EndTable();
+
+ {
+ cimgui.c.ImGui_TableSetupColumn("", cimgui.c.ImGuiTableColumnFlags_NoResize);
+ cimgui.c.ImGui_TableSetupColumn("Number", cimgui.c.ImGuiTableColumnFlags_PreferSortAscending);
+ cimgui.c.ImGui_TableSetupColumn("Name", cimgui.c.ImGuiTableColumnFlags_WidthStretch);
+ cimgui.c.ImGui_TableHeadersRow();
+ }
+
+ inline for (@typeInfo(terminal.Mode).@"enum".fields) |field| {
+ @setEvalBranchQuota(6000);
+ const tag: modes.ModeTag = @bitCast(@as(modes.ModeTag.Backing, field.value));
+
+ cimgui.c.ImGui_TableNextRow();
+ cimgui.c.ImGui_PushIDInt(@intCast(field.value));
+ defer cimgui.c.ImGui_PopID();
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ var value: bool = t.modes.get(@field(terminal.Mode, field.name));
+ _ = cimgui.c.ImGui_Checkbox("##checkbox", &value);
+ }
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ cimgui.c.ImGui_Text(
+ "%s%d",
+ if (tag.ansi) "" else "?",
+ @as(u32, @intCast(tag.value)),
+ );
+ }
+ {
+ _ = cimgui.c.ImGui_TableSetColumnIndex(2);
+ const name = std.fmt.comptimePrint("{s}", .{field.name});
+ cimgui.c.ImGui_Text("%s", name.ptr);
+ }
+ }
+}
+
+/// Render a DynamicRGB color.
+fn dynamicRGB(
+ label: [:0]const u8,
+ rgb: *terminal.color.DynamicRGB,
+) bool {
+ _ = cimgui.c.ImGui_BeginTable(
+ label,
+ if (rgb.override != null) 2 else 1,
+ cimgui.c.ImGuiTableFlags_SizingFixedFit,
+ );
+ defer cimgui.c.ImGui_EndTable();
+
+ if (rgb.override != null) cimgui.c.ImGui_TableSetupColumn(
+ "##label",
+ cimgui.c.ImGuiTableColumnFlags_WidthFixed,
+ );
+ cimgui.c.ImGui_TableSetupColumn(
+ "##value",
+ cimgui.c.ImGuiTableColumnFlags_WidthStretch,
+ );
+
+ if (rgb.override) |c| {
+ cimgui.c.ImGui_TableNextRow();
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ cimgui.c.ImGui_Text("override:");
+ cimgui.c.ImGui_SameLine();
+ widgets.helpMarker("Overridden color set by escape sequences.");
+
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ var col = [3]f32{
+ @as(f32, @floatFromInt(c.r)) / 255.0,
+ @as(f32, @floatFromInt(c.g)) / 255.0,
+ @as(f32, @floatFromInt(c.b)) / 255.0,
+ };
+ _ = cimgui.c.ImGui_ColorEdit3(
+ "##override",
+ &col,
+ cimgui.c.ImGuiColorEditFlags_None,
+ );
+ }
+
+ cimgui.c.ImGui_TableNextRow();
+ _ = cimgui.c.ImGui_TableSetColumnIndex(0);
+ if (rgb.default) |c| {
+ if (rgb.override != null) {
+ cimgui.c.ImGui_Text("default:");
+ cimgui.c.ImGui_SameLine();
+ widgets.helpMarker("Default color from configuration.");
+
+ _ = cimgui.c.ImGui_TableSetColumnIndex(1);
+ }
+
+ var col = [3]f32{
+ @as(f32, @floatFromInt(c.r)) / 255.0,
+ @as(f32, @floatFromInt(c.g)) / 255.0,
+ @as(f32, @floatFromInt(c.b)) / 255.0,
+ };
+ _ = cimgui.c.ImGui_ColorEdit3(
+ "##default",
+ &col,
+ cimgui.c.ImGuiColorEditFlags_None,
+ );
+ } else {
+ cimgui.c.ImGui_TextDisabled("(unset)");
+ }
+
+ return false;
+}
+
+/// Render a color palette as a 16x16 grid of color buttons.
+fn palette(
+ label: [:0]const u8,
+ pal: *const terminal.color.Palette,
+) void {
+ cimgui.c.ImGui_PushID(label);
+ defer cimgui.c.ImGui_PopID();
+
+ for (0..16) |row| {
+ for (0..16) |col| {
+ const idx = row * 16 + col;
+ const rgb = pal[idx];
+ var col_arr = [3]f32{
+ @as(f32, @floatFromInt(rgb.r)) / 255.0,
+ @as(f32, @floatFromInt(rgb.g)) / 255.0,
+ @as(f32, @floatFromInt(rgb.b)) / 255.0,
+ };
+
+ if (col > 0) cimgui.c.ImGui_SameLine();
+
+ cimgui.c.ImGui_PushIDInt(@intCast(idx));
+ _ = cimgui.c.ImGui_ColorEdit3(
+ "##color",
+ &col_arr,
+ cimgui.c.ImGuiColorEditFlags_NoInputs,
+ );
+ if (cimgui.c.ImGui_IsItemHovered(cimgui.c.ImGuiHoveredFlags_DelayShort)) {
+ cimgui.c.ImGui_SetTooltip(
+ "%d: #%02X%02X%02X",
+ idx,
+ rgb.r,
+ rgb.g,
+ rgb.b,
+ );
+ }
+ cimgui.c.ImGui_PopID();
+ }
+ }
+}
diff --git a/vendor/libghostty-vt/src/inspector/widgets/termio.zig b/vendor/libghostty-vt/src/inspector/widgets/termio.zig
new file mode 100644
index 00000000..b721d942
--- /dev/null
+++ b/vendor/libghostty-vt/src/inspector/widgets/termio.zig
@@ -0,0 +1,811 @@
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const ArenaAllocator = std.heap.ArenaAllocator;
+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");
+
+/// VT event stream inspector widget.
+pub const Stream = struct {
+ events: VTEvent.Ring,
+ parser_stream: VTHandler.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,
+
+ pub fn init(alloc: Allocator) !Stream {
+ var events: VTEvent.Ring = try .init(alloc, 2);
+ errdefer events.deinit(alloc);
+
+ var handler: VTHandler = .init;
+ 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 recordPtyRead(
+ self: *Stream,
+ alloc: Allocator,
+ t: *terminal.Terminal,
+ data: []const u8,
+ ) !void {
+ self.parser_stream.handler.state = .{
+ .alloc = alloc,
+ .terminal = t,
+ .events = &self.events,
+ };
+ defer self.parser_stream.handler.state = null;
+ self.parser_stream.nextSlice(data);
+ }
+
+ 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";
+
+ // Controls
+ {
+ const pause_play: [:0]const u8 = if (!handler.paused)
+ "Pause##pause_play"
+ else
+ "Resume##pause_play";
+ if (cimgui.c.ImGui_Button(pause_play.ptr)) {
+ handler.paused = !handler.paused;
+ }
+
+ 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;
+ }
+ }
+ }
+
+ // Events Table
+ if (events.empty()) {
+ cimgui.c.ImGui_Text("Waiting for events...");
+ } else {
+ // TODO: Eventually
+ // eventTable(events);
+ }
+
+ {
+ cimgui.c.ImGui_Separator();
+
+ _ = 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.raw_description.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;
+}
+
+pub fn eventTable(events: *const VTEvent.Ring) void {
+ if (!cimgui.c.ImGui_BeginTable(
+ "events",
+ 3,
+ cimgui.c.ImGuiTableFlags_RowBg |
+ cimgui.c.ImGuiTableFlags_Borders,
+ )) return;
+ 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,
+ );
+
+ 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();
+
+ 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.raw_description.ptr);
+ }
+}
+
+/// VT event. This isn't public because this is just how we store internal
+/// events.
+const VTEvent = struct {
+ /// The arena that all allocated memory for this event is stored.
+ arena_state: ArenaAllocator.State,
+
+ /// Sequence number, just monotonically increasing and wrapping if
+ /// it ever overflows. It gives us a nice way to visualize progress.
+ seq: usize = 1,
+
+ /// Kind of event, for filtering
+ kind: Kind,
+
+ /// The description of the raw event in a more human-friendly format.
+ /// For example for control sequences this is the full sequence but
+ /// control characters are replaced with human-readable names, e.g.
+ /// 0x07 (bell) becomes BEL.
+ raw_description: [: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);
+
+ /// Circular buffer of VT events.
+ pub const Ring = CircBuf(VTEvent, undefined);
+
+ /// Initialize the event information for the given parser action.
+ pub fn init(
+ alloc_gpa: Allocator,
+ t: *const terminal.Terminal,
+ action: terminal.Parser.Action,
+ ) !VTEvent {
+ var arena: ArenaAllocator = .init(alloc_gpa);
+ errdefer arena.deinit();
+ const alloc = arena.allocator();
+
+ var md = Metadata.init(alloc);
+ var buf: std.Io.Writer.Allocating = .init(alloc);
+ try encodeAction(alloc, &buf.writer, &md, action);
+ const desc = try buf.toOwnedSliceSentinel(0);
+
+ 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,
+ };
+
+ return .{
+ .arena_state = arena.state,
+ .kind = kind,
+ .raw_description = desc,
+ .cursor = t.screens.active.cursor,
+ .scrolling_region = t.scrolling_region,
+ .metadata = md.unmanaged,
+ };
+ }
+
+ pub fn deinit(self: *VTEvent, alloc_gpa: Allocator) void {
+ var arena = self.arena_state.promote(alloc_gpa);
+ arena.deinit();
+ }
+
+ /// 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.raw_description.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 for the Stream widget. This isn't public
+/// because there is no reason to use this directly.
+const VTHandler = struct {
+ /// The capture state, must be set before use. If null, then
+ /// events are dropped.
+ state: ?State,
+
+ /// True to pause this artificially.
+ paused: bool,
+
+ /// Current sequence number
+ current_seq: usize,
+
+ /// Exclude certain actions by tag.
+ filter_exclude: ActionTagSet,
+ filter_text: cimgui.c.ImGuiTextFilter,
+
+ const Stream = terminal.Stream(VTHandler);
+
+ pub const ActionTagSet = std.EnumSet(terminal.Parser.Action.Tag);
+
+ pub const State = struct {
+ /// The allocator to use for the events.
+ alloc: Allocator,
+
+ /// The terminal state at the time of the event.
+ terminal: *const terminal.Terminal,
+
+ /// The event ring to write events to.
+ events: *VTEvent.Ring,
+ };
+
+ pub const init: VTHandler = .{
+ .state = null,
+ .paused = false,
+ .current_seq = 1,
+ .filter_exclude = .initMany(&.{.print}),
+ .filter_text = .{},
+ };
+
+ pub fn deinit(self: *VTHandler) void {
+ // Required for the parser stream interface
+ _ = self;
+ }
+
+ pub fn vt(
+ self: *VTHandler,
+ comptime action: VTHandler.Stream.Action.Tag,
+ value: VTHandler.Stream.Action.Value(action),
+ ) void {
+ _ = self;
+ _ = value;
+ }
+
+ /// This is called with every single terminal action.
+ pub fn vtRaw(self: *VTHandler, action: terminal.Parser.Action) !bool {
+ const state: *State = if (self.state) |*s| s else return true;
+ const alloc = state.alloc;
+ const vt_events = state.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 manually paused, we ignore all events.
+ if (self.paused) 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
+ var ev: VTEvent = try .init(
+ alloc,
+ state.terminal,
+ 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(alloc, new_capacity);
+ try vt_events.append(ev);
+ } else {
+ var it = vt_events.iterator(.forward);
+ if (it.next()) |old_ev| old_ev.deinit(alloc);
+ vt_events.deleteOldest(1);
+ try vt_events.append(ev);
+ },
+
+ else => return err,
+ };
+
+ // Do NOT skip it, because we want to record more information
+ // about this event.
+ return false;
+ }
+};
+
+/// Enum representing keyboard navigation actions
+const KeyAction = enum {
+ down,
+ none,
+ up,
+};