Inspector Revamp (#10509)

I'm going to keep this PR description brief, because there's a lot more
work I want to do here. And when I do it I'll talk more about it. But
this basically rewrites the inspector. It has almost all the same
functionality (missing one from the old inspector -- cell picking). But
it is organized in a much cleaner way and the memory management is also
a lot more clean. We also expose a LOT more details about things like
PageList and screens.

There are plenty of bugs and polish issues here. But going to merge this
because the inspector isn't a hot path item for people and a review of
this is not really possible. Plus it paves the way to more debug
overlays.
This commit is contained in:
Mitchell Hashimoto
2026-01-31 10:21:51 -08:00
committed by GitHub
33 changed files with 4920 additions and 2476 deletions

View File

@@ -1335,7 +1335,7 @@ extension Ghostty {
mode: ghostty_action_inspector_e) {
switch (target.tag) {
case GHOSTTY_TARGET_APP:
Ghostty.logger.warning("toggle split zoom does nothing with an app target")
Ghostty.logger.warning("toggle inspector does nothing with an app target")
return
case GHOSTTY_TARGET_SURFACE:

View File

@@ -0,0 +1,100 @@
import GhosttyKit
import Metal
extension Ghostty {
/// Represents the inspector for a surface within Ghostty.
///
/// Wraps a `ghostty_inspector_t`
final class Inspector: Sendable {
private let inspector: ghostty_inspector_t
/// Read the underlying C value for this inspector. This is unsafe because the value will be
/// freed when the Inspector class is deinitialized.
var unsafeCValue: ghostty_inspector_t {
inspector
}
/// Initialize from the C structure.
init(cInspector: ghostty_inspector_t) {
self.inspector = cInspector
}
/// Set the focus state of the inspector.
@MainActor
func setFocus(_ focused: Bool) {
ghostty_inspector_set_focus(inspector, focused)
}
/// Set the content scale of the inspector.
@MainActor
func setContentScale(x: Double, y: Double) {
ghostty_inspector_set_content_scale(inspector, x, y)
}
/// Set the size of the inspector.
@MainActor
func setSize(width: UInt32, height: UInt32) {
ghostty_inspector_set_size(inspector, width, height)
}
/// Send a mouse button event to the inspector.
@MainActor
func mouseButton(
_ state: ghostty_input_mouse_state_e,
button: ghostty_input_mouse_button_e,
mods: ghostty_input_mods_e
) {
ghostty_inspector_mouse_button(inspector, state, button, mods)
}
/// Send a mouse position event to the inspector.
@MainActor
func mousePos(x: Double, y: Double) {
ghostty_inspector_mouse_pos(inspector, x, y)
}
/// Send a mouse scroll event to the inspector.
@MainActor
func mouseScroll(x: Double, y: Double, mods: ghostty_input_scroll_mods_t) {
ghostty_inspector_mouse_scroll(inspector, x, y, mods)
}
/// Send a key event to the inspector.
@MainActor
func key(
_ action: ghostty_input_action_e,
key: ghostty_input_key_e,
mods: ghostty_input_mods_e
) {
ghostty_inspector_key(inspector, action, key, mods)
}
/// Send text to the inspector.
@MainActor
func text(_ text: String) {
text.withCString { ptr in
ghostty_inspector_text(inspector, ptr)
}
}
/// Initialize Metal rendering for the inspector.
@MainActor
func metalInit(device: MTLDevice) -> Bool {
let devicePtr = Unmanaged.passRetained(device).toOpaque()
return ghostty_inspector_metal_init(inspector, devicePtr)
}
/// Render the inspector using Metal.
@MainActor
func metalRender(
commandBuffer: MTLCommandBuffer,
descriptor: MTLRenderPassDescriptor
) {
ghostty_inspector_metal_render(
inspector,
Unmanaged.passRetained(commandBuffer).toOpaque(),
Unmanaged.passRetained(descriptor).toOpaque()
)
}
}
}

View File

@@ -98,7 +98,7 @@ extension Ghostty {
didSet { surfaceViewDidChange() }
}
private var inspector: ghostty_inspector_t? {
private var inspector: Ghostty.Inspector? {
guard let surfaceView = self.surfaceView else { return nil }
return surfaceView.inspector
}
@@ -150,8 +150,7 @@ extension Ghostty {
guard let surfaceView = self.surfaceView else { return }
guard let inspector = self.inspector else { return }
guard let device = self.device else { return }
let devicePtr = Unmanaged.passRetained(device).toOpaque()
ghostty_inspector_metal_init(inspector, devicePtr)
_ = inspector.metalInit(device: device)
// Register an observer for render requests
center.addObserver(
@@ -172,10 +171,10 @@ extension Ghostty {
let fbFrame = self.convertToBacking(self.frame)
let xScale = fbFrame.size.width / self.frame.size.width
let yScale = fbFrame.size.height / self.frame.size.height
ghostty_inspector_set_content_scale(inspector, xScale, yScale)
inspector.setContentScale(x: xScale, y: yScale)
// When our scale factor changes, so does our fb size so we send that too
ghostty_inspector_set_size(inspector, UInt32(fbFrame.size.width), UInt32(fbFrame.size.height))
inspector.setSize(width: UInt32(fbFrame.size.width), height: UInt32(fbFrame.size.height))
}
// MARK: NSView
@@ -184,7 +183,7 @@ extension Ghostty {
let result = super.becomeFirstResponder()
if (result) {
if let inspector = self.inspector {
ghostty_inspector_set_focus(inspector, true)
inspector.setFocus(true)
}
}
return result
@@ -194,7 +193,7 @@ extension Ghostty {
let result = super.resignFirstResponder()
if (result) {
if let inspector = self.inspector {
ghostty_inspector_set_focus(inspector, false)
inspector.setFocus(false)
}
}
return result
@@ -229,25 +228,25 @@ extension Ghostty {
override func mouseDown(with event: NSEvent) {
guard let inspector = self.inspector else { return }
let mods = Ghostty.ghosttyMods(event.modifierFlags)
ghostty_inspector_mouse_button(inspector, GHOSTTY_MOUSE_PRESS, GHOSTTY_MOUSE_LEFT, mods)
inspector.mouseButton(GHOSTTY_MOUSE_PRESS, button: GHOSTTY_MOUSE_LEFT, mods: mods)
}
override func mouseUp(with event: NSEvent) {
guard let inspector = self.inspector else { return }
let mods = Ghostty.ghosttyMods(event.modifierFlags)
ghostty_inspector_mouse_button(inspector, GHOSTTY_MOUSE_RELEASE, GHOSTTY_MOUSE_LEFT, mods)
inspector.mouseButton(GHOSTTY_MOUSE_RELEASE, button: GHOSTTY_MOUSE_LEFT, mods: mods)
}
override func rightMouseDown(with event: NSEvent) {
guard let inspector = self.inspector else { return }
let mods = Ghostty.ghosttyMods(event.modifierFlags)
ghostty_inspector_mouse_button(inspector, GHOSTTY_MOUSE_PRESS, GHOSTTY_MOUSE_RIGHT, mods)
inspector.mouseButton(GHOSTTY_MOUSE_PRESS, button: GHOSTTY_MOUSE_RIGHT, mods: mods)
}
override func rightMouseUp(with event: NSEvent) {
guard let inspector = self.inspector else { return }
let mods = Ghostty.ghosttyMods(event.modifierFlags)
ghostty_inspector_mouse_button(inspector, GHOSTTY_MOUSE_RELEASE, GHOSTTY_MOUSE_RIGHT, mods)
inspector.mouseButton(GHOSTTY_MOUSE_RELEASE, button: GHOSTTY_MOUSE_RIGHT, mods: mods)
}
override func mouseMoved(with event: NSEvent) {
@@ -255,7 +254,7 @@ extension Ghostty {
// Convert window position to view position. Note (0, 0) is bottom left.
let pos = self.convert(event.locationInWindow, from: nil)
ghostty_inspector_mouse_pos(inspector, pos.x, frame.height - pos.y)
inspector.mousePos(x: pos.x, y: frame.height - pos.y)
}
@@ -297,7 +296,7 @@ extension Ghostty {
// Pack our momentum value into the mods bitmask
mods |= Int32(momentum.rawValue) << 1
ghostty_inspector_mouse_scroll(inspector, x, y, mods)
inspector.mouseScroll(x: x, y: y, mods: mods)
}
override func keyDown(with event: NSEvent) {
@@ -336,7 +335,7 @@ extension Ghostty {
guard let inspector = self.inspector else { return }
guard let key = Ghostty.Input.Key(keyCode: event.keyCode) else { return }
let mods = Ghostty.ghosttyMods(event.modifierFlags)
ghostty_inspector_key(inspector, action, key.cKey, mods)
inspector.key(action, key: key.cKey, mods: mods)
}
// MARK: NSTextInputClient
@@ -406,9 +405,7 @@ extension Ghostty {
let len = chars.utf8CString.count
if (len == 0) { return }
chars.withCString { ptr in
ghostty_inspector_text(inspector, ptr)
}
inspector.text(chars)
}
override func doCommand(by selector: Selector) {
@@ -435,11 +432,7 @@ extension Ghostty {
updateSize()
// Render
ghostty_inspector_metal_render(
inspector,
Unmanaged.passRetained(commandBuffer).toOpaque(),
Unmanaged.passRetained(descriptor).toOpaque()
)
inspector.metalRender(commandBuffer: commandBuffer, descriptor: descriptor)
guard let drawable = self.currentDrawable else { return }
commandBuffer.present(drawable)

View File

@@ -173,10 +173,11 @@ extension Ghostty {
}
// Returns the inspector instance for this surface, or nil if the
// surface has been closed.
var inspector: ghostty_inspector_t? {
// surface has been closed or no inspector is active.
var inspector: Ghostty.Inspector? {
guard let surface = self.surface else { return nil }
return ghostty_surface_inspector(surface)
guard let cInspector = ghostty_surface_inspector(surface) else { return nil }
return Ghostty.Inspector(cInspector: cInspector)
}
// True if the inspector should be visible

View File

@@ -49,6 +49,7 @@ pub fn build(b: *std.Build) !void {
var flags: std.ArrayList([]const u8) = .empty;
defer flags.deinit(b.allocator);
try flags.appendSlice(b.allocator, &.{
"-DIMGUI_HAS_DOCK=1",
"-DIMGUI_USE_WCHAR32=1",
"-DIMGUI_DISABLE_OBSOLETE_FUNCTIONS=1",
});

View File

@@ -5,6 +5,8 @@ pub const c = @cImport({
// during import time to get the right types. Without this
// you get stack size mismatches on some structs.
@cDefine("IMGUI_USE_WCHAR32", "1");
@cDefine("IMGUI_HAS_DOCK", "1");
@cInclude("dcimgui.h");
});
@@ -25,11 +27,39 @@ pub extern fn ImGui_ImplOSX_Init(*anyopaque) callconv(.c) bool;
pub extern fn ImGui_ImplOSX_Shutdown() callconv(.c) void;
pub extern fn ImGui_ImplOSX_NewFrame(*anyopaque) callconv(.c) void;
// Internal API functions from dcimgui_internal.h
// Internal API types and functions from dcimgui_internal.h
// We declare these manually because the internal header contains bitfields
// that Zig's cImport cannot translate.
pub const ImGuiDockNodeFlagsPrivate = struct {
pub const DockSpace: c.ImGuiDockNodeFlags = 1 << 10;
pub const CentralNode: c.ImGuiDockNodeFlags = 1 << 11;
pub const NoTabBar: c.ImGuiDockNodeFlags = 1 << 12;
pub const HiddenTabBar: c.ImGuiDockNodeFlags = 1 << 13;
pub const NoWindowMenuButton: c.ImGuiDockNodeFlags = 1 << 14;
pub const NoCloseButton: c.ImGuiDockNodeFlags = 1 << 15;
pub const NoResizeX: c.ImGuiDockNodeFlags = 1 << 16;
pub const NoResizeY: c.ImGuiDockNodeFlags = 1 << 17;
pub const DockedWindowsInFocusRoute: c.ImGuiDockNodeFlags = 1 << 18;
pub const NoDockingSplitOther: c.ImGuiDockNodeFlags = 1 << 19;
pub const NoDockingOverMe: c.ImGuiDockNodeFlags = 1 << 20;
pub const NoDockingOverOther: c.ImGuiDockNodeFlags = 1 << 21;
pub const NoDockingOverEmpty: c.ImGuiDockNodeFlags = 1 << 22;
};
pub extern fn ImGui_DockBuilderDockWindow(window_name: [*:0]const u8, node_id: c.ImGuiID) callconv(.c) void;
pub extern fn ImGui_DockBuilderGetNode(node_id: c.ImGuiID) callconv(.c) ?*anyopaque;
pub extern fn ImGui_DockBuilderGetCentralNode(node_id: c.ImGuiID) callconv(.c) ?*anyopaque;
pub extern fn ImGui_DockBuilderAddNode() callconv(.c) c.ImGuiID;
pub extern fn ImGui_DockBuilderAddNodeEx(node_id: c.ImGuiID, flags: c.ImGuiDockNodeFlags) callconv(.c) c.ImGuiID;
pub extern fn ImGui_DockBuilderRemoveNode(node_id: c.ImGuiID) callconv(.c) void;
pub extern fn ImGui_DockBuilderRemoveNodeDockedWindows(node_id: c.ImGuiID) callconv(.c) void;
pub extern fn ImGui_DockBuilderRemoveNodeDockedWindowsEx(node_id: c.ImGuiID, clear_settings_refs: bool) callconv(.c) void;
pub extern fn ImGui_DockBuilderRemoveNodeChildNodes(node_id: c.ImGuiID) callconv(.c) void;
pub extern fn ImGui_DockBuilderSetNodePos(node_id: c.ImGuiID, pos: c.ImVec2) callconv(.c) void;
pub extern fn ImGui_DockBuilderSetNodeSize(node_id: c.ImGuiID, size: c.ImVec2) callconv(.c) void;
pub extern fn ImGui_DockBuilderSplitNode(node_id: c.ImGuiID, split_dir: c.ImGuiDir, size_ratio_for_node_at_dir: f32, out_id_at_dir: *c.ImGuiID, out_id_at_opposite_dir: *c.ImGuiID) callconv(.c) c.ImGuiID;
pub extern fn ImGui_DockBuilderCopyDockSpace(src_dockspace_id: c.ImGuiID, dst_dockspace_id: c.ImGuiID, in_window_remap_pairs: *c.ImVector_const_charPtr) callconv(.c) void;
pub extern fn ImGui_DockBuilderCopyNode(src_node_id: c.ImGuiID, dst_node_id: c.ImGuiID, out_node_remap_pairs: *c.ImVector_ImGuiID) callconv(.c) void;
pub extern fn ImGui_DockBuilderCopyWindowSettings(src_name: [*:0]const u8, dst_name: [*:0]const u8) callconv(.c) void;
pub extern fn ImGui_DockBuilderFinish(node_id: c.ImGuiID) callconv(.c) void;
// Extension functions from ext.cpp

View File

@@ -803,7 +803,7 @@ pub fn deinit(self: *Surface) void {
self.io.deinit();
if (self.inspector) |v| {
v.deinit();
v.deinit(self.alloc);
self.alloc.destroy(v);
}
@@ -879,8 +879,10 @@ pub fn activateInspector(self: *Surface) !void {
// Setup the inspector
const ptr = try self.alloc.create(inspectorpkg.Inspector);
errdefer self.alloc.destroy(ptr);
ptr.* = try inspectorpkg.Inspector.init(self);
ptr.* = try inspectorpkg.Inspector.init(self.alloc);
errdefer ptr.deinit(self.alloc);
self.inspector = ptr;
errdefer self.inspector = null;
// Put the inspector onto the render state
{
@@ -912,7 +914,7 @@ pub fn deactivateInspector(self: *Surface) void {
self.queueIo(.{ .inspector = false }, .unlocked);
// Deinit the inspector
insp.deinit();
insp.deinit(self.alloc);
self.alloc.destroy(insp);
self.inspector = null;
}
@@ -2618,7 +2620,7 @@ pub fn keyCallback(
defer crash.sentry.thread_state = null;
// Setup our inspector event if we have an inspector.
var insp_ev: ?inspectorpkg.key.Event = if (self.inspector != null) ev: {
var insp_ev: ?inspectorpkg.KeyEvent = if (self.inspector != null) ev: {
var copy = event;
copy.utf8 = "";
if (event.utf8.len > 0) copy.utf8 = try self.alloc.dupe(u8, event.utf8);
@@ -2635,7 +2637,7 @@ pub fn keyCallback(
break :ev;
};
if (insp.recordKeyEvent(ev)) {
if (insp.recordKeyEvent(self.alloc, ev)) {
self.queueRender() catch {};
} else |err| {
log.warn("error adding key event to inspector err={}", .{err});
@@ -2798,7 +2800,7 @@ pub fn keyCallback(
fn maybeHandleBinding(
self: *Surface,
event: input.KeyEvent,
insp_ev: ?*inspectorpkg.key.Event,
insp_ev: ?*inspectorpkg.KeyEvent,
) !?InputEffect {
switch (event.action) {
// Release events never trigger a binding but we need to check if
@@ -3131,7 +3133,7 @@ fn endKeySequence(
fn encodeKey(
self: *Surface,
event: input.KeyEvent,
insp_ev: ?*inspectorpkg.key.Event,
insp_ev: ?*inspectorpkg.KeyEvent,
) !?termio.Message.WriteReq {
const write_req: termio.Message.WriteReq = req: {
// Build our encoding options, which requires the lock.
@@ -3872,36 +3874,8 @@ pub fn mouseButtonCallback(
// log.debug("mouse action={} button={} mods={}", .{ action, button, mods });
// If we have an inspector, we always queue a render
if (self.inspector) |insp| {
if (self.inspector != null) {
defer self.queueRender() catch {};
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
// If the inspector is requesting a cell, then we intercept
// left mouse clicks and send them to the inspector.
if (insp.cell == .requested and
button == .left and
action == .press)
{
const pos = try self.rt_surface.getCursorPos();
const point = self.posToViewport(pos.x, pos.y);
const screen: *terminal.Screen = self.renderer_state.terminal.screens.active;
const p = screen.pages.pin(.{ .viewport = point }) orelse {
log.warn("failed to get pin for clicked point", .{});
return false;
};
insp.cell.select(
self.alloc,
p,
point.x,
point.y,
) catch |err| {
log.warn("error selecting cell for inspector err={}", .{err});
};
return false;
}
}
// Always record our latest mouse state

View File

@@ -1061,7 +1061,7 @@ pub const Inspector = struct {
render: {
const surface = &self.surface.core_surface;
const inspector = surface.inspector orelse break :render;
inspector.render();
inspector.render(surface);
}
// Render

View File

@@ -89,7 +89,7 @@ pub const InspectorWidget = extern struct {
const surface = priv.surface orelse return;
const core_surface = surface.core() orelse return;
const inspector = core_surface.inspector orelse return;
inspector.render();
inspector.render(core_surface);
}
//---------------------------------------------------------------

12
src/inspector/AGENTS.md Normal file
View File

@@ -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.

File diff suppressed because it is too large Load Diff

View File

@@ -1,223 +0,0 @@
const std = @import("std");
const assert = @import("../quirks.zig").inlineAssert;
const Allocator = std.mem.Allocator;
const cimgui = @import("dcimgui");
const terminal = @import("../terminal/main.zig");
/// A cell being inspected. This duplicates much of the data in
/// the terminal data structure because we want the inspector to
/// not have a reference to the terminal state or to grab any
/// locks.
pub const Cell = struct {
/// The main codepoint for this cell.
codepoint: u21,
/// Codepoints for this cell to produce a single grapheme cluster.
/// This is only non-empty if the cell is part of a multi-codepoint
/// grapheme cluster. This does NOT include the primary codepoint.
cps: []const u21,
/// The style of this cell.
style: terminal.Style,
/// Wide state of the terminal cell
wide: terminal.Cell.Wide,
pub fn init(
alloc: Allocator,
pin: terminal.Pin,
) !Cell {
const cell = pin.rowAndCell().cell;
const style = pin.style(cell);
const cps: []const u21 = if (cell.hasGrapheme()) cps: {
const src = pin.grapheme(cell).?;
assert(src.len > 0);
break :cps try alloc.dupe(u21, src);
} else &.{};
errdefer if (cps.len > 0) alloc.free(cps);
return .{
.codepoint = cell.codepoint(),
.cps = cps,
.style = style,
.wide = cell.wide,
};
}
pub fn deinit(self: *Cell, alloc: Allocator) void {
if (self.cps.len > 0) alloc.free(self.cps);
}
pub fn renderTable(
self: *const Cell,
t: *const terminal.Terminal,
x: usize,
y: usize,
) void {
// We have a selected cell, show information about it.
_ = cimgui.c.ImGui_BeginTable(
"table_cursor",
2,
cimgui.c.ImGuiTableFlags_None,
);
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);
cimgui.c.ImGui_Text("row=%d col=%d", y, x);
}
}
// NOTE: we don't currently write the character itself because
// we haven't hooked up imgui to our font system. That's hard! We
// can/should instead hook up our renderer to imgui and just render
// the single glyph in an image view so it looks _identical_ to the
// terminal.
codepoint: {
cimgui.c.ImGui_TableNextRow();
{
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
cimgui.c.ImGui_Text("Codepoints");
}
{
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
if (cimgui.c.ImGui_BeginListBox("##codepoints", .{ .x = 0, .y = 0 })) {
defer cimgui.c.ImGui_EndListBox();
if (self.codepoint == 0) {
_ = cimgui.c.ImGui_SelectableEx("(empty)", false, 0, .{});
break :codepoint;
}
// Primary codepoint
var buf: [256]u8 = undefined;
{
const key = std.fmt.bufPrintZ(&buf, "U+{X}", .{self.codepoint}) catch
"<internal error>";
_ = cimgui.c.ImGui_SelectableEx(key.ptr, false, 0, .{});
}
// All extras
for (self.cps) |cp| {
const key = std.fmt.bufPrintZ(&buf, "U+{X}", .{cp}) catch
"<internal error>";
_ = cimgui.c.ImGui_SelectableEx(key.ptr, false, 0, .{});
}
}
}
}
// Character width property
cimgui.c.ImGui_TableNextRow();
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
cimgui.c.ImGui_Text("Width Property");
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
cimgui.c.ImGui_Text(@tagName(self.wide));
// If we have a color then we show the color
cimgui.c.ImGui_TableNextRow();
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
cimgui.c.ImGui_Text("Foreground Color");
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
switch (self.style.fg_color) {
.none => cimgui.c.ImGui_Text("default"),
.palette => |idx| {
const rgb = t.colors.palette.current[idx];
cimgui.c.ImGui_Text("Palette %d", idx);
var color: [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",
&color,
cimgui.c.ImGuiColorEditFlags_DisplayHex |
cimgui.c.ImGuiColorEditFlags_NoPicker |
cimgui.c.ImGuiColorEditFlags_NoLabel,
);
},
.rgb => |rgb| {
var color: [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",
&color,
cimgui.c.ImGuiColorEditFlags_DisplayHex |
cimgui.c.ImGuiColorEditFlags_NoPicker |
cimgui.c.ImGuiColorEditFlags_NoLabel,
);
},
}
cimgui.c.ImGui_TableNextRow();
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
cimgui.c.ImGui_Text("Background Color");
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
switch (self.style.bg_color) {
.none => cimgui.c.ImGui_Text("default"),
.palette => |idx| {
const rgb = t.colors.palette.current[idx];
cimgui.c.ImGui_Text("Palette %d", idx);
var color: [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_bg",
&color,
cimgui.c.ImGuiColorEditFlags_DisplayHex |
cimgui.c.ImGuiColorEditFlags_NoPicker |
cimgui.c.ImGuiColorEditFlags_NoLabel,
);
},
.rgb => |rgb| {
var color: [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_bg",
&color,
cimgui.c.ImGuiColorEditFlags_DisplayHex |
cimgui.c.ImGuiColorEditFlags_NoPicker |
cimgui.c.ImGuiColorEditFlags_NoLabel,
);
},
}
// Boolean styles
const styles = .{
"bold", "italic", "faint", "blink",
"inverse", "invisible", "strikethrough",
};
inline for (styles) |style| style: {
if (!@field(self.style.flags, style)) break :style;
cimgui.c.ImGui_TableNextRow();
{
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
cimgui.c.ImGui_Text(style.ptr);
}
{
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
cimgui.c.ImGui_Text("true");
}
}
cimgui.c.ImGui_TextDisabled("(Any styles not shown are not currently set)");
}
};

View File

@@ -1,142 +0,0 @@
const cimgui = @import("dcimgui");
const terminal = @import("../terminal/main.zig");
/// Render cursor information with a table already open.
pub fn renderInTable(
t: *const terminal.Terminal,
cursor: *const terminal.Screen.Cursor,
) void {
{
cimgui.c.ImGui_TableNextRow();
{
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
cimgui.c.ImGui_Text("Position (x, y)");
}
{
_ = 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("Style");
}
{
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
cimgui.c.ImGui_Text("%s", @tagName(cursor.cursor_style).ptr);
}
}
if (cursor.pending_wrap) {
cimgui.c.ImGui_TableNextRow();
{
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
cimgui.c.ImGui_Text("Pending Wrap");
}
{
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
cimgui.c.ImGui_Text("%s", if (cursor.pending_wrap) "true".ptr else "false".ptr);
}
}
// If we have a color then we show the color
cimgui.c.ImGui_TableNextRow();
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
cimgui.c.ImGui_Text("Foreground Color");
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
switch (cursor.style.fg_color) {
.none => cimgui.c.ImGui_Text("default"),
.palette => |idx| {
const rgb = t.colors.palette.current[idx];
cimgui.c.ImGui_Text("Palette %d", idx);
var color: [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",
&color,
cimgui.c.ImGuiColorEditFlags_DisplayHex |
cimgui.c.ImGuiColorEditFlags_NoPicker |
cimgui.c.ImGuiColorEditFlags_NoLabel,
);
},
.rgb => |rgb| {
var color: [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",
&color,
cimgui.c.ImGuiColorEditFlags_DisplayHex |
cimgui.c.ImGuiColorEditFlags_NoPicker |
cimgui.c.ImGuiColorEditFlags_NoLabel,
);
},
}
cimgui.c.ImGui_TableNextRow();
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
cimgui.c.ImGui_Text("Background Color");
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
switch (cursor.style.bg_color) {
.none => cimgui.c.ImGui_Text("default"),
.palette => |idx| {
const rgb = t.colors.palette.current[idx];
cimgui.c.ImGui_Text("Palette %d", idx);
var color: [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_bg",
&color,
cimgui.c.ImGuiColorEditFlags_DisplayHex |
cimgui.c.ImGuiColorEditFlags_NoPicker |
cimgui.c.ImGuiColorEditFlags_NoLabel,
);
},
.rgb => |rgb| {
var color: [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_bg",
&color,
cimgui.c.ImGuiColorEditFlags_DisplayHex |
cimgui.c.ImGuiColorEditFlags_NoPicker |
cimgui.c.ImGuiColorEditFlags_NoLabel,
);
},
}
// Boolean styles
const styles = .{
"bold", "italic", "faint", "blink",
"inverse", "invisible", "strikethrough",
};
inline for (styles) |style| style: {
if (!@field(cursor.style.flags, style)) break :style;
cimgui.c.ImGui_TableNextRow();
{
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
cimgui.c.ImGui_Text(style.ptr);
}
{
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
cimgui.c.ImGui_Text("true");
}
}
}

View File

@@ -1,240 +0,0 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const input = @import("../input.zig");
const CircBuf = @import("../datastruct/main.zig").CircBuf;
const cimgui = @import("dcimgui");
/// Circular buffer of key events.
pub const EventRing = CircBuf(Event, undefined);
/// Represents a recorded keyboard event.
pub const Event = struct {
/// The input event.
event: input.KeyEvent,
/// The binding that was triggered as a result of this event.
/// Multiple bindings are possible if they are chained.
binding: []const input.Binding.Action = &.{},
/// The data sent to the pty as a result of this keyboard event.
/// This is allocated using the inspector allocator.
pty: []const u8 = "",
/// State for the inspector GUI. Do not set this unless you're the inspector.
imgui_state: struct {
selected: bool = false,
} = .{},
pub fn init(alloc: Allocator, event: input.KeyEvent) !Event {
var copy = event;
copy.utf8 = "";
if (event.utf8.len > 0) copy.utf8 = try alloc.dupe(u8, event.utf8);
return .{ .event = copy };
}
pub fn deinit(self: *const Event, alloc: Allocator) void {
alloc.free(self.binding);
if (self.event.utf8.len > 0) alloc.free(self.event.utf8);
if (self.pty.len > 0) alloc.free(self.pty);
}
/// Returns a label that can be used for this event. This is null-terminated
/// so it can be easily used with C APIs.
pub fn label(self: *const Event, buf: []u8) ![:0]const u8 {
var buf_stream = std.io.fixedBufferStream(buf);
const writer = buf_stream.writer();
switch (self.event.action) {
.press => try writer.writeAll("Press: "),
.release => try writer.writeAll("Release: "),
.repeat => try writer.writeAll("Repeat: "),
}
if (self.event.mods.shift) try writer.writeAll("Shift+");
if (self.event.mods.ctrl) try writer.writeAll("Ctrl+");
if (self.event.mods.alt) try writer.writeAll("Alt+");
if (self.event.mods.super) try writer.writeAll("Super+");
// Write our key. If we have an invalid key we attempt to write
// the utf8 associated with it if we have it to handle non-ascii.
try writer.writeAll(switch (self.event.key) {
.unidentified => if (self.event.utf8.len > 0) self.event.utf8 else @tagName(self.event.key),
else => @tagName(self.event.key),
});
// Deadkey
if (self.event.composing) try writer.writeAll(" (composing)");
// Null-terminator
try writer.writeByte(0);
return buf[0..(buf_stream.getWritten().len - 1) :0];
}
/// Render this event in the inspector GUI.
pub fn render(self: *const Event) void {
_ = cimgui.c.ImGui_BeginTable(
"##event",
2,
cimgui.c.ImGuiTableFlags_None,
);
defer cimgui.c.ImGui_EndTable();
if (self.binding.len > 0) {
cimgui.c.ImGui_TableNextRow();
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
cimgui.c.ImGui_Text("Triggered Binding");
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
const height: f32 = height: {
const item_count: f32 = @floatFromInt(@min(self.binding.len, 5));
const padding = cimgui.c.ImGui_GetStyle().*.FramePadding.y * 2;
break :height cimgui.c.ImGui_GetTextLineHeightWithSpacing() * item_count + padding;
};
if (cimgui.c.ImGui_BeginListBox("##bindings", .{ .x = 0, .y = height })) {
defer cimgui.c.ImGui_EndListBox();
for (self.binding) |action| {
_ = cimgui.c.ImGui_SelectableEx(
@tagName(action).ptr,
false,
cimgui.c.ImGuiSelectableFlags_None,
.{ .x = 0, .y = 0 },
);
}
}
}
pty: {
cimgui.c.ImGui_TableNextRow();
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
cimgui.c.ImGui_Text("Encoding to Pty");
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
if (self.pty.len == 0) {
cimgui.c.ImGui_TextDisabled("(no data)");
break :pty;
}
self.renderPty() catch {
cimgui.c.ImGui_TextDisabled("(error rendering pty data)");
break :pty;
};
}
{
cimgui.c.ImGui_TableNextRow();
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
cimgui.c.ImGui_Text("Action");
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
cimgui.c.ImGui_Text("%s", @tagName(self.event.action).ptr);
}
{
cimgui.c.ImGui_TableNextRow();
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
cimgui.c.ImGui_Text("Key");
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
cimgui.c.ImGui_Text("%s", @tagName(self.event.key).ptr);
}
if (!self.event.mods.empty()) {
cimgui.c.ImGui_TableNextRow();
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
cimgui.c.ImGui_Text("Mods");
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
if (self.event.mods.shift) cimgui.c.ImGui_Text("shift ");
if (self.event.mods.ctrl) cimgui.c.ImGui_Text("ctrl ");
if (self.event.mods.alt) cimgui.c.ImGui_Text("alt ");
if (self.event.mods.super) cimgui.c.ImGui_Text("super ");
}
if (self.event.composing) {
cimgui.c.ImGui_TableNextRow();
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
cimgui.c.ImGui_Text("Composing");
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
cimgui.c.ImGui_Text("true");
}
utf8: {
cimgui.c.ImGui_TableNextRow();
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
cimgui.c.ImGui_Text("UTF-8");
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
if (self.event.utf8.len == 0) {
cimgui.c.ImGui_TextDisabled("(empty)");
break :utf8;
}
self.renderUtf8(self.event.utf8) catch {
cimgui.c.ImGui_TextDisabled("(error rendering utf-8)");
break :utf8;
};
}
}
fn renderUtf8(self: *const Event, utf8: []const u8) !void {
_ = self;
// Format the codepoint sequence
var buf: [1024]u8 = undefined;
var buf_stream = std.io.fixedBufferStream(&buf);
const writer = buf_stream.writer();
if (std.unicode.Utf8View.init(utf8)) |view| {
var it = view.iterator();
while (it.nextCodepoint()) |cp| {
try writer.print("U+{X} ", .{cp});
}
} else |_| {
try writer.writeAll("(invalid utf-8)");
}
try writer.writeByte(0);
// Render as a textbox
_ = cimgui.c.ImGui_InputText(
"##utf8",
&buf,
buf_stream.getWritten().len - 1,
cimgui.c.ImGuiInputTextFlags_ReadOnly,
);
}
fn renderPty(self: *const Event) !void {
// Format the codepoint sequence
var buf: [1024]u8 = undefined;
var buf_stream = std.io.fixedBufferStream(&buf);
const writer = buf_stream.writer();
for (self.pty) |byte| {
// Print ESC special because its so common
if (byte == 0x1B) {
try writer.writeAll("ESC ");
continue;
}
// Print ASCII as-is
if (byte > 0x20 and byte < 0x7F) {
try writer.writeByte(byte);
continue;
}
// Everything else as a hex byte
try writer.print("0x{X} ", .{byte});
}
try writer.writeByte(0);
// Render as a textbox
_ = cimgui.c.ImGui_InputText(
"##pty",
&buf,
buf_stream.getWritten().len - 1,
cimgui.c.ImGuiInputTextFlags_ReadOnly,
);
}
};
test "event string" {
const testing = std.testing;
const alloc = testing.allocator;
var event = try Event.init(alloc, .{ .key = .key_a });
defer event.deinit(alloc);
var buf: [1024]u8 = undefined;
try testing.expectEqualStrings("Press: key_a", try event.label(&buf));
}

View File

@@ -1,13 +1,8 @@
const std = @import("std");
pub const cell = @import("cell.zig");
pub const cursor = @import("cursor.zig");
pub const key = @import("key.zig");
pub const page = @import("page.zig");
pub const termio = @import("termio.zig");
pub const Cell = cell.Cell;
pub const widgets = @import("widgets.zig");
pub const Inspector = @import("Inspector.zig");
pub const KeyEvent = widgets.key.Event;
test {
@import("std").testing.refAllDecls(@This());
}

View File

@@ -1,163 +0,0 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const cimgui = @import("dcimgui");
const terminal = @import("../terminal/main.zig");
const units = @import("units.zig");
pub fn render(page: *const terminal.Page) void {
cimgui.c.ImGui_PushIDPtr(page);
defer cimgui.c.ImGui_PopID();
_ = cimgui.c.ImGui_BeginTable(
"##page_state",
2,
cimgui.c.ImGuiTableFlags_None,
);
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);
cimgui.c.ImGui_Text("%d bytes (%d KiB)", page.memory.len, units.toKibiBytes(page.memory.len));
cimgui.c.ImGui_Text("%d VM pages", page.memory.len / std.heap.page_size_min);
}
}
{
cimgui.c.ImGui_TableNextRow();
{
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
cimgui.c.ImGui_Text("Unique Styles");
}
{
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
cimgui.c.ImGui_Text("%d", page.styles.count());
}
}
{
cimgui.c.ImGui_TableNextRow();
{
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
cimgui.c.ImGui_Text("Grapheme Entries");
}
{
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
cimgui.c.ImGui_Text("%d", page.graphemeCount());
}
}
{
cimgui.c.ImGui_TableNextRow();
{
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
cimgui.c.ImGui_Text("Capacity");
}
{
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
_ = cimgui.c.ImGui_BeginTable(
"##capacity",
2,
cimgui.c.ImGuiTableFlags_None,
);
defer cimgui.c.ImGui_EndTable();
const cap = page.capacity;
{
cimgui.c.ImGui_TableNextRow();
{
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
cimgui.c.ImGui_Text("Columns");
}
{
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
cimgui.c.ImGui_Text("%d", @as(u32, @intCast(cap.cols)));
}
}
{
cimgui.c.ImGui_TableNextRow();
{
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
cimgui.c.ImGui_Text("Rows");
}
{
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
cimgui.c.ImGui_Text("%d", @as(u32, @intCast(cap.rows)));
}
}
{
cimgui.c.ImGui_TableNextRow();
{
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
cimgui.c.ImGui_Text("Unique Styles");
}
{
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
cimgui.c.ImGui_Text("%d", @as(u32, @intCast(cap.styles)));
}
}
{
cimgui.c.ImGui_TableNextRow();
{
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
cimgui.c.ImGui_Text("Grapheme Bytes");
}
{
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
cimgui.c.ImGui_Text("%d", cap.grapheme_bytes);
}
}
}
}
{
cimgui.c.ImGui_TableNextRow();
{
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
cimgui.c.ImGui_Text("Size");
}
{
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
_ = cimgui.c.ImGui_BeginTable(
"##size",
2,
cimgui.c.ImGuiTableFlags_None,
);
defer cimgui.c.ImGui_EndTable();
const size = page.size;
{
cimgui.c.ImGui_TableNextRow();
{
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
cimgui.c.ImGui_Text("Columns");
}
{
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
cimgui.c.ImGui_Text("%d", @as(u32, @intCast(size.cols)));
}
}
{
cimgui.c.ImGui_TableNextRow();
{
_ = cimgui.c.ImGui_TableSetColumnIndex(0);
cimgui.c.ImGui_Text("Rows");
}
{
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
cimgui.c.ImGui_Text("%d", @as(u32, @intCast(size.rows)));
}
}
}
} // size table
}

View File

@@ -1,398 +0,0 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const cimgui = @import("dcimgui");
const terminal = @import("../terminal/main.zig");
const CircBuf = @import("../datastruct/main.zig").CircBuf;
const Surface = @import("../Surface.zig");
/// The stream handler for our inspector.
pub const Stream = terminal.Stream(VTHandler);
/// VT event circular buffer.
pub const VTEventRing = CircBuf(VTEvent, undefined);
/// VT event
pub const VTEvent = struct {
/// Sequence number, just monotonically increasing.
seq: usize = 1,
/// Kind of event, for filtering
kind: Kind,
/// The formatted string of the event. This is allocated. We format the
/// event for now because there is so much data to copy if we wanted to
/// store the raw event.
str: [: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);
/// Initialize the event information for the given parser action.
pub fn init(
alloc: Allocator,
surface: *Surface,
action: terminal.Parser.Action,
) !VTEvent {
var md = Metadata.init(alloc);
errdefer md.deinit();
var buf: std.Io.Writer.Allocating = .init(alloc);
defer buf.deinit();
try encodeAction(alloc, &buf.writer, &md, action);
const str = try buf.toOwnedSliceSentinel(0);
errdefer alloc.free(str);
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,
};
const t = surface.renderer_state.terminal;
return .{
.kind = kind,
.str = str,
.cursor = t.screens.active.cursor,
.scrolling_region = t.scrolling_region,
.metadata = md.unmanaged,
};
}
pub fn deinit(self: *VTEvent, alloc: Allocator) void {
{
var it = self.metadata.valueIterator();
while (it.next()) |v| alloc.free(v.*);
self.metadata.deinit(alloc);
}
alloc.free(self.str);
}
/// 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.str.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.
pub const VTHandler = struct {
/// The surface that the inspector is attached to. We use this instead
/// of the inspector because this is pointer-stable.
surface: *Surface,
/// True if the handler is currently recording.
active: bool = true,
/// Current sequence number
current_seq: usize = 1,
/// Exclude certain actions by tag.
filter_exclude: ActionTagSet = .initMany(&.{.print}),
filter_text: cimgui.c.ImGuiTextFilter = .{},
const ActionTagSet = std.EnumSet(terminal.Parser.Action.Tag);
pub fn init(surface: *Surface) VTHandler {
return .{
.surface = surface,
};
}
pub fn deinit(self: *VTHandler) void {
_ = self;
}
pub fn vt(
self: *VTHandler,
comptime action: Stream.Action.Tag,
value: Stream.Action.Value(action),
) !void {
_ = self;
_ = value;
}
/// This is called with every single terminal action.
pub fn handleManually(self: *VTHandler, action: terminal.Parser.Action) !bool {
const insp = self.surface.inspector orelse return false;
// 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 pausing, then we ignore all events.
if (!self.active) 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
const alloc = self.surface.alloc;
var ev = try VTEvent.init(alloc, self.surface, 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;
insp.vt_events.append(ev) catch |err| switch (err) {
error.OutOfMemory => if (insp.vt_events.capacity() < max_capacity) {
// We're out of memory, but we can allocate to our capacity.
const new_capacity = @min(insp.vt_events.capacity() * 2, max_capacity);
try insp.vt_events.resize(insp.surface.alloc, new_capacity);
try insp.vt_events.append(ev);
} else {
var it = insp.vt_events.iterator(.forward);
if (it.next()) |old_ev| old_ev.deinit(insp.surface.alloc);
insp.vt_events.deleteOldest(1);
try insp.vt_events.append(ev);
},
else => return err,
};
return true;
}
};

227
src/inspector/widgets.zig Normal file
View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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("-");
}
}

View File

@@ -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);
}
}
};

View File

@@ -0,0 +1,71 @@
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_SeparatorText("Overlays");
// Hyperlinks
{
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", .{});
}
}
}
};

View File

@@ -0,0 +1,355 @@
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_None,
)) {
cursorTable(&screen.cursor);
cimgui.c.ImGui_Separator();
cursorStyle(
&screen.cursor,
&data.color_palette.current,
);
}
if (cimgui.c.ImGui_CollapsingHeader(
"Keyboard",
cimgui.c.ImGuiTreeNodeFlags_None,
)) keyboardTable(
screen,
data.modify_other_keys_2,
);
if (cimgui.c.ImGui_CollapsingHeader(
"Kitty Graphics",
cimgui.c.ImGuiTreeNodeFlags_None,
)) kittyGraphicsTable(&screen.kitty_images);
if (cimgui.c.ImGui_CollapsingHeader(
"Internal Terminal State",
cimgui.c.ImGuiTreeNodeFlags_None,
)) internalStateTable(&screen.pages);
}
// 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(
pages: *const terminal.PageList,
) void {
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);
}

View File

@@ -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,
);
},
}
}

View File

@@ -0,0 +1,500 @@
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,
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,
};
}
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);
// In debug we show the ImGui demo window so we can easily view
// available widgets and such.
if (comptime builtin.mode == .Debug) {
var show: bool = true; // Always show it
cimgui.c.ImGui_ShowDemoWindow(&show);
}
// 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,
);
}
}
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_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_DockBuilderDockWindow(window_imgui_demo, 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)),
);
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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;
try 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,
};

