gtk-ng: port the terminal inspector

This is a (relatively) straightforward port of the terminal inspector
from the old GTK application runtime. It's split into three widgets. At
the lowest level is a widget designed for showing a generic Dear ImGui
application. Above that is a widget that embeds the ImGui widget and
plumbs it into the core Inspector. At the top is a custom Window widget
that embeds the Inspector widget.

And then there's all the plumbing necessary to hook everything into the
rest of Ghostty.

In theory this design _should_ allow showing the Inspector in a split
or a tab in the future, not just in a separate window. It should also
make it easier to display _other_ Dear ImGui applications if they are
ever needed.
pull/8212/head
Jeffrey C. Ollie 2025-08-11 22:48:37 -05:00 committed by Mitchell Hashimoto
parent 57f1033198
commit bd7177a924
12 changed files with 1135 additions and 10 deletions

View File

@ -99,7 +99,6 @@ pub fn performIpc(
}
/// Redraw the inspector for the given surface.
pub fn redrawInspector(self: *App, surface: *Surface) void {
_ = self;
_ = surface;
pub fn redrawInspector(_: *App, surface: *Surface) void {
surface.redrawInspector();
}

View File

@ -95,3 +95,8 @@ pub fn setClipboardString(
pub fn defaultTermioEnv(self: *Self) !std.process.EnvMap {
return try self.surface.defaultTermioEnv();
}
/// Redraw the inspector for our surface.
pub fn redrawInspector(self: *Self) void {
self.surface.redrawInspector();
}

View File

@ -48,6 +48,9 @@ pub const blueprints: []const Blueprint = &.{
.{ .major = 1, .minor = 5, .name = "tab" },
.{ .major = 1, .minor = 5, .name = "window" },
.{ .major = 1, .minor = 5, .name = "command-palette" },
.{ .major = 1, .minor = 5, .name = "imgui-widget" },
.{ .major = 1, .minor = 5, .name = "inspector-widget" },
.{ .major = 1, .minor = 5, .name = "inspector-window" },
};
/// CSS files in css_path

View File

@ -561,6 +561,8 @@ pub const Application = extern struct {
.initial_size => return Action.initialSize(target, value),
.inspector => return Action.controlInspector(target, value),
.mouse_over_link => Action.mouseOverLink(target, value),
.mouse_shape => Action.mouseShape(target, value),
.mouse_visibility => Action.mouseVisibility(target, value),
@ -620,13 +622,6 @@ pub const Application = extern struct {
.toggle_split_zoom => return Action.toggleSplitZoom(target),
.show_on_screen_keyboard => return Action.showOnScreenKeyboard(target),
// Unimplemented but todo on gtk-ng branch
.inspector,
=> {
log.warn("unimplemented action={}", .{action});
return false;
},
// Unimplemented
.secure_input,
.close_all_windows,
@ -2235,6 +2230,15 @@ const Action = struct {
},
}
}
pub fn controlInspector(target: apprt.Target, value: apprt.Action.Value(.inspector)) bool {
switch (target) {
.app => return false,
.surface => |surface| {
return surface.rt_surface.gobj().controlInspector(value);
},
}
}
};
/// This sets various GTK-related environment variables as necessary

View File

@ -0,0 +1,492 @@
const std = @import("std");
const adw = @import("adw");
const gdk = @import("gdk");
const gobject = @import("gobject");
const gtk = @import("gtk");
const cimgui = @import("cimgui");
const gl = @import("opengl");
const input = @import("../../../input.zig");
const gresource = @import("../build/gresource.zig");
const key = @import("../key.zig");
const Common = @import("../class.zig").Common;
const log = std.log.scoped(.gtk_ghostty_imgui_widget);
pub const RenderCallback = *const fn (?*anyopaque) void;
pub const RenderUserdata = *anyopaque;
/// A widget for embedding a Dear ImGui application.
pub const ImguiWidget = extern struct {
const Self = @This();
parent_instance: Parent,
pub const Parent = adw.Bin;
pub const getGObjectType = gobject.ext.defineClass(Self, .{
.name = "GhosttyImguiWidget",
.instanceInit = &init,
.classInit = &Class.init,
.parent_class = &Class.parent,
.private = .{ .Type = Private, .offset = &Private.offset },
});
pub const properties = struct {};
pub const signals = struct {};
const Private = struct {
/// GL area where we display the Dear ImGui application.
gl_area: *gtk.GLArea,
/// GTK input method context
im_context: *gtk.IMMulticontext,
/// Dear ImGui context
ig_context: ?*cimgui.c.ImGuiContext = null,
/// True if the the Dear ImGui OpenGL backend was initialized.
ig_gl_backend_initialized: bool = false,
/// Our previous instant used to calculate delta time for animations.
instant: ?std.time.Instant = null,
/// This is called every frame to populate the Dear ImGui frame.
render_callback: ?RenderCallback = null,
render_userdata: ?RenderUserdata = null,
pub var offset: c_int = 0;
};
//---------------------------------------------------------------
// Virtual Methods
fn init(self: *Self, _: *Class) callconv(.c) void {
gtk.Widget.initTemplate(self.as(gtk.Widget));
const priv = self.private();
priv.ig_context = ig_context: {
const ig_context = cimgui.c.igCreateContext(null) orelse {
log.warn("unable to initialize Dear ImGui context", .{});
break :ig_context null;
};
errdefer cimgui.c.igDestroyContext(ig_context);
cimgui.c.igSetCurrentContext(ig_context);
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
io.BackendPlatformName = "ghostty_gtk";
break :ig_context ig_context;
};
}
fn dispose(self: *Self) callconv(.c) void {
gtk.Widget.disposeTemplate(
self.as(gtk.Widget),
getGObjectType(),
);
gobject.Object.virtual_methods.dispose.call(
Class.parent,
self.as(Parent),
);
}
fn finalize(self: *Self) callconv(.c) void {
const priv = self.private();
// If the the Dear ImGui OpenGL backend was never initialized then we
// need to destroy the Dear ImGui context manually here. If it _was_
// initialized cleanup will be handled when the GLArea is unrealized.
if (!priv.ig_gl_backend_initialized) {
if (priv.ig_context) |ig_context| {
cimgui.c.igDestroyContext(ig_context);
priv.ig_context = null;
}
}
gobject.Object.virtual_methods.finalize.call(
Class.parent,
self.as(Parent),
);
}
//---------------------------------------------------------------
// Public methods
pub fn new() *Self {
return gobject.ext.newInstance(Self, .{});
}
/// Use to setup the Dear ImGui application.
pub fn setup(self: *Self, callback: *const fn () void) void {
self.setCurrentContext() catch return;
callback();
}
/// Set the callback used to render every frame.
pub fn setRenderCallback(
self: *Self,
callback: ?RenderCallback,
userdata: ?RenderUserdata,
) void {
const priv = self.private();
priv.render_callback = callback;
priv.render_userdata = userdata;
}
/// This should be called anytime the underlying data for the UI changes
/// so that the UI can be refreshed.
pub fn queueRender(self: *ImguiWidget) void {
const priv = self.private();
priv.gl_area.queueRender();
}
//---------------------------------------------------------------
// Private Methods
/// Set our imgui context to be current, or return an error.
fn setCurrentContext(self: *Self) error{ContextNotInitialized}!void {
const priv = self.private();
const ig_context = priv.ig_context orelse {
log.warn("Dear ImGui context not initialized", .{});
return error.ContextNotInitialized;
};
cimgui.c.igSetCurrentContext(ig_context);
}
/// Initialize the frame. Expects that the context is already current.
fn newFrame(self: *Self) void {
// If we can't determine the time since the last frame we default to
// 1/60th of a second.
const default_delta_time = 1 / 60;
const priv = self.private();
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
// Determine our delta time
const now = std.time.Instant.now() catch unreachable;
io.DeltaTime = if (priv.instant) |prev| delta: {
const since_ns = now.since(prev);
const since_s: f32 = @floatFromInt(since_ns / std.time.ns_per_s);
break :delta @max(0.00001, since_s);
} else default_delta_time;
priv.instant = now;
}
/// Handle key press/release events.
fn keyEvent(
self: *ImguiWidget,
action: input.Action,
ec_key: *gtk.EventControllerKey,
keyval: c_uint,
_: c_uint,
gtk_mods: gdk.ModifierType,
) bool {
self.queueRender();
self.setCurrentContext() catch return false;
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
const mods = key.translateMods(gtk_mods);
cimgui.c.ImGuiIO_AddKeyEvent(io, cimgui.c.ImGuiKey_LeftShift, mods.shift);
cimgui.c.ImGuiIO_AddKeyEvent(io, cimgui.c.ImGuiKey_LeftCtrl, mods.ctrl);
cimgui.c.ImGuiIO_AddKeyEvent(io, cimgui.c.ImGuiKey_LeftAlt, mods.alt);
cimgui.c.ImGuiIO_AddKeyEvent(io, cimgui.c.ImGuiKey_LeftSuper, mods.super);
// If our keyval has a key, then we send that key event
if (key.keyFromKeyval(keyval)) |inputkey| {
if (inputkey.imguiKey()) |imgui_key| {
cimgui.c.ImGuiIO_AddKeyEvent(io, imgui_key, action == .press);
}
}
// Try to process the event as text
if (ec_key.as(gtk.EventController).getCurrentEvent()) |event| {
const priv = self.private();
_ = priv.im_context.as(gtk.IMContext).filterKeypress(event);
}
return true;
}
/// Translate a GTK mouse button to a Dear ImGui mouse button.
fn translateMouseButton(button: c_uint) ?c_int {
return switch (button) {
1 => cimgui.c.ImGuiMouseButton_Left,
2 => cimgui.c.ImGuiMouseButton_Middle,
3 => cimgui.c.ImGuiMouseButton_Right,
else => null,
};
}
/// Get the scale factor that the display is operating at.
fn getScaleFactor(self: *Self) f64 {
const priv = self.private();
return @floatFromInt(priv.gl_area.as(gtk.Widget).getScaleFactor());
}
//---------------------------------------------------------------
// Properties
//---------------------------------------------------------------
// Signal Handlers
fn glAreaRealize(_: *gtk.GLArea, self: *Self) callconv(.c) void {
const priv = self.private();
priv.gl_area.makeCurrent();
if (priv.gl_area.getError()) |err| {
log.err("GLArea for Dear ImGui widget failed to realize: {s}", .{err.f_message orelse "(unknown)"});
return;
}
self.setCurrentContext() catch return;
// realize means that our OpenGL context is ready, so we can now
// initialize the ImgUI OpenGL backend for our context.
_ = cimgui.ImGui_ImplOpenGL3_Init(null);
priv.ig_gl_backend_initialized = true;
}
/// Handle a request to unrealize the GLArea
fn glAreaUnrealize(_: *gtk.GLArea, self: *ImguiWidget) callconv(.c) void {
self.setCurrentContext() catch return;
cimgui.ImGui_ImplOpenGL3_Shutdown();
}
/// Handle a request to resize the GLArea
fn glAreaResize(area: *gtk.GLArea, width: c_int, height: c_int, self: *Self) callconv(.c) void {
self.setCurrentContext() catch return;
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
const scale_factor = area.as(gtk.Widget).getScaleFactor();
// Our display size is always unscaled. We'll do the scaling in the
// style instead. This creates crisper looking fonts.
io.DisplaySize = .{ .x = @floatFromInt(width), .y = @floatFromInt(height) };
io.DisplayFramebufferScale = .{ .x = 1, .y = 1 };
// Setup a new style and scale it appropriately.
const style = cimgui.c.ImGuiStyle_ImGuiStyle();
defer cimgui.c.ImGuiStyle_destroy(style);
cimgui.c.ImGuiStyle_ScaleAllSizes(style, @floatFromInt(scale_factor));
const active_style = cimgui.c.igGetStyle();
active_style.* = style.*;
}
/// Handle a request to render the contents of our GLArea
fn glAreaRender(_: *gtk.GLArea, _: *gdk.GLContext, self: *Self) callconv(.c) c_int {
self.setCurrentContext() catch return @intFromBool(false);
// Setup our frame. We render twice because some ImGui behaviors
// take multiple renders to process. I don't know how to make this
// more efficient.
for (0..2) |_| {
cimgui.ImGui_ImplOpenGL3_NewFrame();
self.newFrame();
cimgui.c.igNewFrame();
// Use the callback to draw the UI.
const priv = self.private();
if (priv.render_callback) |cb| cb(priv.render_userdata);
// Render
cimgui.c.igRender();
}
// OpenGL final render
gl.clearColor(0x28 / 0xFF, 0x2C / 0xFF, 0x34 / 0xFF, 1.0);
gl.clear(gl.c.GL_COLOR_BUFFER_BIT);
cimgui.ImGui_ImplOpenGL3_RenderDrawData(cimgui.c.igGetDrawData());
return @intFromBool(true);
}
fn ecFocusEnter(_: *gtk.EventControllerFocus, self: *Self) callconv(.c) void {
self.queueRender();
self.setCurrentContext() catch return;
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
cimgui.c.ImGuiIO_AddFocusEvent(io, true);
}
fn ecFocusLeave(_: *gtk.EventControllerFocus, self: *Self) callconv(.c) void {
self.queueRender();
self.setCurrentContext() catch return;
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
cimgui.c.ImGuiIO_AddFocusEvent(io, false);
}
fn ecKeyPressed(
ec_key: *gtk.EventControllerKey,
keyval: c_uint,
keycode: c_uint,
gtk_mods: gdk.ModifierType,
self: *ImguiWidget,
) callconv(.c) c_int {
return @intFromBool(self.keyEvent(
.press,
ec_key,
keyval,
keycode,
gtk_mods,
));
}
fn ecKeyReleased(
ec_key: *gtk.EventControllerKey,
keyval: c_uint,
keycode: c_uint,
gtk_mods: gdk.ModifierType,
self: *ImguiWidget,
) callconv(.c) void {
_ = self.keyEvent(
.release,
ec_key,
keyval,
keycode,
gtk_mods,
);
}
fn ecMousePressed(
gesture: *gtk.GestureClick,
_: c_int,
_: f64,
_: f64,
self: *ImguiWidget,
) callconv(.c) void {
self.queueRender();
self.setCurrentContext() catch return;
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
const gdk_button = gesture.as(gtk.GestureSingle).getCurrentButton();
if (translateMouseButton(gdk_button)) |button| {
cimgui.c.ImGuiIO_AddMouseButtonEvent(io, button, true);
}
}
fn ecMouseReleased(
gesture: *gtk.GestureClick,
_: c_int,
_: f64,
_: f64,
self: *ImguiWidget,
) callconv(.c) void {
self.queueRender();
self.setCurrentContext() catch return;
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
const gdk_button = gesture.as(gtk.GestureSingle).getCurrentButton();
if (translateMouseButton(gdk_button)) |button| {
cimgui.c.ImGuiIO_AddMouseButtonEvent(io, button, false);
}
}
fn ecMouseMotion(
_: *gtk.EventControllerMotion,
x: f64,
y: f64,
self: *ImguiWidget,
) callconv(.c) void {
self.queueRender();
self.setCurrentContext() catch return;
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
const scale_factor = self.getScaleFactor();
cimgui.c.ImGuiIO_AddMousePosEvent(
io,
@floatCast(x * scale_factor),
@floatCast(y * scale_factor),
);
}
fn ecMouseScroll(
_: *gtk.EventControllerScroll,
x: f64,
y: f64,
self: *ImguiWidget,
) callconv(.c) c_int {
self.queueRender();
self.setCurrentContext() catch return @intFromBool(false);
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
cimgui.c.ImGuiIO_AddMouseWheelEvent(
io,
@floatCast(x),
@floatCast(-y),
);
return @intFromBool(true);
}
fn imCommit(
_: *gtk.IMMulticontext,
bytes: [*:0]u8,
self: *ImguiWidget,
) callconv(.c) void {
self.queueRender();
self.setCurrentContext() catch return;
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
cimgui.c.ImGuiIO_AddInputCharactersUTF8(io, bytes);
}
const C = Common(Self, Private);
pub const as = C.as;
pub const ref = C.ref;
pub const refSink = C.refSink;
pub const unref = C.unref;
const private = C.private;
pub const Class = extern struct {
parent_class: Parent.Class,
var parent: *Parent.Class = undefined;
pub const Instance = Self;
fn init(class: *Class) callconv(.c) void {
gtk.Widget.Class.setTemplateFromResource(
class.as(gtk.Widget.Class),
comptime gresource.blueprint(.{
.major = 1,
.minor = 5,
.name = "imgui-widget",
}),
);
// Bindings
class.bindTemplateChildPrivate("gl_area", .{});
class.bindTemplateChildPrivate("im_context", .{});
// Template Callbacks
class.bindTemplateCallback("realize", &glAreaRealize);
class.bindTemplateCallback("unrealize", &glAreaUnrealize);
class.bindTemplateCallback("resize", &glAreaResize);
class.bindTemplateCallback("render", &glAreaRender);
class.bindTemplateCallback("focus_enter", &ecFocusEnter);
class.bindTemplateCallback("focus_leave", &ecFocusLeave);
class.bindTemplateCallback("key_pressed", &ecKeyPressed);
class.bindTemplateCallback("key_released", &ecKeyReleased);
class.bindTemplateCallback("mouse_pressed", &ecMousePressed);
class.bindTemplateCallback("mouse_released", &ecMouseReleased);
class.bindTemplateCallback("mouse_motion", &ecMouseMotion);
class.bindTemplateCallback("scroll", &ecMouseScroll);
class.bindTemplateCallback("im_commit", &imCommit);
// Properties
// Signals
// Virtual methods
gobject.Object.virtual_methods.dispose.implement(class, &dispose);
gobject.Object.virtual_methods.finalize.implement(class, &finalize);
}
pub const as = C.Class.as;
pub const bindTemplateChildPrivate = C.Class.bindTemplateChildPrivate;
pub const bindTemplateCallback = C.Class.bindTemplateCallback;
};
};

View File

@ -0,0 +1,210 @@
const std = @import("std");
const adw = @import("adw");
const gobject = @import("gobject");
const gtk = @import("gtk");
const gresource = @import("../build/gresource.zig");
const Inspector = @import("../../../inspector/Inspector.zig");
const Common = @import("../class.zig").Common;
const WeakRef = @import("../weak_ref.zig").WeakRef;
const Surface = @import("surface.zig").Surface;
const ImguiWidget = @import("imgui_widget.zig").ImguiWidget;
const log = std.log.scoped(.gtk_ghostty_inspector_widget);
/// Widget for displaying the Ghostty inspector.
pub const InspectorWidget = extern struct {
const Self = @This();
parent_instance: Parent,
pub const Parent = adw.Bin;
pub const getGObjectType = gobject.ext.defineClass(Self, .{
.name = "GhosttyInspectorWidget",
.instanceInit = &init,
.classInit = &Class.init,
.parent_class = &Class.parent,
.private = .{ .Type = Private, .offset = &Private.offset },
});
pub const properties = struct {
pub const surface = struct {
pub const name = "surface";
const impl = gobject.ext.defineProperty(
name,
Self,
?*Surface,
.{
.accessor = gobject.ext.typedAccessor(Self, ?*Surface, .{
.getter = getSurface,
.getter_transfer = .full,
.setter = setSurface,
.setter_transfer = .none,
}),
},
);
};
};
pub const signals = struct {};
const Private = struct {
/// The surface that we are attached to
surface: WeakRef(Surface) = .empty,
/// The embedded Dear ImGui widget.
imgui_widget: *ImguiWidget,
pub var offset: c_int = 0;
};
//---------------------------------------------------------------
// Virtual Methods
fn init(self: *Self, _: *Class) callconv(.c) void {
gtk.Widget.initTemplate(self.as(gtk.Widget));
const priv = self.private();
priv.imgui_widget.setup(Inspector.setup);
priv.imgui_widget.setRenderCallback(imguiRender, self);
}
fn dispose(self: *Self) callconv(.c) void {
const priv = self.private();
deactivate: {
const surface = priv.surface.get() orelse break :deactivate;
defer surface.unref();
const core_surface = surface.core() orelse break :deactivate;
core_surface.deactivateInspector();
}
gtk.Widget.disposeTemplate(
self.as(gtk.Widget),
getGObjectType(),
);
gobject.Object.virtual_methods.dispose.call(
Class.parent,
self.as(Parent),
);
}
//---------------------------------------------------------------
// Public methods
pub fn new(surface: *Surface) *Self {
return gobject.ext.newInstance(Self, .{
.surface = surface,
});
}
/// Queue a render of the Dear ImGui widget.
pub fn queueRender(self: *Self) void {
const priv = self.private();
priv.imgui_widget.queueRender();
}
//---------------------------------------------------------------
// Private Methods
/// This is the callback from the embedded Dear ImGui widget that is called
/// to do the actual drawing.
fn imguiRender(ud: ?*anyopaque) void {
const self: *Self = @ptrCast(@alignCast(ud orelse return));
const priv = self.private();
const surface = priv.surface.get() orelse return;
defer surface.unref();
const core_surface = surface.core() orelse return;
const inspector = core_surface.inspector orelse return;
inspector.render();
}
//---------------------------------------------------------------
// Properties
fn getSurface(self: *Self) ?*Surface {
const priv = self.private();
return priv.surface.get();
}
fn setSurface(self: *Self, newvalue_: ?*Surface) void {
const priv = self.private();
if (priv.surface.get()) |oldvalue| oldvalue: {
defer oldvalue.unref();
// We don't need to do anything if we're just setting the same surface.
if (newvalue_) |newvalue| if (newvalue == oldvalue) return;
// Deactivate the inspector on the old surface.
const core_surface = oldvalue.core() orelse break :oldvalue;
core_surface.deactivateInspector();
}
const newvalue = newvalue_ orelse {
priv.surface.set(null);
return;
};
const core_surface = newvalue.core() orelse {
priv.surface.set(null);
return;
};
// Activate the inspector on the new surface.
core_surface.activateInspector() catch |err| {
log.err("failed to activate inspector err={}", .{err});
};
priv.surface.set(newvalue);
self.queueRender();
}
//---------------------------------------------------------------
// Signal Handlers
const C = Common(Self, Private);
pub const as = C.as;
pub const ref = C.ref;
pub const refSink = C.refSink;
pub const unref = C.unref;
const private = C.private;
pub const Class = extern struct {
parent_class: Parent.Class,
var parent: *Parent.Class = undefined;
pub const Instance = Self;
fn init(class: *Class) callconv(.c) void {
gobject.ext.ensureType(ImguiWidget);
gtk.Widget.Class.setTemplateFromResource(
class.as(gtk.Widget.Class),
comptime gresource.blueprint(.{
.major = 1,
.minor = 5,
.name = "inspector-widget",
}),
);
// Bindings
class.bindTemplateChildPrivate("imgui_widget", .{});
// Properties
gobject.ext.registerProperties(class, &.{
properties.surface.impl,
});
// Signals
// Virtual methods
gobject.Object.virtual_methods.dispose.implement(class, &dispose);
}
pub const as = C.Class.as;
pub const bindTemplateChildPrivate = C.Class.bindTemplateChildPrivate;
pub const bindTemplateCallback = C.Class.bindTemplateCallback;
};
};

View File

@ -0,0 +1,225 @@
const std = @import("std");
const build_config = @import("../../../build_config.zig");
const adw = @import("adw");
const gdk = @import("gdk");
const gobject = @import("gobject");
const gtk = @import("gtk");
const gresource = @import("../build/gresource.zig");
const key = @import("../key.zig");
const Common = @import("../class.zig").Common;
const Application = @import("application.zig").Application;
const Surface = @import("surface.zig").Surface;
const DebugWarning = @import("debug_warning.zig").DebugWarning;
const InspectorWidget = @import("inspector_widget.zig").InspectorWidget;
const WeakRef = @import("../weak_ref.zig").WeakRef;
const log = std.log.scoped(.gtk_ghostty_inspector_window);
/// Window for displaying the Ghostty inspector.
pub const InspectorWindow = extern struct {
const Self = @This();
parent_instance: Parent,
pub const Parent = adw.ApplicationWindow;
pub const getGObjectType = gobject.ext.defineClass(Self, .{
.name = "GhosttyInspectorWindow",
.instanceInit = &init,
.classInit = &Class.init,
.parent_class = &Class.parent,
.private = .{ .Type = Private, .offset = &Private.offset },
});
pub const properties = struct {
pub const surface = struct {
pub const name = "surface";
const impl = gobject.ext.defineProperty(
name,
Self,
?*Surface,
.{
.accessor = gobject.ext.typedAccessor(Self, ?*Surface, .{
.getter = getSurface,
.getter_transfer = .full,
.setter = setSurface,
.setter_transfer = .none,
}),
},
);
};
pub const debug = struct {
pub const name = "debug";
const impl = gobject.ext.defineProperty(
name,
Self,
bool,
.{
.default = build_config.is_debug,
.accessor = gobject.ext.typedAccessor(Self, bool, .{
.getter = struct {
pub fn getter(_: *Self) bool {
return build_config.is_debug;
}
}.getter,
}),
},
);
};
};
pub const signals = struct {};
const Private = struct {
/// The surface that we are attached to
surface: WeakRef(Surface) = .empty,
/// The embedded inspector widget.
inspector_widget: *InspectorWidget,
pub var offset: c_int = 0;
};
//---------------------------------------------------------------
// Virtual Methods
fn init(self: *Self, _: *Class) callconv(.c) void {
gtk.Widget.initTemplate(self.as(gtk.Widget));
// Add our dev CSS class if we're in debug mode.
if (comptime build_config.is_debug) {
self.as(gtk.Widget).addCssClass("devel");
}
// Set our window icon. We can't set this in the blueprint file
// because its dependent on the build config.
self.as(gtk.Window).setIconName(build_config.bundle_id);
}
fn dispose(self: *Self) callconv(.c) void {
gtk.Widget.disposeTemplate(
self.as(gtk.Widget),
getGObjectType(),
);
gobject.Object.virtual_methods.dispose.call(
Class.parent,
self.as(Parent),
);
}
//---------------------------------------------------------------
// Public methods
pub fn new(surface: *Surface) *Self {
const self = gobject.ext.newInstance(Self, .{
.surface = surface,
});
// Bump the ref so that we aren't immediately closed.
return self.ref();
}
/// Present the window.
pub fn present(self: *Self) void {
self.as(gtk.Window).present();
}
/// Queue a render of the embedded widget.
pub fn queueRender(self: *Self) void {
const priv = self.private();
priv.inspector_widget.queueRender();
}
/// The surface we are connected to is going away, shut ourselves down.
pub fn shutdown(self: *Self) void {
const priv = self.private();
priv.surface.set(null);
self.as(gobject.Object).notifyByPspec(properties.surface.impl.param_spec);
self.as(gtk.Window).close();
}
//---------------------------------------------------------------
// Private Methods
fn isFullscreen(self: *Self) bool {
return self.as(gtk.Window).isFullscreen() != 0;
}
fn isMaximized(self: *Self) bool {
return self.as(gtk.Window).isMaximized() != 0;
}
//---------------------------------------------------------------
// Properties
fn getSurface(self: *Self) ?*Surface {
const priv = self.private();
return priv.surface.get();
}
fn setSurface(self: *Self, newvalue: ?*Surface) void {
const priv = self.private();
priv.surface.set(newvalue);
}
//---------------------------------------------------------------
// Signal Handlers
/// The user has clicked on the close button.
fn closeRequest(_: *gtk.Window, self: *Self) callconv(.c) c_int {
const priv = self.private();
priv.surface.set(null);
self.as(gobject.Object).notifyByPspec(properties.surface.impl.param_spec);
self.as(gtk.Window).destroy();
return @intFromBool(false);
}
const C = Common(Self, Private);
pub const as = C.as;
pub const ref = C.ref;
pub const refSink = C.refSink;
pub const unref = C.unref;
const private = C.private;
pub const Class = extern struct {
parent_class: Parent.Class,
var parent: *Parent.Class = undefined;
pub const Instance = Self;
fn init(class: *Class) callconv(.c) void {
gobject.ext.ensureType(DebugWarning);
gobject.ext.ensureType(InspectorWidget);
gtk.Widget.Class.setTemplateFromResource(
class.as(gtk.Widget.Class),
comptime gresource.blueprint(.{
.major = 1,
.minor = 5,
.name = "inspector-window",
}),
);
// Template Bindings
class.bindTemplateChildPrivate("inspector_widget", .{});
// Template Callbacks
class.bindTemplateCallback("close_request", &closeRequest);
// Properties
gobject.ext.registerProperties(class, &.{
properties.surface.impl,
properties.debug.impl,
});
// Signals
// Virtual methods
gobject.Object.virtual_methods.dispose.implement(class, &dispose);
}
pub const as = C.Class.as;
pub const bindTemplateChildPrivate = C.Class.bindTemplateChildPrivate;
pub const bindTemplateCallback = C.Class.bindTemplateCallback;
};
};

View File

@ -29,6 +29,8 @@ const ChildExited = @import("surface_child_exited.zig").SurfaceChildExited;
const ClipboardConfirmationDialog = @import("clipboard_confirmation_dialog.zig").ClipboardConfirmationDialog;
const TitleDialog = @import("surface_title_dialog.zig").SurfaceTitleDialog;
const Window = @import("window.zig").Window;
const WeakRef = @import("../weak_ref.zig").WeakRef;
const InspectorWindow = @import("inspector_window.zig").InspectorWindow;
const log = std.log.scoped(.gtk_ghostty_surface);
@ -470,6 +472,9 @@ pub const Surface = extern struct {
// false by a parent widget.
bell_ringing: bool = false,
/// A weak reference to an inspector window.
inspector: WeakRef(InspectorWindow) = .empty,
// Template binds
child_exited_overlay: *ChildExited,
context_menu: *gtk.PopoverMenu,
@ -573,6 +578,58 @@ pub const Surface = extern struct {
return self.as(gtk.Widget).activateAction("win.toggle-command-palette", null) != 0;
}
pub fn toggleInspector(self: *Self) bool {
const priv = self.private();
if (priv.inspector.get()) |inspector| {
defer inspector.unref();
inspector.shutdown();
priv.inspector.set(null);
return true;
}
const inspector = InspectorWindow.new(self);
defer inspector.unref();
priv.inspector.set(inspector);
inspector.present();
return true;
}
pub fn showInspector(self: *Self) bool {
const priv = self.private();
const inspector = priv.inspector.get() orelse inspector: {
const inspector = InspectorWindow.new(self);
priv.inspector.set(inspector);
break :inspector inspector;
};
defer inspector.unref();
inspector.present();
return true;
}
pub fn hideInspector(self: *Self) bool {
const priv = self.private();
if (priv.inspector.get()) |inspector| {
defer inspector.unref();
inspector.shutdown();
priv.inspector.set(null);
}
return true;
}
pub fn controlInspector(self: *Self, value: apprt.Action.Value(.inspector)) bool {
switch (value) {
.toggle => return self.toggleInspector(),
.show => return self.showInspector(),
.hide => return self.hideInspector(),
}
}
/// Redraw our inspector, if there is one associated with this surface.
pub fn redrawInspector(self: *Self) void {
const priv = self.private();
const inspector = priv.inspector.get() orelse return;
defer inspector.unref();
inspector.queueRender();
}
pub fn showOnScreenKeyboard(self: *Self, event: ?*gdk.Event) bool {
const priv = self.private();
return priv.im_context.as(gtk.IMContext).activateOsk(event) != 0;
@ -1287,10 +1344,12 @@ pub const Surface = extern struct {
fn dispose(self: *Self) callconv(.c) void {
const priv = self.private();
if (priv.config) |v| {
v.unref();
priv.config = null;
}
if (priv.progress_bar_timer) |timer| {
if (glib.Source.remove(timer) == 0) {
log.warn("unable to remove progress bar timer", .{});
@ -1298,6 +1357,11 @@ pub const Surface = extern struct {
priv.progress_bar_timer = null;
}
if (priv.inspector.get()) |inspector| {
defer inspector.unref();
inspector.shutdown();
}
gtk.Widget.disposeTemplate(
self.as(gtk.Widget),
getGObjectType(),

View File

@ -28,6 +28,7 @@ const Surface = @import("surface.zig").Surface;
const Tab = @import("tab.zig").Tab;
const DebugWarning = @import("debug_warning.zig").DebugWarning;
const CommandPalette = @import("command_palette.zig").CommandPalette;
const InspectorWindow = @import("inspector_window.zig").InspectorWindow;
const WeakRef = @import("../weak_ref.zig").WeakRef;
const log = std.log.scoped(.gtk_ghostty_window);
@ -347,6 +348,7 @@ pub const Window = extern struct {
.{ "clear", actionClear, null },
// TODO: accept the surface that toggled the command palette
.{ "toggle-command-palette", actionToggleCommandPalette, null },
.{ "toggle-inspector", actionToggleInspector, null },
};
const action_map = self.as(gio.ActionMap);
@ -1820,6 +1822,23 @@ pub const Window = extern struct {
self.toggleCommandPalette();
}
/// Toggle the Ghostty inspector for the active surface.
fn toggleInspector(self: *Self) void {
const surface = self.getActiveSurface() orelse return;
_ = surface.toggleInspector();
}
/// React to a GTK action requesting that the Ghostty inspector be toggled.
fn actionToggleInspector(
_: *gio.SimpleAction,
_: ?*glib.Variant,
self: *Window,
) callconv(.c) void {
// TODO: accept the surface that toggled the command palette as a
// parameter
self.toggleInspector();
}
const C = Common(Self, Private);
pub const as = C.as;
pub const ref = C.ref;

View File

@ -0,0 +1,51 @@
using Gtk 4.0;
using Adw 1;
template $GhosttyImguiWidget: Adw.Bin {
styles [
"imgui",
]
Adw.Bin {
Gtk.GLArea gl_area {
auto-render: true;
// needs to be focusable so that we can receive events
focusable: true;
focus-on-click: true;
allowed-apis: gl;
realize => $realize();
unrealize => $unrealize();
resize => $resize();
render => $render();
EventControllerFocus {
enter => $focus_enter();
leave => $focus_leave();
}
EventControllerKey {
key-pressed => $key_pressed();
key-released => $key_released();
}
GestureClick {
pressed => $mouse_pressed();
released => $mouse_released();
button: 0;
}
EventControllerMotion {
motion => $mouse_motion();
}
EventControllerScroll {
scroll => $scroll();
flags: both_axes;
}
}
}
}
IMMulticontext im_context {
commit => $im_commit();
}

View File

@ -0,0 +1,15 @@
using Gtk 4.0;
using Adw 1;
template $GhosttyInspectorWidget: Adw.Bin {
styles [
"inspector",
]
hexpand: true;
vexpand: true;
Adw.Bin {
$GhosttyImguiWidget imgui_widget {}
}
}

View File

@ -0,0 +1,38 @@
using Gtk 4.0;
using Adw 1;
template $GhosttyInspectorWindow: Adw.ApplicationWindow {
title: _("Ghostty: Terminal Inspector");
icon-name: "com.mitchellh.ghostty";
default-width: 1000;
default-height: 600;
close-request => $close_request();
styles [
"inspector",
]
content: Adw.ToolbarView {
[top]
Adw.HeaderBar {
title-widget: Adw.WindowTitle {
title: bind template.title;
};
}
Gtk.Box {
orientation: vertical;
spacing: 0;
hexpand: true;
vexpand: true;
$GhosttyDebugWarning {
visible: bind template.debug;
}
$GhosttyInspectorWidget inspector_widget {
surface: bind template.surface;
}
}
};
}