mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-14 03:25:50 +00:00
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:
@@ -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:
|
||||
|
||||
100
macos/Sources/Ghostty/Ghostty.Inspector.swift
Normal file
100
macos/Sources/Ghostty/Ghostty.Inspector.swift
Normal 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()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
12
src/inspector/AGENTS.md
Normal 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
@@ -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)");
|
||||
}
|
||||
};
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
227
src/inspector/widgets.zig
Normal 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);
|
||||
}
|
||||
535
src/inspector/widgets/key.zig
Normal file
535
src/inspector/widgets/key.zig
Normal 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;
|
||||
}
|
||||
428
src/inspector/widgets/page.zig
Normal file
428
src/inspector/widgets/page.zig
Normal 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("-");
|
||||
}
|
||||
}
|
||||
852
src/inspector/widgets/pagelist.zig
Normal file
852
src/inspector/widgets/pagelist.zig
Normal 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);
|
||||
}
|
||||
}
|
||||
};
|
||||
71
src/inspector/widgets/renderer.zig
Normal file
71
src/inspector/widgets/renderer.zig
Normal 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", .{});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
355
src/inspector/widgets/screen.zig
Normal file
355
src/inspector/widgets/screen.zig
Normal 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);
|
||||
}
|
||||
125
src/inspector/widgets/style.zig
Normal file
125
src/inspector/widgets/style.zig
Normal 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,
|
||||
);
|
||||
},
|
||||
}
|
||||
}
|
||||
500
src/inspector/widgets/surface.zig
Normal file
500
src/inspector/widgets/surface.zig
Normal 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)),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
726
src/inspector/widgets/terminal.zig
Normal file
726
src/inspector/widgets/terminal.zig
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
811
src/inspector/widgets/termio.zig
Normal file
811
src/inspector/widgets/termio.zig
Normal 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,
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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});
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user