View File

@@ -19,6 +19,7 @@ pub const Metal = @import("renderer/Metal.zig");
pub const OpenGL = @import("renderer/OpenGL.zig");
pub const WebGL = @import("renderer/WebGL.zig");
pub const Options = @import("renderer/Options.zig");
pub const Overlay = @import("renderer/Overlay.zig");
pub const Thread = @import("renderer/Thread.zig");
pub const State = @import("renderer/State.zig");
pub const CursorStyle = cursor.Style;

View File

@@ -225,13 +225,6 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
/// Our overlay state, if any.
overlay: ?Overlay = null,
// Right now, the debug overlay is turned on and configured by
// modifying these and recompiling. In the future, we will expose
// all of this at runtime via the inspector.
const overlay_features: []const Overlay.Feature = &.{
//.highlight_hyperlinks,
};
const HighlightTag = enum(u8) {
search_match,
search_match_selected,
@@ -1152,6 +1145,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
mouse: renderer.State.Mouse,
preedit: ?renderer.State.Preedit,
scrollbar: terminal.Scrollbar,
overlay_features: []const Overlay.Feature,
};
// Update all our data as tightly as possible within the mutex.
@@ -1231,11 +1225,20 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
};
};
const overlay_features: []const Overlay.Feature = overlay: {
const insp = state.inspector orelse break :overlay &.{};
const renderer_info = insp.rendererInfo();
break :overlay renderer_info.overlayFeatures(
arena_alloc,
) catch &.{};
};
break :critical .{
.links = links,
.mouse = state.mouse,
.preedit = preedit,
.scrollbar = scrollbar,
.overlay_features = overlay_features,
};
};
@@ -1306,7 +1309,9 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
// Rebuild the overlay image if we have one. We can do this
// outside of any critical areas.
self.rebuildOverlay() catch |err| {
self.rebuildOverlay(
critical.overlay_features,
) catch |err| {
log.warn(
"error rebuilding overlay surface err={}",
.{err},
@@ -2241,7 +2246,10 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
/// Build the overlay as configured. Returns null if there is no
/// overlay currently configured.
fn rebuildOverlay(self: *Self) Overlay.InitError!void {
fn rebuildOverlay(
self: *Self,
features: []const Overlay.Feature,
) Overlay.InitError!void {
// const start = std.time.Instant.now() catch unreachable;
// const start_micro = std.time.microTimestamp();
// defer {
@@ -2256,7 +2264,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
// If we have no features enabled, don't build an overlay.
// If we had a previous overlay, deallocate it.
if (overlay_features.len == 0) {
if (features.len == 0) {
if (self.overlay) |*old| {
old.deinit(alloc);
self.overlay = null;
@@ -2277,7 +2285,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
overlay.applyFeatures(
alloc,
&self.terminal_state,
overlay_features,
features,
);
}

View File

@@ -3889,6 +3889,12 @@ pub fn countTrackedPins(self: *const PageList) usize {
return self.tracked_pins.count();
}
/// Returns the tracked pins for this pagelist. The slice is owned by the
/// pagelist and is only valid until the pagelist is modified.
pub fn trackedPins(self: *const PageList) []const *Pin {
return self.tracked_pins.keys();
}
/// Checks if a pin is valid for this pagelist. This is a very slow and
/// expensive operation since we traverse the entire linked list in the
/// worst case. Only for runtime safety/debug.
@@ -5085,7 +5091,7 @@ pub const Pin = struct {
}
};
const Cell = struct {
pub const Cell = struct {
node: *List.Node,
row: *pagepkg.Row,
cell: *pagepkg.Cell,

View File

@@ -147,6 +147,20 @@ pub fn BitmapAllocator(comptime chunk_size: comptime_int) type {
}
}
/// Returns the total capacity in bytes.
pub fn capacityBytes(self: Self) usize {
return self.bitmap_count * bitmap_bit_size * chunk_size;
}
/// Returns the number of bytes currently in use.
pub fn usedBytes(self: Self, base: anytype) usize {
const bitmaps = self.bitmap.ptr(base);
var free_chunks: usize = 0;
for (bitmaps[0..self.bitmap_count]) |bitmap| free_chunks += @popCount(bitmap);
const total_chunks = self.bitmap_count * bitmap_bit_size;
return (total_chunks - free_chunks) * chunk_size;
}
/// For testing only.
fn isAllocated(self: *Self, base: anytype, slice: anytype) bool {
comptime assert(@import("builtin").is_test);

View File

@@ -707,19 +707,21 @@ pub fn Stream(comptime Handler: type) type {
const action = action_opt orelse continue;
if (comptime debug) log.info("action: {f}", .{action});
// If this handler handles everything manually then we do nothing
// if it can be processed.
if (@hasDecl(T, "handleManually")) {
const processed = self.handler.handleManually(action) catch |err| err: {
// A handler can expose this to get the raw action before
// it is further parsed. If this returns `true` then we skip
// processing ourselves.
if (@hasDecl(T, "vtRaw")) {
const skip = self.handler.vtRaw(action) catch |err| err: {
log.warn("error handling action manually err={} action={f}", .{
err,
action,
});
break :err false;
// Always skip erroneous actions because we can't
// be sure...
break :err true;
};
if (processed) continue;
if (skip) continue;
}
switch (action) {

View File

@@ -694,7 +694,11 @@ fn processOutputLocked(self: *Termio, buf: []const u8) void {
// below but at least users only pay for it if they're using the inspector.
if (self.renderer_state.inspector) |insp| {
for (buf, 0..) |byte, i| {
insp.recordPtyRead(buf[i .. i + 1]) catch |err| {
insp.recordPtyRead(
self.alloc,
&self.terminal,
buf[i .. i + 1],
) catch |err| {
log.err("error recording pty read in inspector err={}", .{err});
};