mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 08:46:08 +03:00
1
.gitattributes
vendored
1
.gitattributes
vendored
@ -1,2 +1,3 @@
|
||||
vendor/** linguist-vendored
|
||||
website/** linguist-documentation
|
||||
pkg/cimgui/vendor/** linguist-vendored
|
||||
|
@ -621,6 +621,7 @@ fn addDeps(
|
||||
};
|
||||
|
||||
// Dependencies
|
||||
const cimgui_dep = b.dependency("cimgui", .{ .target = step.target, .optimize = step.optimize });
|
||||
const js_dep = b.dependency("zig_js", .{ .target = step.target, .optimize = step.optimize });
|
||||
const libxev_dep = b.dependency("libxev", .{ .target = step.target, .optimize = step.optimize });
|
||||
const objc_dep = b.dependency("zig_objc", .{ .target = step.target, .optimize = step.optimize });
|
||||
@ -724,6 +725,11 @@ fn addDeps(
|
||||
try static_libs.append(macos_dep.artifact("macos").getEmittedBin());
|
||||
}
|
||||
|
||||
// cimgui
|
||||
step.addModule("cimgui", cimgui_dep.module("cimgui"));
|
||||
step.linkLibrary(cimgui_dep.artifact("cimgui"));
|
||||
try static_libs.append(cimgui_dep.artifact("cimgui").getEmittedBin());
|
||||
|
||||
// Tracy
|
||||
step.addModule("tracy", tracy_dep.module("tracy"));
|
||||
if (tracy) {
|
||||
|
@ -27,6 +27,7 @@
|
||||
},
|
||||
|
||||
// C libs
|
||||
.cimgui = .{ .path = "./pkg/cimgui" },
|
||||
.fontconfig = .{ .path = "./pkg/fontconfig" },
|
||||
.freetype = .{ .path = "./pkg/freetype" },
|
||||
.harfbuzz = .{ .path = "./pkg/harfbuzz" },
|
||||
|
@ -27,6 +27,7 @@ extern "C" {
|
||||
typedef void *ghostty_app_t;
|
||||
typedef void *ghostty_config_t;
|
||||
typedef void *ghostty_surface_t;
|
||||
typedef void *ghostty_inspector_t;
|
||||
|
||||
// Enums are up top so we can reference them later.
|
||||
typedef enum {
|
||||
@ -48,6 +49,12 @@ typedef enum {
|
||||
GHOSTTY_SPLIT_FOCUS_RIGHT,
|
||||
} ghostty_split_focus_direction_e;
|
||||
|
||||
typedef enum {
|
||||
GHOSTTY_INSPECTOR_TOGGLE,
|
||||
GHOSTTY_INSPECTOR_SHOW,
|
||||
GHOSTTY_INSPECTOR_HIDE,
|
||||
} ghostty_inspector_mode_e;
|
||||
|
||||
typedef enum {
|
||||
GHOSTTY_MOUSE_RELEASE,
|
||||
GHOSTTY_MOUSE_PRESS,
|
||||
@ -322,12 +329,14 @@ typedef void (*ghostty_runtime_write_clipboard_cb)(void *, const char *, ghostty
|
||||
typedef void (*ghostty_runtime_new_split_cb)(void *, ghostty_split_direction_e, ghostty_surface_config_s);
|
||||
typedef void (*ghostty_runtime_new_tab_cb)(void *, ghostty_surface_config_s);
|
||||
typedef void (*ghostty_runtime_new_window_cb)(void *, ghostty_surface_config_s);
|
||||
typedef void (*ghostty_runtime_control_inspector_cb)(void *, ghostty_inspector_mode_e);
|
||||
typedef void (*ghostty_runtime_close_surface_cb)(void *, bool);
|
||||
typedef void (*ghostty_runtime_focus_split_cb)(void *, ghostty_split_focus_direction_e);
|
||||
typedef void (*ghostty_runtime_toggle_split_zoom_cb)(void *);
|
||||
typedef void (*ghostty_runtime_goto_tab_cb)(void *, int32_t);
|
||||
typedef void (*ghostty_runtime_toggle_fullscreen_cb)(void *, ghostty_non_native_fullscreen_e);
|
||||
typedef void (*ghostty_runtime_set_initial_window_size_cb)(void *, uint32_t, uint32_t);
|
||||
typedef void (*ghostty_runtime_render_inspector_cb)(void *);
|
||||
|
||||
typedef struct {
|
||||
void *userdata;
|
||||
@ -342,12 +351,14 @@ typedef struct {
|
||||
ghostty_runtime_new_split_cb new_split_cb;
|
||||
ghostty_runtime_new_tab_cb new_tab_cb;
|
||||
ghostty_runtime_new_window_cb new_window_cb;
|
||||
ghostty_runtime_control_inspector_cb control_inspector_cb;
|
||||
ghostty_runtime_close_surface_cb close_surface_cb;
|
||||
ghostty_runtime_focus_split_cb focus_split_cb;
|
||||
ghostty_runtime_toggle_split_zoom_cb toggle_split_zoom_cb;
|
||||
ghostty_runtime_goto_tab_cb goto_tab_cb;
|
||||
ghostty_runtime_toggle_fullscreen_cb toggle_fullscreen_cb;
|
||||
ghostty_runtime_set_initial_window_size_cb set_initial_window_size_cb;
|
||||
ghostty_runtime_render_inspector_cb render_inspector_cb;
|
||||
} ghostty_runtime_config_s;
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
@ -399,6 +410,20 @@ void ghostty_surface_split_focus(ghostty_surface_t, ghostty_split_focus_directio
|
||||
bool ghostty_surface_binding_action(ghostty_surface_t, const char *, uintptr_t);
|
||||
void ghostty_surface_complete_clipboard_request(ghostty_surface_t, const char *, uintptr_t, void *);
|
||||
|
||||
ghostty_inspector_t ghostty_surface_inspector(ghostty_surface_t);
|
||||
void ghostty_inspector_free(ghostty_surface_t);
|
||||
bool ghostty_inspector_metal_init(ghostty_inspector_t, void *);
|
||||
void ghostty_inspector_metal_render(ghostty_inspector_t, void *, void *);
|
||||
bool ghostty_inspector_metal_shutdown(ghostty_inspector_t);
|
||||
void ghostty_inspector_set_focus(ghostty_inspector_t, bool);
|
||||
void ghostty_inspector_set_content_scale(ghostty_inspector_t, double, double);
|
||||
void ghostty_inspector_set_size(ghostty_inspector_t, uint32_t, uint32_t);
|
||||
void ghostty_inspector_mouse_button(ghostty_inspector_t, ghostty_input_mouse_state_e, ghostty_input_mouse_button_e, ghostty_input_mods_e);
|
||||
void ghostty_inspector_mouse_pos(ghostty_inspector_t, double, double);
|
||||
void ghostty_inspector_mouse_scroll(ghostty_inspector_t, double, double, ghostty_input_scroll_mods_t);
|
||||
void ghostty_inspector_key(ghostty_inspector_t, ghostty_input_action_e, ghostty_input_key_e, ghostty_input_mods_e);
|
||||
void ghostty_inspector_text(ghostty_inspector_t, const char *);
|
||||
|
||||
// APIs I'd like to get rid of eventually but are still needed for now.
|
||||
// Don't use these unless you know what you're doing.
|
||||
void ghostty_set_window_background_blur(ghostty_surface_t, void *);
|
||||
|
@ -26,6 +26,8 @@
|
||||
A56D58892ACDE6CA00508D2C /* ServiceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A56D58882ACDE6CA00508D2C /* ServiceProvider.swift */; };
|
||||
A571AB1D2A206FCF00248498 /* GhosttyKit.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A5D495A1299BEC7E00DD1313 /* GhosttyKit.xcframework */; };
|
||||
A59444F729A2ED5200725BBA /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59444F629A2ED5200725BBA /* SettingsView.swift */; };
|
||||
A59FB5CF2AE0DB50009128F3 /* InspectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59FB5CE2AE0DB50009128F3 /* InspectorView.swift */; };
|
||||
A59FB5D12AE0DEA7009128F3 /* MetalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59FB5D02AE0DEA7009128F3 /* MetalView.swift */; };
|
||||
A5A1F8852A489D6800D1E8BC /* terminfo in Resources */ = {isa = PBXBuildFile; fileRef = A5A1F8842A489D6800D1E8BC /* terminfo */; };
|
||||
A5B30539299BEAAB0047F10C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5B30538299BEAAB0047F10C /* Assets.xcassets */; };
|
||||
A5CDF1912AAF9A5800513312 /* ConfigurationErrors.xib in Resources */ = {isa = PBXBuildFile; fileRef = A5CDF1902AAF9A5800513312 /* ConfigurationErrors.xib */; };
|
||||
@ -59,6 +61,8 @@
|
||||
A56D58882ACDE6CA00508D2C /* ServiceProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceProvider.swift; sourceTree = "<group>"; };
|
||||
A571AB1C2A206FC600248498 /* Ghostty-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = "Ghostty-Info.plist"; sourceTree = "<group>"; };
|
||||
A59444F629A2ED5200725BBA /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
|
||||
A59FB5CE2AE0DB50009128F3 /* InspectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InspectorView.swift; sourceTree = "<group>"; };
|
||||
A59FB5D02AE0DEA7009128F3 /* MetalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetalView.swift; sourceTree = "<group>"; };
|
||||
A5A1F8842A489D6800D1E8BC /* terminfo */ = {isa = PBXFileReference; lastKnownFileType = folder; name = terminfo; path = "../zig-out/share/terminfo"; sourceTree = "<group>"; };
|
||||
A5B30531299BEAAA0047F10C /* Ghostty.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Ghostty.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
A5B30538299BEAAB0047F10C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
@ -115,6 +119,7 @@
|
||||
children = (
|
||||
A5CEAFFE29C2410700646FDA /* Backport.swift */,
|
||||
8503D7C62A549C66006CFF3D /* FullScreenHandler.swift */,
|
||||
A59FB5D02AE0DEA7009128F3 /* MetalView.swift */,
|
||||
A5FECBD829D2010400022361 /* WindowAccessor.swift */,
|
||||
A5CEAFDA29B8005900646FDA /* SplitView */,
|
||||
);
|
||||
@ -151,6 +156,7 @@
|
||||
A55B7BB729B6F53A0055DE60 /* Package.swift */,
|
||||
A55B7BB529B6F47F0055DE60 /* AppState.swift */,
|
||||
A55B7BBB29B6FC330055DE60 /* SurfaceView.swift */,
|
||||
A59FB5CE2AE0DB50009128F3 /* InspectorView.swift */,
|
||||
A5278A9A2AA05B2600CD3039 /* Ghostty.Input.swift */,
|
||||
A56D58852ACDDB4100508D2C /* Ghostty.Shell.swift */,
|
||||
A55B7BBD29B701360055DE60 /* Ghostty.SplitView.swift */,
|
||||
@ -288,6 +294,7 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
A59FB5CF2AE0DB50009128F3 /* InspectorView.swift in Sources */,
|
||||
A53426392A7DC55C00EBB7A2 /* PrimaryWindowManager.swift in Sources */,
|
||||
85DE1C922A6A3DCA00493853 /* PrimaryWindow.swift in Sources */,
|
||||
A56D58892ACDE6CA00508D2C /* ServiceProvider.swift in Sources */,
|
||||
@ -304,6 +311,7 @@
|
||||
A55B7BB629B6F47F0055DE60 /* AppState.swift in Sources */,
|
||||
A5CEAFDC29B8009000646FDA /* SplitView.swift in Sources */,
|
||||
A5CDF1932AAF9E0800513312 /* ConfigurationErrorsController.swift in Sources */,
|
||||
A59FB5D12AE0DEA7009128F3 /* MetalView.swift in Sources */,
|
||||
A55685E029A03A9F004303CE /* AppError.swift in Sources */,
|
||||
A5FECBD929D2010400022361 /* WindowAccessor.swift in Sources */,
|
||||
A535B9DA299C569B0017E2E4 /* ErrorView.swift in Sources */,
|
||||
|
@ -36,6 +36,7 @@ class AppDelegate: NSObject, ObservableObject, NSApplicationDelegate, GhosttyApp
|
||||
@IBOutlet private var menuIncreaseFontSize: NSMenuItem?
|
||||
@IBOutlet private var menuDecreaseFontSize: NSMenuItem?
|
||||
@IBOutlet private var menuResetFontSize: NSMenuItem?
|
||||
@IBOutlet private var menuTerminalInspector: NSMenuItem?
|
||||
|
||||
/// The dock menu
|
||||
private var dockMenu: NSMenu = NSMenu()
|
||||
@ -216,6 +217,7 @@ class AppDelegate: NSObject, ObservableObject, NSApplicationDelegate, GhosttyApp
|
||||
syncMenuShortcut(action: "increase_font_size:1", menuItem: self.menuIncreaseFontSize)
|
||||
syncMenuShortcut(action: "decrease_font_size:1", menuItem: self.menuDecreaseFontSize)
|
||||
syncMenuShortcut(action: "reset_font_size", menuItem: self.menuResetFontSize)
|
||||
syncMenuShortcut(action: "inspector:toggle", menuItem: self.menuTerminalInspector)
|
||||
|
||||
// Dock menu
|
||||
reloadDockMenu()
|
||||
@ -394,4 +396,9 @@ class AppDelegate: NSObject, ObservableObject, NSApplicationDelegate, GhosttyApp
|
||||
guard let surface = focusedSurface() else { return }
|
||||
ghostty.changeFontSize(surface: surface, .reset)
|
||||
}
|
||||
|
||||
@IBAction func toggleTerminalInspector(_ sender: Any) {
|
||||
guard let surface = focusedSurface() else { return }
|
||||
ghostty.toggleTerminalInspector(surface: surface)
|
||||
}
|
||||
}
|
||||
|
@ -136,12 +136,14 @@ extension Ghostty {
|
||||
new_split_cb: { userdata, direction, surfaceConfig in AppState.newSplit(userdata, direction: direction, config: surfaceConfig) },
|
||||
new_tab_cb: { userdata, surfaceConfig in AppState.newTab(userdata, config: surfaceConfig) },
|
||||
new_window_cb: { userdata, surfaceConfig in AppState.newWindow(userdata, config: surfaceConfig) },
|
||||
control_inspector_cb: { userdata, mode in AppState.controlInspector(userdata, mode: mode) },
|
||||
close_surface_cb: { userdata, processAlive in AppState.closeSurface(userdata, processAlive: processAlive) },
|
||||
focus_split_cb: { userdata, direction in AppState.focusSplit(userdata, direction: direction) },
|
||||
toggle_split_zoom_cb: { userdata in AppState.toggleSplitZoom(userdata) },
|
||||
goto_tab_cb: { userdata, n in AppState.gotoTab(userdata, n: n) },
|
||||
toggle_fullscreen_cb: { userdata, nonNativeFullscreen in AppState.toggleFullscreen(userdata, nonNativeFullscreen: nonNativeFullscreen) },
|
||||
set_initial_window_size_cb: { userdata, width, height in AppState.setInitialWindowSize(userdata, width: width, height: height) }
|
||||
set_initial_window_size_cb: { userdata, width, height in AppState.setInitialWindowSize(userdata, width: width, height: height) },
|
||||
render_inspector_cb: { userdata in AppState.renderInspector(userdata) }
|
||||
)
|
||||
|
||||
// Create the ghostty app.
|
||||
@ -299,6 +301,13 @@ extension Ghostty {
|
||||
}
|
||||
}
|
||||
|
||||
func toggleTerminalInspector(surface: ghostty_surface_t) {
|
||||
let action = "inspector:toggle"
|
||||
if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) {
|
||||
AppDelegate.logger.warning("action failed action=\(action)")
|
||||
}
|
||||
}
|
||||
|
||||
// Called when the selected keyboard changes. We have to notify Ghostty so that
|
||||
// it can reload the keyboard mapping for input.
|
||||
@objc private func keyboardSelectionDidChange(notification: NSNotification) {
|
||||
@ -416,6 +425,14 @@ extension Ghostty {
|
||||
DispatchQueue.main.async { state.appTick() }
|
||||
}
|
||||
|
||||
static func renderInspector(_ userdata: UnsafeMutableRawPointer?) {
|
||||
guard let surface = self.surfaceUserdata(from: userdata) else { return }
|
||||
NotificationCenter.default.post(
|
||||
name: Notification.inspectorNeedsDisplay,
|
||||
object: surface
|
||||
)
|
||||
}
|
||||
|
||||
static func setTitle(_ userdata: UnsafeMutableRawPointer?, title: UnsafePointer<CChar>?) {
|
||||
let surfaceView = Unmanaged<SurfaceView>.fromOpaque(userdata!).takeUnretainedValue()
|
||||
guard let titleStr = String(cString: title!, encoding: .utf8) else { return }
|
||||
@ -486,6 +503,13 @@ extension Ghostty {
|
||||
)
|
||||
}
|
||||
|
||||
static func controlInspector(_ userdata: UnsafeMutableRawPointer?, mode: ghostty_inspector_mode_e) {
|
||||
guard let surface = self.surfaceUserdata(from: userdata) else { return }
|
||||
NotificationCenter.default.post(name: Notification.didControlInspector, object: surface, userInfo: [
|
||||
"mode": mode,
|
||||
])
|
||||
}
|
||||
|
||||
/// Returns the GhosttyState from the given userdata value.
|
||||
static private func appState(fromView view: SurfaceView) -> AppState? {
|
||||
guard let surface = view.surface else { return nil }
|
||||
|
@ -32,6 +32,27 @@ extension Ghostty {
|
||||
return flags
|
||||
}
|
||||
|
||||
/// Translate event modifier flags to a ghostty mods enum.
|
||||
static func ghosttyMods(_ flags: NSEvent.ModifierFlags) -> ghostty_input_mods_e {
|
||||
var mods: UInt32 = GHOSTTY_MODS_NONE.rawValue
|
||||
|
||||
if (flags.contains(.shift)) { mods |= GHOSTTY_MODS_SHIFT.rawValue }
|
||||
if (flags.contains(.control)) { mods |= GHOSTTY_MODS_CTRL.rawValue }
|
||||
if (flags.contains(.option)) { mods |= GHOSTTY_MODS_ALT.rawValue }
|
||||
if (flags.contains(.command)) { mods |= GHOSTTY_MODS_SUPER.rawValue }
|
||||
if (flags.contains(.capsLock)) { mods |= GHOSTTY_MODS_CAPS.rawValue }
|
||||
|
||||
// Handle sided input. We can't tell that both are pressed in the
|
||||
// Ghostty structure but thats okay -- we don't use that information.
|
||||
let rawFlags = flags.rawValue
|
||||
if (rawFlags & UInt(NX_DEVICERSHIFTKEYMASK) != 0) { mods |= GHOSTTY_MODS_SHIFT_RIGHT.rawValue }
|
||||
if (rawFlags & UInt(NX_DEVICERCTLKEYMASK) != 0) { mods |= GHOSTTY_MODS_CTRL_RIGHT.rawValue }
|
||||
if (rawFlags & UInt(NX_DEVICERALTKEYMASK) != 0) { mods |= GHOSTTY_MODS_ALT_RIGHT.rawValue }
|
||||
if (rawFlags & UInt(NX_DEVICERCMDKEYMASK) != 0) { mods |= GHOSTTY_MODS_SUPER_RIGHT.rawValue }
|
||||
|
||||
return ghostty_input_mods_e(mods)
|
||||
}
|
||||
|
||||
/// A map from the Ghostty key enum to the keyEquivalent string for shortcuts.
|
||||
static let keyToEquivalent: [ghostty_input_key_e : String] = [
|
||||
// 0-9
|
||||
@ -214,5 +235,122 @@ extension Ghostty {
|
||||
0x3B: GHOSTTY_KEY_SEMICOLON,
|
||||
0x2F: GHOSTTY_KEY_SLASH,
|
||||
]
|
||||
|
||||
// Mapping of event keyCode to ghostty input key values. This is cribbed from
|
||||
// glfw mostly since we started as a glfw-based app way back in the day!
|
||||
static let keycodeToKey: [UInt16 : ghostty_input_key_e] = [
|
||||
0x1D: GHOSTTY_KEY_ZERO,
|
||||
0x12: GHOSTTY_KEY_ONE,
|
||||
0x13: GHOSTTY_KEY_TWO,
|
||||
0x14: GHOSTTY_KEY_THREE,
|
||||
0x15: GHOSTTY_KEY_FOUR,
|
||||
0x17: GHOSTTY_KEY_FIVE,
|
||||
0x16: GHOSTTY_KEY_SIX,
|
||||
0x1A: GHOSTTY_KEY_SEVEN,
|
||||
0x1C: GHOSTTY_KEY_EIGHT,
|
||||
0x19: GHOSTTY_KEY_NINE,
|
||||
0x00: GHOSTTY_KEY_A,
|
||||
0x0B: GHOSTTY_KEY_B,
|
||||
0x08: GHOSTTY_KEY_C,
|
||||
0x02: GHOSTTY_KEY_D,
|
||||
0x0E: GHOSTTY_KEY_E,
|
||||
0x03: GHOSTTY_KEY_F,
|
||||
0x05: GHOSTTY_KEY_G,
|
||||
0x04: GHOSTTY_KEY_H,
|
||||
0x22: GHOSTTY_KEY_I,
|
||||
0x26: GHOSTTY_KEY_J,
|
||||
0x28: GHOSTTY_KEY_K,
|
||||
0x25: GHOSTTY_KEY_L,
|
||||
0x2E: GHOSTTY_KEY_M,
|
||||
0x2D: GHOSTTY_KEY_N,
|
||||
0x1F: GHOSTTY_KEY_O,
|
||||
0x23: GHOSTTY_KEY_P,
|
||||
0x0C: GHOSTTY_KEY_Q,
|
||||
0x0F: GHOSTTY_KEY_R,
|
||||
0x01: GHOSTTY_KEY_S,
|
||||
0x11: GHOSTTY_KEY_T,
|
||||
0x20: GHOSTTY_KEY_U,
|
||||
0x09: GHOSTTY_KEY_V,
|
||||
0x0D: GHOSTTY_KEY_W,
|
||||
0x07: GHOSTTY_KEY_X,
|
||||
0x10: GHOSTTY_KEY_Y,
|
||||
0x06: GHOSTTY_KEY_Z,
|
||||
|
||||
0x27: GHOSTTY_KEY_APOSTROPHE,
|
||||
0x2A: GHOSTTY_KEY_BACKSLASH,
|
||||
0x2B: GHOSTTY_KEY_COMMA,
|
||||
0x18: GHOSTTY_KEY_EQUAL,
|
||||
0x32: GHOSTTY_KEY_GRAVE_ACCENT,
|
||||
0x21: GHOSTTY_KEY_LEFT_BRACKET,
|
||||
0x1B: GHOSTTY_KEY_MINUS,
|
||||
0x2F: GHOSTTY_KEY_PERIOD,
|
||||
0x1E: GHOSTTY_KEY_RIGHT_BRACKET,
|
||||
0x29: GHOSTTY_KEY_SEMICOLON,
|
||||
0x2C: GHOSTTY_KEY_SLASH,
|
||||
|
||||
0x33: GHOSTTY_KEY_BACKSPACE,
|
||||
0x39: GHOSTTY_KEY_CAPS_LOCK,
|
||||
0x75: GHOSTTY_KEY_DELETE,
|
||||
0x7D: GHOSTTY_KEY_DOWN,
|
||||
0x77: GHOSTTY_KEY_END,
|
||||
0x24: GHOSTTY_KEY_ENTER,
|
||||
0x35: GHOSTTY_KEY_ESCAPE,
|
||||
0x7A: GHOSTTY_KEY_F1,
|
||||
0x78: GHOSTTY_KEY_F2,
|
||||
0x63: GHOSTTY_KEY_F3,
|
||||
0x76: GHOSTTY_KEY_F4,
|
||||
0x60: GHOSTTY_KEY_F5,
|
||||
0x61: GHOSTTY_KEY_F6,
|
||||
0x62: GHOSTTY_KEY_F7,
|
||||
0x64: GHOSTTY_KEY_F8,
|
||||
0x65: GHOSTTY_KEY_F9,
|
||||
0x6D: GHOSTTY_KEY_F10,
|
||||
0x67: GHOSTTY_KEY_F11,
|
||||
0x6F: GHOSTTY_KEY_F12,
|
||||
0x69: GHOSTTY_KEY_PRINT_SCREEN,
|
||||
0x6B: GHOSTTY_KEY_F14,
|
||||
0x71: GHOSTTY_KEY_F15,
|
||||
0x6A: GHOSTTY_KEY_F16,
|
||||
0x40: GHOSTTY_KEY_F17,
|
||||
0x4F: GHOSTTY_KEY_F18,
|
||||
0x50: GHOSTTY_KEY_F19,
|
||||
0x5A: GHOSTTY_KEY_F20,
|
||||
0x73: GHOSTTY_KEY_HOME,
|
||||
0x72: GHOSTTY_KEY_INSERT,
|
||||
0x7B: GHOSTTY_KEY_LEFT,
|
||||
0x3A: GHOSTTY_KEY_LEFT_ALT,
|
||||
0x3B: GHOSTTY_KEY_LEFT_CONTROL,
|
||||
0x38: GHOSTTY_KEY_LEFT_SHIFT,
|
||||
0x37: GHOSTTY_KEY_LEFT_SUPER,
|
||||
0x47: GHOSTTY_KEY_NUM_LOCK,
|
||||
0x79: GHOSTTY_KEY_PAGE_DOWN,
|
||||
0x74: GHOSTTY_KEY_PAGE_UP,
|
||||
0x7C: GHOSTTY_KEY_RIGHT,
|
||||
0x3D: GHOSTTY_KEY_RIGHT_ALT,
|
||||
0x3E: GHOSTTY_KEY_RIGHT_CONTROL,
|
||||
0x3C: GHOSTTY_KEY_RIGHT_SHIFT,
|
||||
0x36: GHOSTTY_KEY_RIGHT_SUPER,
|
||||
0x31: GHOSTTY_KEY_SPACE,
|
||||
0x30: GHOSTTY_KEY_TAB,
|
||||
0x7E: GHOSTTY_KEY_UP,
|
||||
|
||||
0x52: GHOSTTY_KEY_KP_0,
|
||||
0x53: GHOSTTY_KEY_KP_1,
|
||||
0x54: GHOSTTY_KEY_KP_2,
|
||||
0x55: GHOSTTY_KEY_KP_3,
|
||||
0x56: GHOSTTY_KEY_KP_4,
|
||||
0x57: GHOSTTY_KEY_KP_5,
|
||||
0x58: GHOSTTY_KEY_KP_6,
|
||||
0x59: GHOSTTY_KEY_KP_7,
|
||||
0x5B: GHOSTTY_KEY_KP_8,
|
||||
0x5C: GHOSTTY_KEY_KP_9,
|
||||
0x45: GHOSTTY_KEY_KP_ADD,
|
||||
0x41: GHOSTTY_KEY_KP_DECIMAL,
|
||||
0x4B: GHOSTTY_KEY_KP_DIVIDE,
|
||||
0x4C: GHOSTTY_KEY_KP_ENTER,
|
||||
0x51: GHOSTTY_KEY_KP_EQUAL,
|
||||
0x43: GHOSTTY_KEY_KP_MULTIPLY,
|
||||
0x4E: GHOSTTY_KEY_KP_SUBTRACT,
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -30,7 +30,7 @@ extension Ghostty {
|
||||
// surface. We need to keep the split root around so that we don't
|
||||
// lose all of the surface state so this must be a ZStack.
|
||||
if let surfaceView = zoomedSurface {
|
||||
SurfaceWrapper(surfaceView: surfaceView)
|
||||
InspectableSurface(surfaceView: surfaceView)
|
||||
}
|
||||
}
|
||||
.focusedValue(\.ghosttySurfaceZoomed, zoomedSurface != nil)
|
||||
@ -343,7 +343,7 @@ extension Ghostty {
|
||||
let pubClose = center.publisher(for: Notification.ghosttyCloseSurface, object: leaf.surface)
|
||||
let pubFocus = center.publisher(for: Notification.ghosttyFocusSplit, object: leaf.surface)
|
||||
|
||||
SurfaceWrapper(surfaceView: leaf.surface, isSplit: !neighbors.isEmpty())
|
||||
InspectableSurface(surfaceView: leaf.surface, isSplit: !neighbors.isEmpty())
|
||||
.onReceive(pub) { onNewSplit(notification: $0) }
|
||||
.onReceive(pubClose) { onClose(notification: $0) }
|
||||
.onReceive(pubFocus) { onMoveFocus(notification: $0) }
|
||||
|
443
macos/Sources/Ghostty/InspectorView.swift
Normal file
443
macos/Sources/Ghostty/InspectorView.swift
Normal file
@ -0,0 +1,443 @@
|
||||
import Foundation
|
||||
import MetalKit
|
||||
import SwiftUI
|
||||
import GhosttyKit
|
||||
|
||||
extension Ghostty {
|
||||
/// InspectableSurface is a type of Surface view that allows an inspector to be attached.
|
||||
struct InspectableSurface: View {
|
||||
/// Same as SurfaceWrapper, see the doc comments there.
|
||||
@ObservedObject var surfaceView: SurfaceView
|
||||
var isSplit: Bool = false
|
||||
|
||||
// Maintain whether our view has focus or not
|
||||
@FocusState private var inspectorFocus: Bool
|
||||
|
||||
var body: some View {
|
||||
let center = NotificationCenter.default
|
||||
let pubInspector = center.publisher(for: Notification.didControlInspector, object: surfaceView)
|
||||
|
||||
ZStack {
|
||||
if (!surfaceView.inspectorVisible) {
|
||||
SurfaceWrapper(surfaceView: surfaceView, isSplit: isSplit)
|
||||
} else {
|
||||
SplitView(.vertical, left: {
|
||||
SurfaceWrapper(surfaceView: surfaceView, isSplit: isSplit)
|
||||
}, right: {
|
||||
InspectorViewRepresentable(surfaceView: surfaceView)
|
||||
.focused($inspectorFocus)
|
||||
.focusedValue(\.ghosttySurfaceTitle, surfaceView.title)
|
||||
.focusedValue(\.ghosttySurfaceView, surfaceView)
|
||||
})
|
||||
}
|
||||
}
|
||||
.onReceive(pubInspector) { onControlInspector($0) }
|
||||
}
|
||||
|
||||
private func onControlInspector(_ notification: SwiftUI.Notification) {
|
||||
// Determine our mode
|
||||
guard let modeAny = notification.userInfo?["mode"] else { return }
|
||||
guard let mode = modeAny as? ghostty_inspector_mode_e else { return }
|
||||
|
||||
switch (mode) {
|
||||
case GHOSTTY_INSPECTOR_TOGGLE:
|
||||
surfaceView.inspectorVisible = !surfaceView.inspectorVisible
|
||||
|
||||
case GHOSTTY_INSPECTOR_SHOW:
|
||||
surfaceView.inspectorVisible = true
|
||||
|
||||
case GHOSTTY_INSPECTOR_HIDE:
|
||||
surfaceView.inspectorVisible = false
|
||||
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
// When we show the inspector, we want to focus on the inspector.
|
||||
// When we hide the inspector, we want to move focus back to the surface.
|
||||
if (surfaceView.inspectorVisible) {
|
||||
// We need to delay this until SwiftUI shows the inspector.
|
||||
DispatchQueue.main.async {
|
||||
_ = surfaceView.resignFirstResponder()
|
||||
inspectorFocus = true
|
||||
}
|
||||
} else {
|
||||
Ghostty.moveFocus(to: surfaceView)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct InspectorViewRepresentable: NSViewRepresentable {
|
||||
/// The surface that this inspector represents.
|
||||
let surfaceView: SurfaceView
|
||||
|
||||
func makeNSView(context: Context) -> InspectorView {
|
||||
let view = InspectorView()
|
||||
view.surfaceView = self.surfaceView
|
||||
return view
|
||||
}
|
||||
|
||||
func updateNSView(_ view: InspectorView, context: Context) {
|
||||
view.surfaceView = self.surfaceView
|
||||
}
|
||||
}
|
||||
|
||||
/// Inspector view is the view for the surface inspector (similar to a web inspector).
|
||||
class InspectorView: MTKView, NSTextInputClient {
|
||||
let commandQueue: MTLCommandQueue
|
||||
|
||||
var surfaceView: SurfaceView? = nil {
|
||||
didSet { surfaceViewDidChange() }
|
||||
}
|
||||
|
||||
private var inspector: ghostty_inspector_t? {
|
||||
guard let surfaceView = self.surfaceView else { return nil }
|
||||
return surfaceView.inspector
|
||||
}
|
||||
|
||||
private var markedText: NSMutableAttributedString = NSMutableAttributedString()
|
||||
|
||||
// We need to support being a first responder so that we can get input events
|
||||
override var acceptsFirstResponder: Bool { return true }
|
||||
|
||||
override init(frame: CGRect, device: MTLDevice?) {
|
||||
// Initialize our Metal primitives
|
||||
guard
|
||||
let device = device ?? MTLCreateSystemDefaultDevice(),
|
||||
let commandQueue = device.makeCommandQueue() else {
|
||||
fatalError("GPU not available")
|
||||
}
|
||||
|
||||
// Setup our properties before initializing the parent
|
||||
self.commandQueue = commandQueue
|
||||
super.init(frame: frame, device: device)
|
||||
|
||||
// This makes it so renders only happen when we request
|
||||
self.enableSetNeedsDisplay = true
|
||||
self.isPaused = true
|
||||
|
||||
// After initializing the parent we can set our own properties
|
||||
self.device = MTLCreateSystemDefaultDevice()
|
||||
self.clearColor = MTLClearColor(red: 0x28 / 0xFF, green: 0x2C / 0xFF, blue: 0x34 / 0xFF, alpha: 1.0)
|
||||
|
||||
// Setup our tracking areas for mouse events
|
||||
updateTrackingAreas()
|
||||
}
|
||||
|
||||
required init(coder: NSCoder) {
|
||||
fatalError("init(coder:) is not supported for this view")
|
||||
}
|
||||
|
||||
deinit {
|
||||
trackingAreas.forEach { removeTrackingArea($0) }
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
// MARK: Internal Inspector Funcs
|
||||
|
||||
private func surfaceViewDidChange() {
|
||||
let center = NotificationCenter.default
|
||||
center.removeObserver(self)
|
||||
|
||||
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)
|
||||
|
||||
// Register an observer for render requests
|
||||
center.addObserver(
|
||||
self,
|
||||
selector: #selector(didRequestRender),
|
||||
name: Ghostty.Notification.inspectorNeedsDisplay,
|
||||
object: surfaceView)
|
||||
}
|
||||
|
||||
@objc private func didRequestRender(notification: SwiftUI.Notification) {
|
||||
self.needsDisplay = true
|
||||
}
|
||||
|
||||
private func updateSize() {
|
||||
guard let inspector = self.inspector else { return }
|
||||
|
||||
// Detect our X/Y scale factor so we can update our surface
|
||||
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)
|
||||
|
||||
// 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))
|
||||
}
|
||||
|
||||
// MARK: NSView
|
||||
|
||||
override func becomeFirstResponder() -> Bool {
|
||||
let result = super.becomeFirstResponder()
|
||||
if (result) {
|
||||
if let inspector = self.inspector {
|
||||
ghostty_inspector_set_focus(inspector, true)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
override func resignFirstResponder() -> Bool {
|
||||
let result = super.resignFirstResponder()
|
||||
if (result) {
|
||||
if let inspector = self.inspector {
|
||||
ghostty_inspector_set_focus(inspector, false)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
override func updateTrackingAreas() {
|
||||
// To update our tracking area we just recreate it all.
|
||||
trackingAreas.forEach { removeTrackingArea($0) }
|
||||
|
||||
// This tracking area is across the entire frame to notify us of mouse movements.
|
||||
addTrackingArea(NSTrackingArea(
|
||||
rect: frame,
|
||||
options: [
|
||||
.mouseMoved,
|
||||
|
||||
// Only send mouse events that happen in our visible (not obscured) rect
|
||||
.inVisibleRect,
|
||||
|
||||
// We want active always because we want to still send mouse reports
|
||||
// even if we're not focused or key.
|
||||
.activeAlways,
|
||||
],
|
||||
owner: self,
|
||||
userInfo: nil))
|
||||
}
|
||||
|
||||
override func viewDidChangeBackingProperties() {
|
||||
super.viewDidChangeBackingProperties()
|
||||
updateSize()
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
override func mouseMoved(with event: NSEvent) {
|
||||
guard let inspector = self.inspector else { return }
|
||||
|
||||
// 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)
|
||||
|
||||
}
|
||||
|
||||
override func mouseDragged(with event: NSEvent) {
|
||||
self.mouseMoved(with: event)
|
||||
}
|
||||
|
||||
override func scrollWheel(with event: NSEvent) {
|
||||
guard let inspector = self.inspector else { return }
|
||||
|
||||
// Builds up the "input.ScrollMods" bitmask
|
||||
var mods: Int32 = 0
|
||||
|
||||
var x = event.scrollingDeltaX
|
||||
var y = event.scrollingDeltaY
|
||||
if event.hasPreciseScrollingDeltas {
|
||||
mods = 1
|
||||
|
||||
// We do a 2x speed multiplier. This is subjective, it "feels" better to me.
|
||||
x *= 2;
|
||||
y *= 2;
|
||||
|
||||
// TODO(mitchellh): do we have to scale the x/y here by window scale factor?
|
||||
}
|
||||
|
||||
// Determine our momentum value
|
||||
var momentum: ghostty_input_mouse_momentum_e = GHOSTTY_MOUSE_MOMENTUM_NONE
|
||||
switch (event.momentumPhase) {
|
||||
case .began:
|
||||
momentum = GHOSTTY_MOUSE_MOMENTUM_BEGAN
|
||||
case .stationary:
|
||||
momentum = GHOSTTY_MOUSE_MOMENTUM_STATIONARY
|
||||
case .changed:
|
||||
momentum = GHOSTTY_MOUSE_MOMENTUM_CHANGED
|
||||
case .ended:
|
||||
momentum = GHOSTTY_MOUSE_MOMENTUM_ENDED
|
||||
case .cancelled:
|
||||
momentum = GHOSTTY_MOUSE_MOMENTUM_CANCELLED
|
||||
case .mayBegin:
|
||||
momentum = GHOSTTY_MOUSE_MOMENTUM_MAY_BEGIN
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
// Pack our momentum value into the mods bitmask
|
||||
mods |= Int32(momentum.rawValue) << 1
|
||||
|
||||
ghostty_inspector_mouse_scroll(inspector, x, y, mods)
|
||||
}
|
||||
|
||||
override func keyDown(with event: NSEvent) {
|
||||
let action = event.isARepeat ? GHOSTTY_ACTION_REPEAT : GHOSTTY_ACTION_PRESS
|
||||
keyAction(action, event: event)
|
||||
self.interpretKeyEvents([event])
|
||||
}
|
||||
|
||||
override func keyUp(with event: NSEvent) {
|
||||
keyAction(GHOSTTY_ACTION_RELEASE, event: event)
|
||||
}
|
||||
|
||||
override func flagsChanged(with event: NSEvent) {
|
||||
let mod: UInt32;
|
||||
switch (event.keyCode) {
|
||||
case 0x39: mod = GHOSTTY_MODS_CAPS.rawValue
|
||||
case 0x38, 0x3C: mod = GHOSTTY_MODS_SHIFT.rawValue
|
||||
case 0x3B, 0x3E: mod = GHOSTTY_MODS_CTRL.rawValue
|
||||
case 0x3A, 0x3D: mod = GHOSTTY_MODS_ALT.rawValue
|
||||
case 0x37, 0x36: mod = GHOSTTY_MODS_SUPER.rawValue
|
||||
default: return
|
||||
}
|
||||
|
||||
// The keyAction function will do this AGAIN below which sucks to repeat
|
||||
// but this is super cheap and flagsChanged isn't that common.
|
||||
let mods = Ghostty.ghosttyMods(event.modifierFlags)
|
||||
|
||||
// If the key that pressed this is active, its a press, else release
|
||||
var action = GHOSTTY_ACTION_RELEASE
|
||||
if (mods.rawValue & mod != 0) { action = GHOSTTY_ACTION_PRESS }
|
||||
|
||||
keyAction(action, event: event)
|
||||
}
|
||||
|
||||
private func keyAction(_ action: ghostty_input_action_e, event: NSEvent) {
|
||||
guard let inspector = self.inspector else { return }
|
||||
guard let key = Ghostty.keycodeToKey[event.keyCode] else { return }
|
||||
let mods = Ghostty.ghosttyMods(event.modifierFlags)
|
||||
ghostty_inspector_key(inspector, action, key, mods)
|
||||
}
|
||||
|
||||
// MARK: NSTextInputClient
|
||||
|
||||
func hasMarkedText() -> Bool {
|
||||
return markedText.length > 0
|
||||
}
|
||||
|
||||
func markedRange() -> NSRange {
|
||||
guard markedText.length > 0 else { return NSRange() }
|
||||
return NSRange(0...(markedText.length-1))
|
||||
}
|
||||
|
||||
func selectedRange() -> NSRange {
|
||||
return NSRange()
|
||||
}
|
||||
|
||||
func setMarkedText(_ string: Any, selectedRange: NSRange, replacementRange: NSRange) {
|
||||
switch string {
|
||||
case let v as NSAttributedString:
|
||||
self.markedText = NSMutableAttributedString(attributedString: v)
|
||||
|
||||
case let v as String:
|
||||
self.markedText = NSMutableAttributedString(string: v)
|
||||
|
||||
default:
|
||||
print("unknown marked text: \(string)")
|
||||
}
|
||||
}
|
||||
|
||||
func unmarkText() {
|
||||
self.markedText.mutableString.setString("")
|
||||
}
|
||||
|
||||
func validAttributesForMarkedText() -> [NSAttributedString.Key] {
|
||||
return []
|
||||
}
|
||||
|
||||
func attributedSubstring(forProposedRange range: NSRange, actualRange: NSRangePointer?) -> NSAttributedString? {
|
||||
return nil
|
||||
}
|
||||
|
||||
func characterIndex(for point: NSPoint) -> Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func firstRect(forCharacterRange range: NSRange, actualRange: NSRangePointer?) -> NSRect {
|
||||
return NSMakeRect(frame.origin.x, frame.origin.y, 0, 0)
|
||||
}
|
||||
|
||||
func insertText(_ string: Any, replacementRange: NSRange) {
|
||||
// We must have an associated event
|
||||
guard NSApp.currentEvent != nil else { return }
|
||||
guard let inspector = self.inspector else { return }
|
||||
|
||||
// We want the string view of the any value
|
||||
var chars = ""
|
||||
switch (string) {
|
||||
case let v as NSAttributedString:
|
||||
chars = v.string
|
||||
case let v as String:
|
||||
chars = v
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
let len = chars.utf8CString.count
|
||||
if (len == 0) { return }
|
||||
|
||||
chars.withCString { ptr in
|
||||
ghostty_inspector_text(inspector, ptr)
|
||||
}
|
||||
}
|
||||
|
||||
override func doCommand(by selector: Selector) {
|
||||
// This currently just prevents NSBeep from interpretKeyEvents but in the future
|
||||
// we may want to make some of this work.
|
||||
}
|
||||
|
||||
// MARK: MTKView
|
||||
|
||||
override func draw(_ dirtyRect: NSRect) {
|
||||
guard
|
||||
let commandBuffer = self.commandQueue.makeCommandBuffer(),
|
||||
let descriptor = self.currentRenderPassDescriptor else {
|
||||
return
|
||||
}
|
||||
|
||||
// We always update our size because sometimes draw is called
|
||||
// between resize events and if our size is wrong with the underlying
|
||||
// drawable we will crash.
|
||||
updateSize()
|
||||
|
||||
// Render
|
||||
ghostty_inspector_metal_render(
|
||||
inspector,
|
||||
Unmanaged.passRetained(commandBuffer).toOpaque(),
|
||||
Unmanaged.passRetained(descriptor).toOpaque()
|
||||
)
|
||||
|
||||
guard let drawable = self.currentDrawable else { return }
|
||||
commandBuffer.present(drawable)
|
||||
commandBuffer.commit()
|
||||
}
|
||||
}
|
||||
}
|
@ -102,6 +102,12 @@ extension Ghostty.Notification {
|
||||
/// Notification
|
||||
static let didReceiveInitialWindowFrame = Notification.Name("com.mitchellh.ghostty.didReceiveInitialWindowFrame")
|
||||
static let FrameKey = "com.mitchellh.ghostty.frame"
|
||||
|
||||
/// Notification to render the inspector for a surface
|
||||
static let inspectorNeedsDisplay = Notification.Name("com.mitchellh.ghostty.inspectorNeedsDisplay")
|
||||
|
||||
/// Notification to show/hide the inspector
|
||||
static let didControlInspector = Notification.Name("com.mitchellh.ghostty.didControlInspector")
|
||||
}
|
||||
|
||||
// Make the input enum hashable.
|
||||
|
@ -245,6 +245,23 @@ extension Ghostty {
|
||||
// then the view is moved to a new window.
|
||||
var initialSize: NSSize? = nil
|
||||
|
||||
// Returns the inspector instance for this surface, or nil if the
|
||||
// surface has been closed.
|
||||
var inspector: ghostty_inspector_t? {
|
||||
guard let surface = self.surface else { return nil }
|
||||
return ghostty_surface_inspector(surface)
|
||||
}
|
||||
|
||||
// True if the inspector should be visible
|
||||
@Published var inspectorVisible: Bool = false {
|
||||
didSet {
|
||||
if (oldValue && !inspectorVisible) {
|
||||
guard let surface = self.surface else { return }
|
||||
ghostty_inspector_free(surface)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private(set) var surface: ghostty_surface_t?
|
||||
var error: Error? = nil
|
||||
|
||||
@ -561,25 +578,25 @@ extension Ghostty {
|
||||
|
||||
override func mouseDown(with event: NSEvent) {
|
||||
guard let surface = self.surface else { return }
|
||||
let mods = Self.translateFlags(event.modifierFlags)
|
||||
let mods = Ghostty.ghosttyMods(event.modifierFlags)
|
||||
ghostty_surface_mouse_button(surface, GHOSTTY_MOUSE_PRESS, GHOSTTY_MOUSE_LEFT, mods)
|
||||
}
|
||||
|
||||
override func mouseUp(with event: NSEvent) {
|
||||
guard let surface = self.surface else { return }
|
||||
let mods = Self.translateFlags(event.modifierFlags)
|
||||
let mods = Ghostty.ghosttyMods(event.modifierFlags)
|
||||
ghostty_surface_mouse_button(surface, GHOSTTY_MOUSE_RELEASE, GHOSTTY_MOUSE_LEFT, mods)
|
||||
}
|
||||
|
||||
override func rightMouseDown(with event: NSEvent) {
|
||||
guard let surface = self.surface else { return }
|
||||
let mods = Self.translateFlags(event.modifierFlags)
|
||||
let mods = Ghostty.ghosttyMods(event.modifierFlags)
|
||||
ghostty_surface_mouse_button(surface, GHOSTTY_MOUSE_PRESS, GHOSTTY_MOUSE_RIGHT, mods)
|
||||
}
|
||||
|
||||
override func rightMouseUp(with event: NSEvent) {
|
||||
guard let surface = self.surface else { return }
|
||||
let mods = Self.translateFlags(event.modifierFlags)
|
||||
let mods = Ghostty.ghosttyMods(event.modifierFlags)
|
||||
ghostty_surface_mouse_button(surface, GHOSTTY_MOUSE_RELEASE, GHOSTTY_MOUSE_RIGHT, mods)
|
||||
}
|
||||
|
||||
@ -723,7 +740,7 @@ extension Ghostty {
|
||||
|
||||
// The keyAction function will do this AGAIN below which sucks to repeat
|
||||
// but this is super cheap and flagsChanged isn't that common.
|
||||
let mods = Self.translateFlags(event.modifierFlags)
|
||||
let mods = Ghostty.ghosttyMods(event.modifierFlags)
|
||||
|
||||
// If the key that pressed this is active, its a press, else release
|
||||
var action = GHOSTTY_ACTION_RELEASE
|
||||
@ -734,7 +751,7 @@ extension Ghostty {
|
||||
|
||||
private func keyAction(_ action: ghostty_input_action_e, event: NSEvent) {
|
||||
guard let surface = self.surface else { return }
|
||||
let mods = Self.translateFlags(event.modifierFlags)
|
||||
let mods = Ghostty.ghosttyMods(event.modifierFlags)
|
||||
ghostty_surface_key(surface, action, UInt32(event.keyCode), mods)
|
||||
}
|
||||
|
||||
@ -858,143 +875,6 @@ extension Ghostty {
|
||||
|
||||
print("SEL: \(selector)")
|
||||
}
|
||||
|
||||
private static func translateFlags(_ flags: NSEvent.ModifierFlags) -> ghostty_input_mods_e {
|
||||
var mods: UInt32 = GHOSTTY_MODS_NONE.rawValue
|
||||
|
||||
if (flags.contains(.shift)) { mods |= GHOSTTY_MODS_SHIFT.rawValue }
|
||||
if (flags.contains(.control)) { mods |= GHOSTTY_MODS_CTRL.rawValue }
|
||||
if (flags.contains(.option)) { mods |= GHOSTTY_MODS_ALT.rawValue }
|
||||
if (flags.contains(.command)) { mods |= GHOSTTY_MODS_SUPER.rawValue }
|
||||
if (flags.contains(.capsLock)) { mods |= GHOSTTY_MODS_CAPS.rawValue }
|
||||
|
||||
// Handle sided input. We can't tell that both are pressed in the
|
||||
// Ghostty structure but thats okay -- we don't use that information.
|
||||
let rawFlags = flags.rawValue
|
||||
if (rawFlags & UInt(NX_DEVICERSHIFTKEYMASK) != 0) { mods |= GHOSTTY_MODS_SHIFT_RIGHT.rawValue }
|
||||
if (rawFlags & UInt(NX_DEVICERCTLKEYMASK) != 0) { mods |= GHOSTTY_MODS_CTRL_RIGHT.rawValue }
|
||||
if (rawFlags & UInt(NX_DEVICERALTKEYMASK) != 0) { mods |= GHOSTTY_MODS_ALT_RIGHT.rawValue }
|
||||
if (rawFlags & UInt(NX_DEVICERCMDKEYMASK) != 0) { mods |= GHOSTTY_MODS_SUPER_RIGHT.rawValue }
|
||||
|
||||
return ghostty_input_mods_e(mods)
|
||||
}
|
||||
|
||||
// Mapping of event keyCode to ghostty input key values. This is cribbed from
|
||||
// glfw mostly since we started as a glfw-based app way back in the day!
|
||||
static let keycodes: [UInt16 : ghostty_input_key_e] = [
|
||||
0x1D: GHOSTTY_KEY_ZERO,
|
||||
0x12: GHOSTTY_KEY_ONE,
|
||||
0x13: GHOSTTY_KEY_TWO,
|
||||
0x14: GHOSTTY_KEY_THREE,
|
||||
0x15: GHOSTTY_KEY_FOUR,
|
||||
0x17: GHOSTTY_KEY_FIVE,
|
||||
0x16: GHOSTTY_KEY_SIX,
|
||||
0x1A: GHOSTTY_KEY_SEVEN,
|
||||
0x1C: GHOSTTY_KEY_EIGHT,
|
||||
0x19: GHOSTTY_KEY_NINE,
|
||||
0x00: GHOSTTY_KEY_A,
|
||||
0x0B: GHOSTTY_KEY_B,
|
||||
0x08: GHOSTTY_KEY_C,
|
||||
0x02: GHOSTTY_KEY_D,
|
||||
0x0E: GHOSTTY_KEY_E,
|
||||
0x03: GHOSTTY_KEY_F,
|
||||
0x05: GHOSTTY_KEY_G,
|
||||
0x04: GHOSTTY_KEY_H,
|
||||
0x22: GHOSTTY_KEY_I,
|
||||
0x26: GHOSTTY_KEY_J,
|
||||
0x28: GHOSTTY_KEY_K,
|
||||
0x25: GHOSTTY_KEY_L,
|
||||
0x2E: GHOSTTY_KEY_M,
|
||||
0x2D: GHOSTTY_KEY_N,
|
||||
0x1F: GHOSTTY_KEY_O,
|
||||
0x23: GHOSTTY_KEY_P,
|
||||
0x0C: GHOSTTY_KEY_Q,
|
||||
0x0F: GHOSTTY_KEY_R,
|
||||
0x01: GHOSTTY_KEY_S,
|
||||
0x11: GHOSTTY_KEY_T,
|
||||
0x20: GHOSTTY_KEY_U,
|
||||
0x09: GHOSTTY_KEY_V,
|
||||
0x0D: GHOSTTY_KEY_W,
|
||||
0x07: GHOSTTY_KEY_X,
|
||||
0x10: GHOSTTY_KEY_Y,
|
||||
0x06: GHOSTTY_KEY_Z,
|
||||
|
||||
0x27: GHOSTTY_KEY_APOSTROPHE,
|
||||
0x2A: GHOSTTY_KEY_BACKSLASH,
|
||||
0x2B: GHOSTTY_KEY_COMMA,
|
||||
0x18: GHOSTTY_KEY_EQUAL,
|
||||
0x32: GHOSTTY_KEY_GRAVE_ACCENT,
|
||||
0x21: GHOSTTY_KEY_LEFT_BRACKET,
|
||||
0x1B: GHOSTTY_KEY_MINUS,
|
||||
0x2F: GHOSTTY_KEY_PERIOD,
|
||||
0x1E: GHOSTTY_KEY_RIGHT_BRACKET,
|
||||
0x29: GHOSTTY_KEY_SEMICOLON,
|
||||
0x2C: GHOSTTY_KEY_SLASH,
|
||||
|
||||
0x33: GHOSTTY_KEY_BACKSPACE,
|
||||
0x39: GHOSTTY_KEY_CAPS_LOCK,
|
||||
0x75: GHOSTTY_KEY_DELETE,
|
||||
0x7D: GHOSTTY_KEY_DOWN,
|
||||
0x77: GHOSTTY_KEY_END,
|
||||
0x24: GHOSTTY_KEY_ENTER,
|
||||
0x35: GHOSTTY_KEY_ESCAPE,
|
||||
0x7A: GHOSTTY_KEY_F1,
|
||||
0x78: GHOSTTY_KEY_F2,
|
||||
0x63: GHOSTTY_KEY_F3,
|
||||
0x76: GHOSTTY_KEY_F4,
|
||||
0x60: GHOSTTY_KEY_F5,
|
||||
0x61: GHOSTTY_KEY_F6,
|
||||
0x62: GHOSTTY_KEY_F7,
|
||||
0x64: GHOSTTY_KEY_F8,
|
||||
0x65: GHOSTTY_KEY_F9,
|
||||
0x6D: GHOSTTY_KEY_F10,
|
||||
0x67: GHOSTTY_KEY_F11,
|
||||
0x6F: GHOSTTY_KEY_F12,
|
||||
0x69: GHOSTTY_KEY_PRINT_SCREEN,
|
||||
0x6B: GHOSTTY_KEY_F14,
|
||||
0x71: GHOSTTY_KEY_F15,
|
||||
0x6A: GHOSTTY_KEY_F16,
|
||||
0x40: GHOSTTY_KEY_F17,
|
||||
0x4F: GHOSTTY_KEY_F18,
|
||||
0x50: GHOSTTY_KEY_F19,
|
||||
0x5A: GHOSTTY_KEY_F20,
|
||||
0x73: GHOSTTY_KEY_HOME,
|
||||
0x72: GHOSTTY_KEY_INSERT,
|
||||
0x7B: GHOSTTY_KEY_LEFT,
|
||||
0x3A: GHOSTTY_KEY_LEFT_ALT,
|
||||
0x3B: GHOSTTY_KEY_LEFT_CONTROL,
|
||||
0x38: GHOSTTY_KEY_LEFT_SHIFT,
|
||||
0x37: GHOSTTY_KEY_LEFT_SUPER,
|
||||
0x47: GHOSTTY_KEY_NUM_LOCK,
|
||||
0x79: GHOSTTY_KEY_PAGE_DOWN,
|
||||
0x74: GHOSTTY_KEY_PAGE_UP,
|
||||
0x7C: GHOSTTY_KEY_RIGHT,
|
||||
0x3D: GHOSTTY_KEY_RIGHT_ALT,
|
||||
0x3E: GHOSTTY_KEY_RIGHT_CONTROL,
|
||||
0x3C: GHOSTTY_KEY_RIGHT_SHIFT,
|
||||
0x36: GHOSTTY_KEY_RIGHT_SUPER,
|
||||
0x31: GHOSTTY_KEY_SPACE,
|
||||
0x30: GHOSTTY_KEY_TAB,
|
||||
0x7E: GHOSTTY_KEY_UP,
|
||||
|
||||
0x52: GHOSTTY_KEY_KP_0,
|
||||
0x53: GHOSTTY_KEY_KP_1,
|
||||
0x54: GHOSTTY_KEY_KP_2,
|
||||
0x55: GHOSTTY_KEY_KP_3,
|
||||
0x56: GHOSTTY_KEY_KP_4,
|
||||
0x57: GHOSTTY_KEY_KP_5,
|
||||
0x58: GHOSTTY_KEY_KP_6,
|
||||
0x59: GHOSTTY_KEY_KP_7,
|
||||
0x5B: GHOSTTY_KEY_KP_8,
|
||||
0x5C: GHOSTTY_KEY_KP_9,
|
||||
0x45: GHOSTTY_KEY_KP_ADD,
|
||||
0x41: GHOSTTY_KEY_KP_DECIMAL,
|
||||
0x4B: GHOSTTY_KEY_KP_DIVIDE,
|
||||
0x4C: GHOSTTY_KEY_KP_ENTER,
|
||||
0x51: GHOSTTY_KEY_KP_EQUAL,
|
||||
0x43: GHOSTTY_KEY_KP_MULTIPLY,
|
||||
0x4E: GHOSTTY_KEY_KP_SUBTRACT,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
26
macos/Sources/Helpers/MetalView.swift
Normal file
26
macos/Sources/Helpers/MetalView.swift
Normal file
@ -0,0 +1,26 @@
|
||||
import SwiftUI
|
||||
import MetalKit
|
||||
|
||||
/// Renders an MTKView with the given renderer class.
|
||||
struct MetalView<V: MTKView>: View {
|
||||
@State private var metalView = V()
|
||||
|
||||
var body: some View {
|
||||
MetalViewRepresentable(metalView: $metalView)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate struct MetalViewRepresentable<V: MTKView>: NSViewRepresentable {
|
||||
@Binding var metalView: V
|
||||
|
||||
func makeNSView(context: Context) -> some NSView {
|
||||
metalView
|
||||
}
|
||||
|
||||
func updateNSView(_ view: NSViewType, context: Context) {
|
||||
updateMetalView()
|
||||
}
|
||||
|
||||
func updateMetalView() {
|
||||
}
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="22154" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="22155" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<dependencies>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="22154"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="22155"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
|
||||
@ -33,6 +33,7 @@
|
||||
<outlet property="menuSelectSplitRight" destination="upj-mc-L7X" id="nLY-o1-lky"/>
|
||||
<outlet property="menuSplitHorizontal" destination="VUR-Ld-nLx" id="RxO-Zw-ovb"/>
|
||||
<outlet property="menuSplitVertical" destination="UDZ-4y-6xL" id="fgZ-Wb-8OR"/>
|
||||
<outlet property="menuTerminalInspector" destination="QwP-M5-fvh" id="wJi-Dh-S9f"/>
|
||||
<outlet property="menuToggleFullScreen" destination="8kY-Pi-KaY" id="yQg-6V-OO6"/>
|
||||
<outlet property="menuZoomSplit" destination="oPd-mn-IEH" id="wTu-jK-egI"/>
|
||||
</connections>
|
||||
@ -153,6 +154,13 @@
|
||||
<action selector="decreaseFontSize:" target="bbz-4X-AYv" id="rlw-0o-kA2"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="L3L-I8-sqk"/>
|
||||
<menuItem title="Terminal Inspector" id="QwP-M5-fvh">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleTerminalInspector:" target="bbz-4X-AYv" id="DON-fR-wyr"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
|
@ -3,8 +3,8 @@
|
||||
.version = "0.1.0",
|
||||
.dependencies = .{
|
||||
.macos_sdk = .{
|
||||
.url = "https://github.com/mitchellh/zig-build-macos-sdk/archive/7e50d6ea241403615d5ebc9f1df4680d3907fa92.tar.gz",
|
||||
.hash = "1220eb266898413ecfe5aaf7f29cc17eb479d046adecc94ebc7d5e1e807d2aabdd70",
|
||||
.url = "https://github.com/mitchellh/zig-build-macos-sdk/archive/7a7cb3816617dbaf87ecbc3fd90ad56a6f828275.tar.gz",
|
||||
.hash = "1220a1dd91457d0131b50db15fb1bc51208ecadf1a23ad5dfa2d3a6bb3d1de5230c1",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
91
pkg/cimgui/build.zig
Normal file
91
pkg/cimgui/build.zig
Normal file
@ -0,0 +1,91 @@
|
||||
const std = @import("std");
|
||||
const NativeTargetInfo = std.zig.system.NativeTargetInfo;
|
||||
|
||||
pub fn build(b: *std.Build) !void {
|
||||
const target = b.standardTargetOptions(.{});
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
|
||||
_ = b.addModule("cimgui", .{ .source_file = .{ .path = "main.zig" } });
|
||||
|
||||
const imgui = b.dependency("imgui", .{});
|
||||
const freetype = b.dependency("freetype", .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.@"enable-libpng" = true,
|
||||
});
|
||||
const lib = b.addStaticLibrary(.{
|
||||
.name = "cimgui",
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
lib.linkLibC();
|
||||
lib.linkLibCpp();
|
||||
lib.linkLibrary(freetype.artifact("freetype"));
|
||||
if (target.isWindows()) {
|
||||
lib.linkSystemLibrary("imm32");
|
||||
}
|
||||
|
||||
lib.addIncludePath(imgui.path(""));
|
||||
|
||||
var flags = std.ArrayList([]const u8).init(b.allocator);
|
||||
defer flags.deinit();
|
||||
try flags.appendSlice(&.{
|
||||
"-DCIMGUI_FREETYPE=1",
|
||||
"-DIMGUI_USE_WCHAR32=1",
|
||||
"-DIMGUI_DISABLE_OBSOLETE_FUNCTIONS=1",
|
||||
});
|
||||
if (target.isWindows()) {
|
||||
try flags.appendSlice(&.{
|
||||
"-DIMGUI_IMPL_API=extern\t\"C\"\t__declspec(dllexport)",
|
||||
});
|
||||
} else {
|
||||
try flags.appendSlice(&.{
|
||||
"-DIMGUI_IMPL_API=extern\t\"C\"",
|
||||
});
|
||||
}
|
||||
|
||||
lib.addCSourceFile(.{ .file = .{ .path = "vendor/cimgui.cpp" }, .flags = flags.items });
|
||||
lib.addCSourceFile(.{ .file = imgui.path("imgui.cpp"), .flags = flags.items });
|
||||
lib.addCSourceFile(.{ .file = imgui.path("imgui_draw.cpp"), .flags = flags.items });
|
||||
lib.addCSourceFile(.{ .file = imgui.path("imgui_demo.cpp"), .flags = flags.items });
|
||||
lib.addCSourceFile(.{ .file = imgui.path("imgui_widgets.cpp"), .flags = flags.items });
|
||||
lib.addCSourceFile(.{ .file = imgui.path("imgui_tables.cpp"), .flags = flags.items });
|
||||
lib.addCSourceFile(.{ .file = imgui.path("misc/freetype/imgui_freetype.cpp"), .flags = flags.items });
|
||||
|
||||
lib.addCSourceFile(.{
|
||||
.file = imgui.path("backends/imgui_impl_opengl3.cpp"),
|
||||
.flags = flags.items,
|
||||
});
|
||||
|
||||
if (target.isDarwin()) {
|
||||
if (!target.isNative()) try @import("apple_sdk").addPaths(b, lib);
|
||||
lib.addCSourceFile(.{
|
||||
.file = imgui.path("backends/imgui_impl_metal.mm"),
|
||||
.flags = flags.items,
|
||||
});
|
||||
lib.addCSourceFile(.{
|
||||
.file = imgui.path("backends/imgui_impl_osx.mm"),
|
||||
.flags = flags.items,
|
||||
});
|
||||
}
|
||||
|
||||
lib.installHeadersDirectoryOptions(.{
|
||||
.source_dir = .{ .path = "vendor" },
|
||||
.install_dir = .header,
|
||||
.install_subdir = "",
|
||||
.include_extensions = &.{".h"},
|
||||
});
|
||||
|
||||
b.installArtifact(lib);
|
||||
|
||||
const test_exe = b.addTest(.{
|
||||
.name = "test",
|
||||
.root_source_file = .{ .path = "main.zig" },
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
test_exe.linkLibrary(lib);
|
||||
const tests_run = b.addRunArtifact(test_exe);
|
||||
const test_step = b.step("test", "Run tests");
|
||||
test_step.dependOn(&tests_run.step);
|
||||
}
|
16
pkg/cimgui/build.zig.zon
Normal file
16
pkg/cimgui/build.zig.zon
Normal file
@ -0,0 +1,16 @@
|
||||
.{
|
||||
.name = "cimgui",
|
||||
.version = "1.89.9",
|
||||
.paths = .{""},
|
||||
.dependencies = .{
|
||||
// This should be kept in sync with the submodule in the cimgui source
|
||||
// code to be safe that they're compatible.
|
||||
.imgui = .{
|
||||
.url = "https://github.com/ocornut/imgui/archive/1d8e48c161370c37628c4f37f3f87cb19fbcb723.tar.gz",
|
||||
.hash = "12205e93e208aada4c835acdc3e2c1fac95b3ad92b47abe6412ab043f9f13817ad9b",
|
||||
},
|
||||
|
||||
.apple_sdk = .{ .path = "../apple-sdk" },
|
||||
.freetype = .{ .path = "../freetype" },
|
||||
},
|
||||
}
|
24
pkg/cimgui/c.zig
Normal file
24
pkg/cimgui/c.zig
Normal file
@ -0,0 +1,24 @@
|
||||
const c = @cImport({
|
||||
@cDefine("CIMGUI_DEFINE_ENUMS_AND_STRUCTS", "1");
|
||||
@cInclude("cimgui.h");
|
||||
});
|
||||
|
||||
// Export all of the C API
|
||||
pub usingnamespace c;
|
||||
|
||||
// OpenGL
|
||||
pub extern fn ImGui_ImplOpenGL3_Init(?[*:0]const u8) callconv(.C) bool;
|
||||
pub extern fn ImGui_ImplOpenGL3_Shutdown() callconv(.C) void;
|
||||
pub extern fn ImGui_ImplOpenGL3_NewFrame() callconv(.C) void;
|
||||
pub extern fn ImGui_ImplOpenGL3_RenderDrawData(*c.ImDrawData) callconv(.C) void;
|
||||
|
||||
// Metal
|
||||
pub extern fn ImGui_ImplMetal_Init(*anyopaque) callconv(.C) bool;
|
||||
pub extern fn ImGui_ImplMetal_Shutdown() callconv(.C) void;
|
||||
pub extern fn ImGui_ImplMetal_NewFrame(*anyopaque) callconv(.C) void;
|
||||
pub extern fn ImGui_ImplMetal_RenderDrawData(*c.ImDrawData, *anyopaque, *anyopaque) callconv(.C) void;
|
||||
|
||||
// OSX
|
||||
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;
|
3
pkg/cimgui/main.zig
Normal file
3
pkg/cimgui/main.zig
Normal file
@ -0,0 +1,3 @@
|
||||
pub const c = @import("c.zig");
|
||||
|
||||
test {}
|
5554
pkg/cimgui/vendor/cimgui.cpp
vendored
Normal file
5554
pkg/cimgui/vendor/cimgui.cpp
vendored
Normal file
File diff suppressed because it is too large
Load Diff
4577
pkg/cimgui/vendor/cimgui.h
vendored
Normal file
4577
pkg/cimgui/vendor/cimgui.h
vendored
Normal file
File diff suppressed because it is too large
Load Diff
10
src/App.zig
10
src/App.zig
@ -205,6 +205,7 @@ fn drainMailbox(self: *App, rt_app: *apprt.App) !void {
|
||||
.quit => try self.setQuit(),
|
||||
.surface_message => |msg| try self.surfaceMessage(msg.surface, msg.message),
|
||||
.redraw_surface => |surface| try self.redrawSurface(rt_app, surface),
|
||||
.redraw_inspector => |surface| try self.redrawInspector(rt_app, surface),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -232,6 +233,11 @@ fn redrawSurface(self: *App, rt_app: *apprt.App, surface: *apprt.Surface) !void
|
||||
rt_app.redrawSurface(surface);
|
||||
}
|
||||
|
||||
fn redrawInspector(self: *App, rt_app: *apprt.App, surface: *apprt.Surface) !void {
|
||||
if (!self.hasSurface(&surface.core_surface)) return;
|
||||
rt_app.redrawInspector(surface);
|
||||
}
|
||||
|
||||
/// Create a new window
|
||||
pub fn newWindow(self: *App, rt_app: *apprt.App, msg: Message.NewWindow) !void {
|
||||
if (!@hasDecl(apprt.App, "newWindow")) {
|
||||
@ -304,6 +310,10 @@ pub const Message = union(enum) {
|
||||
/// message if it needs to.
|
||||
redraw_surface: *apprt.Surface,
|
||||
|
||||
/// Redraw the inspector. This is called whenever some non-OS event
|
||||
/// causes the inspector to need to be redrawn.
|
||||
redraw_inspector: *apprt.Surface,
|
||||
|
||||
const NewWindow = struct {
|
||||
/// The parent surface
|
||||
parent: ?*Surface = null,
|
||||
|
141
src/Surface.zig
141
src/Surface.zig
@ -33,6 +33,7 @@ const configpkg = @import("config.zig");
|
||||
const input = @import("input.zig");
|
||||
const App = @import("App.zig");
|
||||
const internal_os = @import("os/main.zig");
|
||||
const inspector = @import("inspector/main.zig");
|
||||
|
||||
const log = std.log.scoped(.surface);
|
||||
|
||||
@ -74,6 +75,9 @@ io: termio.Impl,
|
||||
io_thread: termio.Thread,
|
||||
io_thr: std.Thread,
|
||||
|
||||
/// Terminal inspector
|
||||
inspector: ?*inspector.Inspector = null,
|
||||
|
||||
/// All the cached sizes since we need them at various times.
|
||||
screen_size: renderer.ScreenSize,
|
||||
grid_size: renderer.GridSize,
|
||||
@ -550,8 +554,14 @@ pub fn deinit(self: *Surface) void {
|
||||
self.font_lib.deinit();
|
||||
self.alloc.destroy(self.font_group);
|
||||
|
||||
if (self.inspector) |v| {
|
||||
v.deinit();
|
||||
self.alloc.destroy(v);
|
||||
}
|
||||
|
||||
self.alloc.destroy(self.renderer_state.mutex);
|
||||
self.config.deinit();
|
||||
|
||||
log.info("surface closed addr={x}", .{@intFromPtr(self)});
|
||||
}
|
||||
|
||||
@ -561,6 +571,53 @@ pub fn close(self: *Surface) void {
|
||||
self.rt_surface.close(self.needsConfirmQuit());
|
||||
}
|
||||
|
||||
/// Activate the inspector. This will begin collecting inspection data.
|
||||
/// This will not affect the GUI. The GUI must use performAction to
|
||||
/// show/hide the inspector UI.
|
||||
pub fn activateInspector(self: *Surface) !void {
|
||||
if (self.inspector != null) return;
|
||||
|
||||
// Setup the inspector
|
||||
var ptr = try self.alloc.create(inspector.Inspector);
|
||||
errdefer self.alloc.destroy(ptr);
|
||||
ptr.* = try inspector.Inspector.init(self);
|
||||
self.inspector = ptr;
|
||||
|
||||
// Put the inspector onto the render state
|
||||
{
|
||||
self.renderer_state.mutex.lock();
|
||||
defer self.renderer_state.mutex.unlock();
|
||||
assert(self.renderer_state.inspector == null);
|
||||
self.renderer_state.inspector = self.inspector;
|
||||
}
|
||||
|
||||
// Notify our components we have an inspector active
|
||||
_ = self.renderer_thread.mailbox.push(.{ .inspector = true }, .{ .forever = {} });
|
||||
_ = self.io_thread.mailbox.push(.{ .inspector = true }, .{ .forever = {} });
|
||||
}
|
||||
|
||||
/// Deactivate the inspector and stop collecting any information.
|
||||
pub fn deactivateInspector(self: *Surface) void {
|
||||
const insp = self.inspector orelse return;
|
||||
|
||||
// Remove the inspector from the render state
|
||||
{
|
||||
self.renderer_state.mutex.lock();
|
||||
defer self.renderer_state.mutex.unlock();
|
||||
assert(self.renderer_state.inspector != null);
|
||||
self.renderer_state.inspector = null;
|
||||
}
|
||||
|
||||
// Notify our components we have deactivated inspector
|
||||
_ = self.renderer_thread.mailbox.push(.{ .inspector = false }, .{ .forever = {} });
|
||||
_ = self.io_thread.mailbox.push(.{ .inspector = false }, .{ .forever = {} });
|
||||
|
||||
// Deinit the inspector
|
||||
insp.deinit();
|
||||
self.alloc.destroy(insp);
|
||||
self.inspector = null;
|
||||
}
|
||||
|
||||
/// True if the surface requires confirmation to quit. This should be called
|
||||
/// by apprt to determine if the surface should confirm before quitting.
|
||||
pub fn needsConfirmQuit(self: *Surface) bool {
|
||||
@ -947,6 +1004,24 @@ pub fn keyCallback(
|
||||
) !bool {
|
||||
// log.debug("keyCallback event={}", .{event});
|
||||
|
||||
// Setup our inspector event if we have an inspector.
|
||||
var insp_ev: ?inspector.key.Event = if (self.inspector != null) ev: {
|
||||
var copy = event;
|
||||
copy.utf8 = "";
|
||||
if (event.utf8.len > 0) copy.utf8 = try self.alloc.dupe(u8, event.utf8);
|
||||
break :ev .{ .event = copy };
|
||||
} else null;
|
||||
|
||||
// When we're done processing, we always want to add the event to
|
||||
// the inspector.
|
||||
defer if (insp_ev) |ev| {
|
||||
if (self.inspector.?.recordKeyEvent(ev)) {
|
||||
self.queueRender() catch {};
|
||||
} else |err| {
|
||||
log.warn("error adding key event to inspector err={}", .{err});
|
||||
}
|
||||
};
|
||||
|
||||
// Before encoding, we see if we have any keybindings for this
|
||||
// key. Those always intercept before any encoding tasks.
|
||||
binding: {
|
||||
@ -984,7 +1059,10 @@ pub fn keyCallback(
|
||||
// If we consume this event, then we are done. If we don't consume
|
||||
// it, we processed the action but we still want to process our
|
||||
// encodings, too.
|
||||
if (consumed and performed) return true;
|
||||
if (consumed and performed) {
|
||||
if (insp_ev) |*ev| ev.binding = binding_action;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// If we allow KAM and KAM is enabled then we do nothing.
|
||||
@ -1030,6 +1108,12 @@ pub fn keyCallback(
|
||||
.len = @intCast(seq.len),
|
||||
},
|
||||
}, .{ .forever = {} });
|
||||
if (insp_ev) |*ev| {
|
||||
ev.pty = self.alloc.dupe(u8, seq) catch |err| err: {
|
||||
log.warn("error copying pty data for inspector err={}", .{err});
|
||||
break :err "";
|
||||
};
|
||||
}
|
||||
try self.io_thread.wakeup.notify();
|
||||
|
||||
// If our event is any keypress that isn't a modifier and we generated
|
||||
@ -1554,6 +1638,36 @@ pub fn mouseButtonCallback(
|
||||
const tracy = trace(@src());
|
||||
defer tracy.end();
|
||||
|
||||
// If we have an inspector, we always queue a render
|
||||
if (self.inspector) |insp| {
|
||||
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 cell = self.renderer_state.terminal.screen.getCell(
|
||||
.viewport,
|
||||
point.y,
|
||||
point.x,
|
||||
);
|
||||
|
||||
insp.cell = .{ .selected = .{
|
||||
.row = point.y,
|
||||
.col = point.x,
|
||||
.cell = cell,
|
||||
} };
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Always record our latest mouse state
|
||||
self.mouse.click_state[@intCast(@intFromEnum(button))] = action;
|
||||
self.mouse.mods = @bitCast(mods);
|
||||
@ -1731,6 +1845,17 @@ pub fn cursorPosCallback(
|
||||
self.renderer_state.mutex.lock();
|
||||
defer self.renderer_state.mutex.unlock();
|
||||
|
||||
// If we have an inspector, we need to always record position information
|
||||
if (self.inspector) |insp| {
|
||||
insp.mouse.last_xpos = pos.x;
|
||||
insp.mouse.last_ypos = pos.y;
|
||||
|
||||
const point = self.posToViewport(pos.x, pos.y);
|
||||
insp.mouse.last_point = point.toScreen(&self.io.terminal.screen);
|
||||
|
||||
try self.queueRender();
|
||||
}
|
||||
|
||||
// Do a mouse report
|
||||
if (self.io.terminal.flags.mouse_event != .none) report: {
|
||||
// Shift overrides mouse "grabbing" in the window, taken from Kitty.
|
||||
@ -2278,6 +2403,12 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
|
||||
} else log.warn("runtime doesn't implement toggleFullscreen", .{});
|
||||
},
|
||||
|
||||
.inspector => |mode| {
|
||||
if (@hasDecl(apprt.Surface, "controlInspector")) {
|
||||
self.rt_surface.controlInspector(mode);
|
||||
} else log.warn("runtime doesn't implement controlInspector", .{});
|
||||
},
|
||||
|
||||
.close_surface => self.close(),
|
||||
|
||||
.close_window => try self.app.closeSurface(self),
|
||||
@ -2413,7 +2544,7 @@ fn completeClipboardReadOSC52(self: *Surface, data: []const u8, kind: u8) !void
|
||||
self.io_thread.wakeup.notify() catch {};
|
||||
}
|
||||
|
||||
const face_ttf = @embedFile("font/res/FiraCode-Regular.ttf");
|
||||
const face_bold_ttf = @embedFile("font/res/FiraCode-Bold.ttf");
|
||||
const face_emoji_ttf = @embedFile("font/res/NotoColorEmoji.ttf");
|
||||
const face_emoji_text_ttf = @embedFile("font/res/NotoEmoji-Regular.ttf");
|
||||
pub const face_ttf = @embedFile("font/res/FiraCode-Regular.ttf");
|
||||
pub const face_bold_ttf = @embedFile("font/res/FiraCode-Bold.ttf");
|
||||
pub const face_emoji_ttf = @embedFile("font/res/NotoColorEmoji.ttf");
|
||||
pub const face_emoji_text_ttf = @embedFile("font/res/NotoEmoji-Regular.ttf");
|
||||
|
@ -15,6 +15,7 @@ const build_config = @import("build_config.zig");
|
||||
pub usingnamespace @import("apprt/structs.zig");
|
||||
pub const glfw = @import("apprt/glfw.zig");
|
||||
pub const gtk = @import("apprt/gtk.zig");
|
||||
pub const none = @import("apprt/none.zig");
|
||||
pub const browser = @import("apprt/browser.zig");
|
||||
pub const embedded = @import("apprt/embedded.zig");
|
||||
pub const surface = @import("apprt/surface.zig");
|
||||
@ -25,7 +26,7 @@ pub const surface = @import("apprt/surface.zig");
|
||||
/// Window or something.
|
||||
pub const runtime = switch (build_config.artifact) {
|
||||
.exe => switch (build_config.app_runtime) {
|
||||
.none => struct {},
|
||||
.none => none,
|
||||
.glfw => glfw,
|
||||
.gtk => gtk,
|
||||
},
|
||||
|
@ -13,6 +13,7 @@ const apprt = @import("../apprt.zig");
|
||||
const input = @import("../input.zig");
|
||||
const terminal = @import("../terminal/main.zig");
|
||||
const CoreApp = @import("../App.zig");
|
||||
const CoreInspector = @import("../inspector/main.zig").Inspector;
|
||||
const CoreSurface = @import("../Surface.zig");
|
||||
const configpkg = @import("../config.zig");
|
||||
const Config = configpkg.Config;
|
||||
@ -73,6 +74,9 @@ pub const App = struct {
|
||||
/// New window with options.
|
||||
new_window: ?*const fn (SurfaceUD, apprt.Surface.Options) callconv(.C) void = null,
|
||||
|
||||
/// Control the inspector visibility
|
||||
control_inspector: ?*const fn (SurfaceUD, input.InspectorMode) callconv(.C) void = null,
|
||||
|
||||
/// Close the current surface given by this function.
|
||||
close_surface: ?*const fn (SurfaceUD, bool) callconv(.C) void = null,
|
||||
|
||||
@ -91,6 +95,9 @@ pub const App = struct {
|
||||
/// Set the initial window size. It is up to the user of libghostty to
|
||||
/// determine if it is the initial window and set this appropriately.
|
||||
set_initial_window_size: ?*const fn (SurfaceUD, u32, u32) callconv(.C) void = null,
|
||||
|
||||
/// Render the inspector for the given surface.
|
||||
render_inspector: ?*const fn (SurfaceUD) callconv(.C) void = null,
|
||||
};
|
||||
|
||||
/// Special values for the goto_tab callback.
|
||||
@ -174,6 +181,11 @@ pub const App = struct {
|
||||
// No-op, we use a threaded interface so we're constantly drawing.
|
||||
}
|
||||
|
||||
pub fn redrawInspector(self: *App, surface: *Surface) void {
|
||||
_ = self;
|
||||
surface.queueInspectorRender();
|
||||
}
|
||||
|
||||
pub fn newWindow(self: *App, parent: ?*CoreSurface) !void {
|
||||
_ = self;
|
||||
|
||||
@ -195,6 +207,7 @@ pub const Surface = struct {
|
||||
cursor_pos: apprt.CursorPos,
|
||||
opts: Options,
|
||||
keymap_state: input.Keymap.State,
|
||||
inspector: ?*Inspector = null,
|
||||
|
||||
pub const Options = extern struct {
|
||||
/// Userdata passed to some of the callbacks.
|
||||
@ -290,6 +303,9 @@ pub const Surface = struct {
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Surface) void {
|
||||
// Shut down our inspector
|
||||
self.freeInspector();
|
||||
|
||||
// Remove ourselves from the list of known surfaces in the app.
|
||||
self.app.core_app.deleteSurface(self);
|
||||
|
||||
@ -297,6 +313,37 @@ pub const Surface = struct {
|
||||
self.core_surface.deinit();
|
||||
}
|
||||
|
||||
/// Initialize the inspector instance. A surface can only have one
|
||||
/// inspector at any given time, so this will return the previous inspector
|
||||
/// if it was already initialized.
|
||||
pub fn initInspector(self: *Surface) !*Inspector {
|
||||
if (self.inspector) |v| return v;
|
||||
|
||||
const alloc = self.app.core_app.alloc;
|
||||
var inspector = try alloc.create(Inspector);
|
||||
errdefer alloc.destroy(inspector);
|
||||
inspector.* = try Inspector.init(self);
|
||||
self.inspector = inspector;
|
||||
return inspector;
|
||||
}
|
||||
|
||||
pub fn freeInspector(self: *Surface) void {
|
||||
if (self.inspector) |v| {
|
||||
v.deinit();
|
||||
self.app.core_app.alloc.destroy(v);
|
||||
self.inspector = null;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn controlInspector(self: *const Surface, mode: input.InspectorMode) void {
|
||||
const func = self.app.opts.control_inspector orelse {
|
||||
log.info("runtime embedder does not support the terminal inspector", .{});
|
||||
return;
|
||||
};
|
||||
|
||||
func(self.opts.userdata, mode);
|
||||
}
|
||||
|
||||
pub fn newSplit(self: *const Surface, direction: input.SplitDirection) !void {
|
||||
const func = self.app.opts.new_split orelse {
|
||||
log.info("runtime embedder does not support splits", .{});
|
||||
@ -762,6 +809,15 @@ pub const Surface = struct {
|
||||
func(self.opts.userdata, width, height);
|
||||
}
|
||||
|
||||
fn queueInspectorRender(self: *const Surface) void {
|
||||
const func = self.app.opts.render_inspector orelse {
|
||||
log.info("runtime embedder does not render_inspector", .{});
|
||||
return;
|
||||
};
|
||||
|
||||
func(self.opts.userdata);
|
||||
}
|
||||
|
||||
fn newSurfaceOptions(self: *const Surface) apprt.Surface.Options {
|
||||
const font_size: u16 = font_size: {
|
||||
if (!self.app.config.@"window-inherit-font-size") break :font_size 0;
|
||||
@ -781,6 +837,252 @@ pub const Surface = struct {
|
||||
}
|
||||
};
|
||||
|
||||
/// Inspector is the state required for the terminal inspector. A terminal
|
||||
/// inspector is 1:1 with a Surface.
|
||||
pub const Inspector = struct {
|
||||
const cimgui = @import("cimgui");
|
||||
|
||||
surface: *Surface,
|
||||
ig_ctx: *cimgui.c.ImGuiContext,
|
||||
backend: ?Backend = null,
|
||||
keymap_state: input.Keymap.State = .{},
|
||||
content_scale: f64 = 1,
|
||||
|
||||
/// Our previous instant used to calculate delta time for animations.
|
||||
instant: ?std.time.Instant = null,
|
||||
|
||||
const Backend = enum {
|
||||
metal,
|
||||
|
||||
pub fn deinit(self: Backend) void {
|
||||
switch (self) {
|
||||
.metal => cimgui.c.ImGui_ImplMetal_Shutdown(),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pub fn init(surface: *Surface) !Inspector {
|
||||
const ig_ctx = cimgui.c.igCreateContext(null);
|
||||
errdefer cimgui.c.igDestroyContext(ig_ctx);
|
||||
cimgui.c.igSetCurrentContext(ig_ctx);
|
||||
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
|
||||
io.BackendPlatformName = "ghostty_embedded";
|
||||
|
||||
// Setup our core inspector
|
||||
CoreInspector.setup();
|
||||
surface.core_surface.activateInspector() catch |err| {
|
||||
log.err("failed to activate inspector err={}", .{err});
|
||||
};
|
||||
|
||||
return .{
|
||||
.surface = surface,
|
||||
.ig_ctx = ig_ctx,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Inspector) void {
|
||||
self.surface.core_surface.deactivateInspector();
|
||||
cimgui.c.igSetCurrentContext(self.ig_ctx);
|
||||
if (self.backend) |v| v.deinit();
|
||||
cimgui.c.igDestroyContext(self.ig_ctx);
|
||||
}
|
||||
|
||||
/// Queue a render for the next frame.
|
||||
pub fn queueRender(self: *Inspector) void {
|
||||
self.surface.queueInspectorRender();
|
||||
}
|
||||
|
||||
/// Initialize the inspector for a metal backend.
|
||||
pub fn initMetal(self: *Inspector, device: objc.Object) bool {
|
||||
defer device.msgSend(void, objc.sel("release"), .{});
|
||||
cimgui.c.igSetCurrentContext(self.ig_ctx);
|
||||
|
||||
if (self.backend) |v| {
|
||||
v.deinit();
|
||||
self.backend = null;
|
||||
}
|
||||
|
||||
if (!cimgui.c.ImGui_ImplMetal_Init(device.value)) {
|
||||
log.warn("failed to initialize metal backend", .{});
|
||||
return false;
|
||||
}
|
||||
self.backend = .metal;
|
||||
|
||||
log.debug("initialized metal backend", .{});
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn renderMetal(
|
||||
self: *Inspector,
|
||||
command_buffer: objc.Object,
|
||||
desc: objc.Object,
|
||||
) !void {
|
||||
defer {
|
||||
command_buffer.msgSend(void, objc.sel("release"), .{});
|
||||
desc.msgSend(void, objc.sel("release"), .{});
|
||||
}
|
||||
assert(self.backend == .metal);
|
||||
//log.debug("render", .{});
|
||||
|
||||
// Setup our imgui frame. We need to render multiple frames to ensure
|
||||
// ImGui completes all its state processing. I don't know how to fix
|
||||
// this.
|
||||
for (0..2) |_| {
|
||||
cimgui.c.ImGui_ImplMetal_NewFrame(desc.value);
|
||||
try self.newFrame();
|
||||
cimgui.c.igNewFrame();
|
||||
|
||||
// Build our UI
|
||||
render: {
|
||||
const surface = &self.surface.core_surface;
|
||||
const inspector = surface.inspector orelse break :render;
|
||||
inspector.render();
|
||||
}
|
||||
|
||||
// Render
|
||||
cimgui.c.igRender();
|
||||
}
|
||||
|
||||
// MTLRenderCommandEncoder
|
||||
const encoder = command_buffer.msgSend(
|
||||
objc.Object,
|
||||
objc.sel("renderCommandEncoderWithDescriptor:"),
|
||||
.{desc.value},
|
||||
);
|
||||
defer encoder.msgSend(void, objc.sel("endEncoding"), .{});
|
||||
cimgui.c.ImGui_ImplMetal_RenderDrawData(
|
||||
cimgui.c.igGetDrawData(),
|
||||
command_buffer.value,
|
||||
encoder.value,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn updateContentScale(self: *Inspector, x: f64, y: f64) void {
|
||||
_ = y;
|
||||
cimgui.c.igSetCurrentContext(self.ig_ctx);
|
||||
|
||||
// Cache our scale because we use it for cursor position calculations.
|
||||
self.content_scale = x;
|
||||
|
||||
// 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, @floatCast(x));
|
||||
const active_style = cimgui.c.igGetStyle();
|
||||
active_style.* = style.*;
|
||||
}
|
||||
|
||||
pub fn updateSize(self: *Inspector, width: u32, height: u32) void {
|
||||
cimgui.c.igSetCurrentContext(self.ig_ctx);
|
||||
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
|
||||
io.DisplaySize = .{ .x = @floatFromInt(width), .y = @floatFromInt(height) };
|
||||
}
|
||||
|
||||
pub fn mouseButtonCallback(
|
||||
self: *Inspector,
|
||||
action: input.MouseButtonState,
|
||||
button: input.MouseButton,
|
||||
mods: input.Mods,
|
||||
) void {
|
||||
_ = mods;
|
||||
|
||||
self.queueRender();
|
||||
cimgui.c.igSetCurrentContext(self.ig_ctx);
|
||||
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
|
||||
|
||||
const imgui_button = switch (button) {
|
||||
.left => cimgui.c.ImGuiMouseButton_Left,
|
||||
.middle => cimgui.c.ImGuiMouseButton_Middle,
|
||||
.right => cimgui.c.ImGuiMouseButton_Right,
|
||||
else => return, // unsupported
|
||||
};
|
||||
|
||||
cimgui.c.ImGuiIO_AddMouseButtonEvent(io, imgui_button, action == .press);
|
||||
}
|
||||
|
||||
pub fn scrollCallback(
|
||||
self: *Inspector,
|
||||
xoff: f64,
|
||||
yoff: f64,
|
||||
mods: input.ScrollMods,
|
||||
) void {
|
||||
_ = mods;
|
||||
|
||||
self.queueRender();
|
||||
cimgui.c.igSetCurrentContext(self.ig_ctx);
|
||||
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
|
||||
cimgui.c.ImGuiIO_AddMouseWheelEvent(
|
||||
io,
|
||||
@floatCast(xoff),
|
||||
@floatCast(yoff),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn cursorPosCallback(self: *Inspector, x: f64, y: f64) void {
|
||||
self.queueRender();
|
||||
cimgui.c.igSetCurrentContext(self.ig_ctx);
|
||||
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
|
||||
cimgui.c.ImGuiIO_AddMousePosEvent(
|
||||
io,
|
||||
@floatCast(x * self.content_scale),
|
||||
@floatCast(y * self.content_scale),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn focusCallback(self: *Inspector, focused: bool) void {
|
||||
self.queueRender();
|
||||
cimgui.c.igSetCurrentContext(self.ig_ctx);
|
||||
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
|
||||
cimgui.c.ImGuiIO_AddFocusEvent(io, focused);
|
||||
}
|
||||
|
||||
pub fn textCallback(self: *Inspector, text: [:0]const u8) void {
|
||||
self.queueRender();
|
||||
cimgui.c.igSetCurrentContext(self.ig_ctx);
|
||||
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
|
||||
cimgui.c.ImGuiIO_AddInputCharactersUTF8(io, text.ptr);
|
||||
}
|
||||
|
||||
pub fn keyCallback(
|
||||
self: *Inspector,
|
||||
action: input.Action,
|
||||
key: input.Key,
|
||||
mods: input.Mods,
|
||||
) !void {
|
||||
self.queueRender();
|
||||
cimgui.c.igSetCurrentContext(self.ig_ctx);
|
||||
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
|
||||
|
||||
// Update all our modifiers
|
||||
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);
|
||||
|
||||
// Send our keypress
|
||||
if (key.imguiKey()) |imgui_key| {
|
||||
cimgui.c.ImGuiIO_AddKeyEvent(
|
||||
io,
|
||||
imgui_key,
|
||||
action == .press or action == .repeat,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn newFrame(self: *Inspector) !void {
|
||||
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
|
||||
|
||||
// Determine our delta time
|
||||
const now = try std.time.Instant.now();
|
||||
io.DeltaTime = if (self.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 (1 / 60);
|
||||
self.instant = now;
|
||||
}
|
||||
};
|
||||
|
||||
// C API
|
||||
pub const CAPI = struct {
|
||||
const global = &@import("../main.zig").state;
|
||||
@ -1047,6 +1349,113 @@ pub const CAPI = struct {
|
||||
};
|
||||
}
|
||||
|
||||
export fn ghostty_surface_inspector(ptr: *Surface) ?*Inspector {
|
||||
return ptr.initInspector() catch |err| {
|
||||
log.err("error initializing inspector err={}", .{err});
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
export fn ghostty_inspector_free(ptr: *Surface) void {
|
||||
ptr.freeInspector();
|
||||
}
|
||||
|
||||
export fn ghostty_inspector_metal_init(ptr: *Inspector, device: objc.c.id) bool {
|
||||
return ptr.initMetal(objc.Object.fromId(device));
|
||||
}
|
||||
|
||||
export fn ghostty_inspector_metal_render(
|
||||
ptr: *Inspector,
|
||||
command_buffer: objc.c.id,
|
||||
descriptor: objc.c.id,
|
||||
) void {
|
||||
return ptr.renderMetal(
|
||||
objc.Object.fromId(command_buffer),
|
||||
objc.Object.fromId(descriptor),
|
||||
) catch |err| {
|
||||
log.err("error rendering inspector err={}", .{err});
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
||||
export fn ghostty_inspector_metal_shutdown(ptr: *Inspector) void {
|
||||
if (ptr.backend) |v| {
|
||||
v.deinit();
|
||||
ptr.backend = null;
|
||||
}
|
||||
}
|
||||
|
||||
export fn ghostty_inspector_set_size(ptr: *Inspector, w: u32, h: u32) void {
|
||||
ptr.updateSize(w, h);
|
||||
}
|
||||
|
||||
export fn ghostty_inspector_set_content_scale(ptr: *Inspector, x: f64, y: f64) void {
|
||||
ptr.updateContentScale(x, y);
|
||||
}
|
||||
|
||||
export fn ghostty_inspector_mouse_button(
|
||||
ptr: *Inspector,
|
||||
action: input.MouseButtonState,
|
||||
button: input.MouseButton,
|
||||
mods: c_int,
|
||||
) void {
|
||||
ptr.mouseButtonCallback(
|
||||
action,
|
||||
button,
|
||||
@bitCast(@as(
|
||||
input.Mods.Backing,
|
||||
@truncate(@as(c_uint, @bitCast(mods))),
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
export fn ghostty_inspector_mouse_pos(ptr: *Inspector, x: f64, y: f64) void {
|
||||
ptr.cursorPosCallback(x, y);
|
||||
}
|
||||
|
||||
export fn ghostty_inspector_mouse_scroll(
|
||||
ptr: *Inspector,
|
||||
x: f64,
|
||||
y: f64,
|
||||
scroll_mods: c_int,
|
||||
) void {
|
||||
ptr.scrollCallback(
|
||||
x,
|
||||
y,
|
||||
@bitCast(@as(u8, @truncate(@as(c_uint, @bitCast(scroll_mods))))),
|
||||
);
|
||||
}
|
||||
|
||||
export fn ghostty_inspector_key(
|
||||
ptr: *Inspector,
|
||||
action: input.Action,
|
||||
key: input.Key,
|
||||
c_mods: c_int,
|
||||
) void {
|
||||
ptr.keyCallback(
|
||||
action,
|
||||
key,
|
||||
@bitCast(@as(
|
||||
input.Mods.Backing,
|
||||
@truncate(@as(c_uint, @bitCast(c_mods))),
|
||||
)),
|
||||
) catch |err| {
|
||||
log.err("error processing key event err={}", .{err});
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
||||
export fn ghostty_inspector_text(
|
||||
ptr: *Inspector,
|
||||
str: [*:0]const u8,
|
||||
) void {
|
||||
ptr.textCallback(std.mem.sliceTo(str, 0));
|
||||
}
|
||||
|
||||
export fn ghostty_inspector_set_focus(ptr: *Inspector, focused: bool) void {
|
||||
ptr.focusCallback(focused);
|
||||
}
|
||||
|
||||
/// Sets the window background blur on macOS to the desired value.
|
||||
/// I do this in Zig as an extern function because I don't know how to
|
||||
/// call these functions in Swift.
|
||||
|
@ -215,6 +215,13 @@ pub const App = struct {
|
||||
@panic("This should never be called for GLFW.");
|
||||
}
|
||||
|
||||
pub fn redrawInspector(self: *App, surface: *Surface) void {
|
||||
_ = self;
|
||||
_ = surface;
|
||||
|
||||
// GLFW doesn't support the inspector
|
||||
}
|
||||
|
||||
fn glfwErrorCallback(code: glfw.ErrorCode, desc: [:0]const u8) void {
|
||||
std.log.warn("glfw error={} message={s}", .{ code, desc });
|
||||
|
||||
|
@ -6,5 +6,6 @@ pub const Surface = @import("gtk/Surface.zig");
|
||||
test {
|
||||
@import("std").testing.refAllDecls(@This());
|
||||
|
||||
_ = @import("gtk/inspector.zig");
|
||||
_ = @import("gtk/key.zig");
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ const Surface = @import("Surface.zig");
|
||||
const Window = @import("Window.zig");
|
||||
const ConfigErrorsWindow = @import("ConfigErrorsWindow.zig");
|
||||
const c = @import("c.zig");
|
||||
const inspector = @import("inspector.zig");
|
||||
const key = @import("key.zig");
|
||||
|
||||
const log = std.log.scoped(.gtk);
|
||||
@ -215,6 +216,7 @@ fn updateConfigErrors(self: *App) !void {
|
||||
fn syncActionAccelerators(self: *App) !void {
|
||||
try self.syncActionAccelerator("app.quit", .{ .quit = {} });
|
||||
try self.syncActionAccelerator("app.reload_config", .{ .reload_config = {} });
|
||||
try self.syncActionAccelerator("app.toggle_inspector", .{ .inspector = .toggle });
|
||||
try self.syncActionAccelerator("win.close", .{ .close_surface = {} });
|
||||
try self.syncActionAccelerator("win.new_window", .{ .new_window = {} });
|
||||
try self.syncActionAccelerator("win.new_tab", .{ .new_tab = {} });
|
||||
@ -277,6 +279,12 @@ pub fn redrawSurface(self: *App, surface: *Surface) void {
|
||||
surface.redraw();
|
||||
}
|
||||
|
||||
/// Redraw the inspector for the given surface.
|
||||
pub fn redrawInspector(self: *App, surface: *Surface) void {
|
||||
_ = self;
|
||||
surface.queueInspectorRender();
|
||||
}
|
||||
|
||||
/// Called by CoreApp to create a new window with a new surface.
|
||||
pub fn newWindow(self: *App, parent_: ?*CoreSurface) !void {
|
||||
const alloc = self.core_app.alloc;
|
||||
@ -458,6 +466,7 @@ fn initMenu(self: *App) void {
|
||||
const section = c.g_menu_new();
|
||||
defer c.g_object_unref(section);
|
||||
c.g_menu_append_section(menu, null, @ptrCast(@alignCast(section)));
|
||||
c.g_menu_append(section, "Terminal Inspector", "win.toggle_inspector");
|
||||
c.g_menu_append(section, "Reload Configuration", "app.reload_config");
|
||||
c.g_menu_append(section, "About Ghostty", "win.about");
|
||||
}
|
||||
|
392
src/apprt/gtk/ImguiWidget.zig
Normal file
392
src/apprt/gtk/ImguiWidget.zig
Normal file
@ -0,0 +1,392 @@
|
||||
const ImguiWidget = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
|
||||
const cimgui = @import("cimgui");
|
||||
const c = @import("c.zig");
|
||||
const key = @import("key.zig");
|
||||
const gl = @import("../../renderer/opengl/main.zig");
|
||||
const input = @import("../../input.zig");
|
||||
|
||||
const log = std.log.scoped(.gtk_imgui_widget);
|
||||
|
||||
/// This is called every frame to populate the ImGui frame.
|
||||
render_callback: ?*const fn (?*anyopaque) void = null,
|
||||
render_userdata: ?*anyopaque = null,
|
||||
|
||||
/// Our OpenGL widget
|
||||
gl_area: *c.GtkGLArea,
|
||||
im_context: *c.GtkIMContext,
|
||||
|
||||
/// ImGui Context
|
||||
ig_ctx: *cimgui.c.ImGuiContext,
|
||||
|
||||
/// Our previous instant used to calculate delta time for animations.
|
||||
instant: ?std.time.Instant = null,
|
||||
|
||||
/// Initialize the widget. This must have a stable pointer for events.
|
||||
pub fn init(self: *ImguiWidget) !void {
|
||||
// Each widget gets its own imgui context so we can have multiple
|
||||
// imgui views in the same application.
|
||||
const ig_ctx = cimgui.c.igCreateContext(null);
|
||||
errdefer cimgui.c.igDestroyContext(ig_ctx);
|
||||
cimgui.c.igSetCurrentContext(ig_ctx);
|
||||
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
|
||||
io.BackendPlatformName = "ghostty_gtk";
|
||||
|
||||
// Our OpenGL area for drawing
|
||||
const gl_area = c.gtk_gl_area_new();
|
||||
c.gtk_gl_area_set_auto_render(@ptrCast(gl_area), 1);
|
||||
|
||||
// The GL area has to be focusable so that it can receive events
|
||||
c.gtk_widget_set_focusable(@ptrCast(gl_area), 1);
|
||||
c.gtk_widget_set_focus_on_click(@ptrCast(gl_area), 1);
|
||||
|
||||
// Clicks
|
||||
const gesture_click = c.gtk_gesture_click_new();
|
||||
errdefer c.g_object_unref(gesture_click);
|
||||
c.gtk_gesture_single_set_button(@ptrCast(gesture_click), 0);
|
||||
c.gtk_widget_add_controller(@ptrCast(gl_area), @ptrCast(gesture_click));
|
||||
|
||||
// Mouse movement
|
||||
const ec_motion = c.gtk_event_controller_motion_new();
|
||||
errdefer c.g_object_unref(ec_motion);
|
||||
c.gtk_widget_add_controller(@ptrCast(gl_area), ec_motion);
|
||||
|
||||
// Scroll events
|
||||
const ec_scroll = c.gtk_event_controller_scroll_new(
|
||||
c.GTK_EVENT_CONTROLLER_SCROLL_BOTH_AXES |
|
||||
c.GTK_EVENT_CONTROLLER_SCROLL_DISCRETE,
|
||||
);
|
||||
errdefer c.g_object_unref(ec_scroll);
|
||||
c.gtk_widget_add_controller(@ptrCast(gl_area), ec_scroll);
|
||||
|
||||
// Focus controller will tell us about focus enter/exit events
|
||||
const ec_focus = c.gtk_event_controller_focus_new();
|
||||
errdefer c.g_object_unref(ec_focus);
|
||||
c.gtk_widget_add_controller(@ptrCast(gl_area), ec_focus);
|
||||
|
||||
// Key event controller will tell us about raw keypress events.
|
||||
const ec_key = c.gtk_event_controller_key_new();
|
||||
errdefer c.g_object_unref(ec_key);
|
||||
c.gtk_widget_add_controller(@ptrCast(gl_area), ec_key);
|
||||
errdefer c.gtk_widget_remove_controller(@ptrCast(gl_area), ec_key);
|
||||
|
||||
// The input method context that we use to translate key events into
|
||||
// characters. This doesn't have an event key controller attached because
|
||||
// we call it manually from our own key controller.
|
||||
const im_context = c.gtk_im_multicontext_new();
|
||||
errdefer c.g_object_unref(im_context);
|
||||
|
||||
// Signals
|
||||
_ = c.g_signal_connect_data(gl_area, "destroy", c.G_CALLBACK(>kDestroy), self, null, c.G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(gl_area, "realize", c.G_CALLBACK(>kRealize), self, null, c.G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(gl_area, "unrealize", c.G_CALLBACK(>kUnrealize), self, null, c.G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(gl_area, "render", c.G_CALLBACK(>kRender), self, null, c.G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(gl_area, "resize", c.G_CALLBACK(>kResize), self, null, c.G_CONNECT_DEFAULT);
|
||||
|
||||
_ = c.g_signal_connect_data(ec_focus, "enter", c.G_CALLBACK(>kFocusEnter), self, null, c.G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(ec_focus, "leave", c.G_CALLBACK(>kFocusLeave), self, null, c.G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(ec_key, "key-pressed", c.G_CALLBACK(>kKeyPressed), self, null, c.G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(ec_key, "key-released", c.G_CALLBACK(>kKeyReleased), self, null, c.G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(ec_motion, "motion", c.G_CALLBACK(>kMouseMotion), self, null, c.G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(ec_scroll, "scroll", c.G_CALLBACK(>kMouseScroll), self, null, c.G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(gesture_click, "pressed", c.G_CALLBACK(>kMouseDown), self, null, c.G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(gesture_click, "released", c.G_CALLBACK(>kMouseUp), self, null, c.G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(im_context, "commit", c.G_CALLBACK(>kInputCommit), self, null, c.G_CONNECT_DEFAULT);
|
||||
|
||||
self.* = .{
|
||||
.gl_area = @ptrCast(gl_area),
|
||||
.im_context = @ptrCast(im_context),
|
||||
.ig_ctx = ig_ctx,
|
||||
};
|
||||
}
|
||||
|
||||
/// Deinitialize the widget. This should ONLY be called if the widget gl_area
|
||||
/// was never added to a parent. Otherwise, cleanup automatically happens
|
||||
/// when the widget is destroyed and this should NOT be called.
|
||||
pub fn deinit(self: *ImguiWidget) void {
|
||||
cimgui.c.igDestroyContext(self.ig_ctx);
|
||||
}
|
||||
|
||||
/// This should be called anytime the underlying data for the UI changes
|
||||
/// so that the UI can be refreshed.
|
||||
pub fn queueRender(self: *const ImguiWidget) void {
|
||||
c.gtk_gl_area_queue_render(self.gl_area);
|
||||
}
|
||||
|
||||
/// Initialize the frame. Expects that the context is already current.
|
||||
fn newFrame(self: *ImguiWidget) !void {
|
||||
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
|
||||
|
||||
// Determine our delta time
|
||||
const now = try std.time.Instant.now();
|
||||
io.DeltaTime = if (self.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 (1 / 60);
|
||||
self.instant = now;
|
||||
}
|
||||
|
||||
fn translateMouseButton(button: c.guint) ?c_int {
|
||||
return switch (button) {
|
||||
1 => cimgui.c.ImGuiMouseButton_Left,
|
||||
2 => cimgui.c.ImGuiMouseButton_Middle,
|
||||
3 => cimgui.c.ImGuiMouseButton_Right,
|
||||
else => null,
|
||||
};
|
||||
}
|
||||
|
||||
fn gtkDestroy(v: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void {
|
||||
_ = v;
|
||||
log.debug("imgui widget destroy", .{});
|
||||
|
||||
const self: *ImguiWidget = @ptrCast(@alignCast(ud.?));
|
||||
self.deinit();
|
||||
}
|
||||
|
||||
fn gtkRealize(area: *c.GtkGLArea, ud: ?*anyopaque) callconv(.C) void {
|
||||
log.debug("gl surface realized", .{});
|
||||
|
||||
// We need to make the context current so we can call GL functions.
|
||||
c.gtk_gl_area_make_current(area);
|
||||
if (c.gtk_gl_area_get_error(area)) |err| {
|
||||
log.err("surface failed to realize: {s}", .{err.*.message});
|
||||
return;
|
||||
}
|
||||
|
||||
// realize means that our OpenGL context is ready, so we can now
|
||||
// initialize the ImgUI OpenGL backend for our context.
|
||||
const self: *ImguiWidget = @ptrCast(@alignCast(ud.?));
|
||||
cimgui.c.igSetCurrentContext(self.ig_ctx);
|
||||
_ = cimgui.c.ImGui_ImplOpenGL3_Init(null);
|
||||
}
|
||||
|
||||
fn gtkUnrealize(area: *c.GtkGLArea, ud: ?*anyopaque) callconv(.C) void {
|
||||
_ = area;
|
||||
log.debug("gl surface unrealized", .{});
|
||||
|
||||
const self: *ImguiWidget = @ptrCast(@alignCast(ud.?));
|
||||
cimgui.c.igSetCurrentContext(self.ig_ctx);
|
||||
cimgui.c.ImGui_ImplOpenGL3_Shutdown();
|
||||
}
|
||||
|
||||
fn gtkResize(area: *c.GtkGLArea, width: c.gint, height: c.gint, ud: ?*anyopaque) callconv(.C) void {
|
||||
const self: *ImguiWidget = @ptrCast(@alignCast(ud.?));
|
||||
cimgui.c.igSetCurrentContext(self.ig_ctx);
|
||||
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
|
||||
const scale_factor = c.gtk_widget_get_scale_factor(@ptrCast(area));
|
||||
log.debug("gl resize width={} height={} scale={}", .{
|
||||
width,
|
||||
height,
|
||||
scale_factor,
|
||||
});
|
||||
|
||||
// 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.*;
|
||||
}
|
||||
|
||||
fn gtkRender(area: *c.GtkGLArea, ctx: *c.GdkGLContext, ud: ?*anyopaque) callconv(.C) c.gboolean {
|
||||
_ = area;
|
||||
_ = ctx;
|
||||
const self: *ImguiWidget = @ptrCast(@alignCast(ud.?));
|
||||
cimgui.c.igSetCurrentContext(self.ig_ctx);
|
||||
|
||||
// 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.c.ImGui_ImplOpenGL3_NewFrame();
|
||||
self.newFrame() catch |err| {
|
||||
log.err("failed to setup frame: {}", .{err});
|
||||
return 0;
|
||||
};
|
||||
cimgui.c.igNewFrame();
|
||||
|
||||
// Build our UI
|
||||
if (self.render_callback) |cb| cb(self.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.c.ImGui_ImplOpenGL3_RenderDrawData(cimgui.c.igGetDrawData());
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
fn gtkMouseMotion(
|
||||
_: *c.GtkEventControllerMotion,
|
||||
x: c.gdouble,
|
||||
y: c.gdouble,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) void {
|
||||
const self: *ImguiWidget = @ptrCast(@alignCast(ud.?));
|
||||
cimgui.c.igSetCurrentContext(self.ig_ctx);
|
||||
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
|
||||
const scale_factor: f64 = @floatFromInt(c.gtk_widget_get_scale_factor(
|
||||
@ptrCast(self.gl_area),
|
||||
));
|
||||
cimgui.c.ImGuiIO_AddMousePosEvent(
|
||||
io,
|
||||
@floatCast(x * scale_factor),
|
||||
@floatCast(y * scale_factor),
|
||||
);
|
||||
self.queueRender();
|
||||
}
|
||||
|
||||
fn gtkMouseDown(
|
||||
gesture: *c.GtkGestureClick,
|
||||
_: c.gint,
|
||||
_: c.gdouble,
|
||||
_: c.gdouble,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) void {
|
||||
const self: *ImguiWidget = @ptrCast(@alignCast(ud.?));
|
||||
self.queueRender();
|
||||
|
||||
cimgui.c.igSetCurrentContext(self.ig_ctx);
|
||||
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
|
||||
const gdk_button = c.gtk_gesture_single_get_current_button(@ptrCast(gesture));
|
||||
if (translateMouseButton(gdk_button)) |button| {
|
||||
cimgui.c.ImGuiIO_AddMouseButtonEvent(io, button, true);
|
||||
}
|
||||
}
|
||||
|
||||
fn gtkMouseUp(
|
||||
gesture: *c.GtkGestureClick,
|
||||
_: c.gint,
|
||||
_: c.gdouble,
|
||||
_: c.gdouble,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) void {
|
||||
const self: *ImguiWidget = @ptrCast(@alignCast(ud.?));
|
||||
self.queueRender();
|
||||
|
||||
cimgui.c.igSetCurrentContext(self.ig_ctx);
|
||||
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
|
||||
const gdk_button = c.gtk_gesture_single_get_current_button(@ptrCast(gesture));
|
||||
if (translateMouseButton(gdk_button)) |button| {
|
||||
cimgui.c.ImGuiIO_AddMouseButtonEvent(io, button, false);
|
||||
}
|
||||
}
|
||||
|
||||
fn gtkMouseScroll(
|
||||
_: *c.GtkEventControllerScroll,
|
||||
x: c.gdouble,
|
||||
y: c.gdouble,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) void {
|
||||
const self: *ImguiWidget = @ptrCast(@alignCast(ud.?));
|
||||
self.queueRender();
|
||||
|
||||
cimgui.c.igSetCurrentContext(self.ig_ctx);
|
||||
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
|
||||
cimgui.c.ImGuiIO_AddMouseWheelEvent(
|
||||
io,
|
||||
@floatCast(x),
|
||||
@floatCast(-y),
|
||||
);
|
||||
}
|
||||
|
||||
fn gtkFocusEnter(_: *c.GtkEventControllerFocus, ud: ?*anyopaque) callconv(.C) void {
|
||||
const self: *ImguiWidget = @ptrCast(@alignCast(ud.?));
|
||||
self.queueRender();
|
||||
|
||||
cimgui.c.igSetCurrentContext(self.ig_ctx);
|
||||
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
|
||||
cimgui.c.ImGuiIO_AddFocusEvent(io, true);
|
||||
}
|
||||
|
||||
fn gtkFocusLeave(_: *c.GtkEventControllerFocus, ud: ?*anyopaque) callconv(.C) void {
|
||||
const self: *ImguiWidget = @ptrCast(@alignCast(ud.?));
|
||||
self.queueRender();
|
||||
|
||||
cimgui.c.igSetCurrentContext(self.ig_ctx);
|
||||
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
|
||||
cimgui.c.ImGuiIO_AddFocusEvent(io, false);
|
||||
}
|
||||
|
||||
fn gtkInputCommit(
|
||||
_: *c.GtkIMContext,
|
||||
bytes: [*:0]u8,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) void {
|
||||
const self: *ImguiWidget = @ptrCast(@alignCast(ud.?));
|
||||
self.queueRender();
|
||||
|
||||
cimgui.c.igSetCurrentContext(self.ig_ctx);
|
||||
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
|
||||
cimgui.c.ImGuiIO_AddInputCharactersUTF8(io, bytes);
|
||||
}
|
||||
|
||||
fn gtkKeyPressed(
|
||||
ec_key: *c.GtkEventControllerKey,
|
||||
keyval: c.guint,
|
||||
keycode: c.guint,
|
||||
gtk_mods: c.GdkModifierType,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) c.gboolean {
|
||||
return if (keyEvent(.press, ec_key, keyval, keycode, gtk_mods, ud)) 1 else 0;
|
||||
}
|
||||
|
||||
fn gtkKeyReleased(
|
||||
ec_key: *c.GtkEventControllerKey,
|
||||
keyval: c.guint,
|
||||
keycode: c.guint,
|
||||
state: c.GdkModifierType,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) c.gboolean {
|
||||
return if (keyEvent(.release, ec_key, keyval, keycode, state, ud)) 1 else 0;
|
||||
}
|
||||
|
||||
fn keyEvent(
|
||||
action: input.Action,
|
||||
ec_key: *c.GtkEventControllerKey,
|
||||
keyval: c.guint,
|
||||
keycode: c.guint,
|
||||
gtk_mods: c.GdkModifierType,
|
||||
ud: ?*anyopaque,
|
||||
) bool {
|
||||
_ = keycode;
|
||||
|
||||
const self: *ImguiWidget = @ptrCast(@alignCast(ud.?));
|
||||
self.queueRender();
|
||||
|
||||
cimgui.c.igSetCurrentContext(self.ig_ctx);
|
||||
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
|
||||
|
||||
// Translate the GTK mods and update the modifiers on every keypress
|
||||
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
|
||||
const event = c.gtk_event_controller_get_current_event(@ptrCast(ec_key));
|
||||
_ = c.gtk_im_context_filter_keypress(self.im_context, event);
|
||||
|
||||
return true;
|
||||
}
|
@ -14,6 +14,7 @@ const CoreSurface = @import("../../Surface.zig");
|
||||
|
||||
const App = @import("App.zig");
|
||||
const Window = @import("Window.zig");
|
||||
const inspector = @import("inspector.zig");
|
||||
const c = @import("c.zig");
|
||||
|
||||
const log = std.log.scoped(.gtk);
|
||||
@ -76,6 +77,9 @@ font_size: ?font.face.DesiredSize = null,
|
||||
size: apprt.SurfaceSize,
|
||||
cursor_pos: apprt.CursorPos,
|
||||
|
||||
/// Inspector state.
|
||||
inspector: ?*inspector.Inspector = null,
|
||||
|
||||
/// Key input states. See gtkKeyPressed for detailed descriptions.
|
||||
in_keypress: bool = false,
|
||||
im_context: *c.GtkIMContext,
|
||||
@ -218,6 +222,9 @@ pub fn deinit(self: *Surface) void {
|
||||
// We don't allocate anything if we aren't realized.
|
||||
if (!self.realized) return;
|
||||
|
||||
// Delete our inspector if we have one
|
||||
self.controlInspector(.hide);
|
||||
|
||||
// Remove ourselves from the list of known surfaces in the app.
|
||||
self.app.core_app.deleteSurface(self);
|
||||
|
||||
@ -235,6 +242,11 @@ fn render(self: *Surface) !void {
|
||||
try self.core_surface.renderer.draw();
|
||||
}
|
||||
|
||||
/// Queue the inspector to render if we have one.
|
||||
pub fn queueInspectorRender(self: *Surface) void {
|
||||
if (self.inspector) |v| v.queueRender();
|
||||
}
|
||||
|
||||
/// Invalidate the surface so that it forces a redraw on the next tick.
|
||||
pub fn redraw(self: *Surface) void {
|
||||
c.gtk_gl_area_queue_render(self.gl_area);
|
||||
@ -281,6 +293,33 @@ pub fn close(self: *Surface, processActive: bool) void {
|
||||
c.gtk_widget_show(alert);
|
||||
}
|
||||
|
||||
pub fn controlInspector(self: *Surface, mode: input.InspectorMode) void {
|
||||
const show = switch (mode) {
|
||||
.toggle => self.inspector == null,
|
||||
.show => true,
|
||||
.hide => false,
|
||||
};
|
||||
|
||||
if (!show) {
|
||||
if (self.inspector) |v| {
|
||||
v.close();
|
||||
self.inspector = null;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// If we already have an inspector, we don't need to show anything.
|
||||
if (self.inspector != null) return;
|
||||
self.inspector = inspector.Inspector.create(
|
||||
self,
|
||||
.{ .window = {} },
|
||||
) catch |err| {
|
||||
log.err("failed to control inspector err={}", .{err});
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
||||
pub fn toggleFullscreen(self: *Surface, mac_non_native: configpkg.NonNativeFullscreen) void {
|
||||
self.window.toggleFullscreen(mac_non_native);
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ const CoreSurface = @import("../../Surface.zig");
|
||||
|
||||
const App = @import("App.zig");
|
||||
const Surface = @import("Surface.zig");
|
||||
const icon = @import("icon.zig");
|
||||
const c = @import("c.zig");
|
||||
|
||||
const log = std.log.scoped(.gtk);
|
||||
@ -28,7 +29,7 @@ notebook: *c.GtkNotebook,
|
||||
|
||||
/// The resources directory for the icon (if any). We need to retain a
|
||||
/// pointer to this because GTK can use it at any time.
|
||||
icon_search_dir: ?[:0]const u8 = null,
|
||||
icon: icon.Icon,
|
||||
|
||||
pub fn create(alloc: Allocator, app: *App) !*Window {
|
||||
// Allocate a fixed pointer for our window. We try to minimize
|
||||
@ -48,6 +49,7 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
// Set up our own state
|
||||
self.* = .{
|
||||
.app = app,
|
||||
.icon = undefined,
|
||||
.window = undefined,
|
||||
.notebook = undefined,
|
||||
};
|
||||
@ -62,28 +64,8 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
|
||||
// If we don't have the icon then we'll try to add our resources dir
|
||||
// to the search path and see if we can find it there.
|
||||
const icon_name = "com.mitchellh.ghostty";
|
||||
const icon_theme = c.gtk_icon_theme_get_for_display(c.gtk_widget_get_display(window));
|
||||
if (c.gtk_icon_theme_has_icon(icon_theme, icon_name) == 0) icon: {
|
||||
const base = self.app.core_app.resources_dir orelse {
|
||||
log.info("gtk app missing Ghostty icon and no resources dir detected", .{});
|
||||
log.info("gtk app will not have Ghostty icon", .{});
|
||||
break :icon;
|
||||
};
|
||||
|
||||
// Note that this method for adding the icon search path is
|
||||
// a fallback mechanism. The recommended mechanism is the
|
||||
// Freedesktop Icon Theme Specification. We distribute a ".desktop"
|
||||
// file in zig-out/share that should be installed to the proper
|
||||
// place.
|
||||
const dir = try std.fmt.allocPrintZ(app.core_app.alloc, "{s}/icons", .{base});
|
||||
self.icon_search_dir = dir;
|
||||
c.gtk_icon_theme_add_search_path(icon_theme, dir.ptr);
|
||||
if (c.gtk_icon_theme_has_icon(icon_theme, icon_name) == 0) {
|
||||
log.warn("Ghostty icon for gtk app not found", .{});
|
||||
}
|
||||
}
|
||||
c.gtk_window_set_icon_name(gtk_window, icon_name);
|
||||
self.icon = try icon.appIcon(self.app, window);
|
||||
c.gtk_window_set_icon_name(gtk_window, self.icon.name);
|
||||
|
||||
// Apply background opacity if we have it
|
||||
if (app.config.@"background-opacity" < 1) {
|
||||
@ -167,6 +149,7 @@ fn initActions(self: *Window) void {
|
||||
.{ "close", >kActionClose },
|
||||
.{ "new_window", >kActionNewWindow },
|
||||
.{ "new_tab", >kActionNewTab },
|
||||
.{ "toggle_inspector", >kActionToggleInspector },
|
||||
};
|
||||
|
||||
inline for (actions) |entry| {
|
||||
@ -185,7 +168,7 @@ fn initActions(self: *Window) void {
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Window) void {
|
||||
if (self.icon_search_dir) |ptr| self.app.core_app.alloc.free(ptr);
|
||||
self.icon.deinit(self.app);
|
||||
}
|
||||
|
||||
/// Add a new tab to this window.
|
||||
@ -564,6 +547,19 @@ fn gtkActionNewTab(
|
||||
};
|
||||
}
|
||||
|
||||
fn gtkActionToggleInspector(
|
||||
_: *c.GSimpleAction,
|
||||
_: *c.GVariant,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) void {
|
||||
const self: *Window = @ptrCast(@alignCast(ud orelse return));
|
||||
const surface = self.actionSurface() orelse return;
|
||||
_ = surface.performBindingAction(.{ .inspector = .toggle }) catch |err| {
|
||||
log.warn("error performing binding action error={}", .{err});
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns the surface to use for an action.
|
||||
fn actionSurface(self: *Window) ?*CoreSurface {
|
||||
const page_idx = c.gtk_notebook_get_current_page(self.notebook);
|
||||
|
52
src/apprt/gtk/icon.zig
Normal file
52
src/apprt/gtk/icon.zig
Normal file
@ -0,0 +1,52 @@
|
||||
const std = @import("std");
|
||||
|
||||
const App = @import("App.zig");
|
||||
const c = @import("c.zig");
|
||||
|
||||
const log = std.log.scoped(.gtk_icon);
|
||||
|
||||
/// An icon. The icon may be associated with some allocated state so when
|
||||
/// the icon is no longer in use it should be deinitialized.
|
||||
pub const Icon = struct {
|
||||
name: [:0]const u8,
|
||||
state: ?[:0]const u8 = null,
|
||||
|
||||
pub fn deinit(self: *const Icon, app: *App) void {
|
||||
if (self.state) |v| app.core_app.alloc.free(v);
|
||||
}
|
||||
};
|
||||
|
||||
/// Returns the application icon that can be used anywhere. This attempts to
|
||||
/// find the icon in the theme and if it can't be found, it is loaded from
|
||||
/// the resources dir. If the resources dir can't be found, we'll log a warning
|
||||
/// and let GTK choose a fallback.
|
||||
pub fn appIcon(app: *App, widget: *c.GtkWidget) !Icon {
|
||||
const icon_name = "com.mitchellh.ghostty";
|
||||
var result: Icon = .{ .name = icon_name };
|
||||
|
||||
// If we don't have the icon then we'll try to add our resources dir
|
||||
// to the search path and see if we can find it there.
|
||||
const icon_theme = c.gtk_icon_theme_get_for_display(c.gtk_widget_get_display(widget));
|
||||
if (c.gtk_icon_theme_has_icon(icon_theme, icon_name) == 0) icon: {
|
||||
const base = app.core_app.resources_dir orelse {
|
||||
log.info("gtk app missing Ghostty icon and no resources dir detected", .{});
|
||||
log.info("gtk app will not have Ghostty icon", .{});
|
||||
break :icon;
|
||||
};
|
||||
|
||||
// Note that this method for adding the icon search path is
|
||||
// a fallback mechanism. The recommended mechanism is the
|
||||
// Freedesktop Icon Theme Specification. We distribute a ".desktop"
|
||||
// file in zig-out/share that should be installed to the proper
|
||||
// place.
|
||||
const dir = try std.fmt.allocPrintZ(app.core_app.alloc, "{s}/icons", .{base});
|
||||
errdefer app.core_app.alloc.free(dir);
|
||||
result.state = dir;
|
||||
c.gtk_icon_theme_add_search_path(icon_theme, dir.ptr);
|
||||
if (c.gtk_icon_theme_has_icon(icon_theme, icon_name) == 0) {
|
||||
log.warn("Ghostty icon for gtk app not found", .{});
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
189
src/apprt/gtk/inspector.zig
Normal file
189
src/apprt/gtk/inspector.zig
Normal file
@ -0,0 +1,189 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const assert = std.debug.assert;
|
||||
|
||||
const App = @import("App.zig");
|
||||
const Surface = @import("Surface.zig");
|
||||
const TerminalWindow = @import("Window.zig");
|
||||
const ImguiWidget = @import("ImguiWidget.zig");
|
||||
const c = @import("c.zig");
|
||||
const icon = @import("icon.zig");
|
||||
const CoreInspector = @import("../../inspector/main.zig").Inspector;
|
||||
|
||||
const log = std.log.scoped(.inspector);
|
||||
|
||||
/// Inspector is the primary stateful object that represents a terminal
|
||||
/// inspector. An inspector is 1:1 with a Surface and is owned by a Surface.
|
||||
/// Closing a surface must close its inspector.
|
||||
pub const Inspector = struct {
|
||||
/// The surface that owns this inspector.
|
||||
surface: *Surface,
|
||||
|
||||
/// The current state of where this inspector is rendered. The Inspector
|
||||
/// is the state of the inspector but this is the state of the GUI.
|
||||
location: LocationState,
|
||||
|
||||
/// This is true if we want to destroy this inspector as soon as the
|
||||
/// location is closed. For example: set this to true, request the
|
||||
/// window be closed, let GTK do its cleanup, then note this to destroy
|
||||
/// the inner state.
|
||||
destroy_on_close: bool = true,
|
||||
|
||||
/// Location where the inspector will be launched.
|
||||
pub const Location = union(LocationKey) {
|
||||
hidden: void,
|
||||
window: void,
|
||||
};
|
||||
|
||||
/// The internal state for each possible location.
|
||||
const LocationState = union(LocationKey) {
|
||||
hidden: void,
|
||||
window: Window,
|
||||
};
|
||||
|
||||
const LocationKey = enum {
|
||||
/// No GUI, but load the inspector state.
|
||||
hidden,
|
||||
|
||||
/// A dedicated window for the inspector.
|
||||
window,
|
||||
};
|
||||
|
||||
/// Create an inspector for the given surface in the given location.
|
||||
pub fn create(surface: *Surface, location: Location) !*Inspector {
|
||||
const alloc = surface.app.core_app.alloc;
|
||||
var ptr = try alloc.create(Inspector);
|
||||
errdefer alloc.destroy(ptr);
|
||||
try ptr.init(surface, location);
|
||||
return ptr;
|
||||
}
|
||||
|
||||
/// Destroy all memory associated with this inspector. You generally
|
||||
/// should NOT call this publicly and should call `close` instead to
|
||||
/// use the GTK lifecycle.
|
||||
pub fn destroy(self: *Inspector) void {
|
||||
assert(self.location == .hidden);
|
||||
const alloc = self.allocator();
|
||||
self.surface.inspector = null;
|
||||
self.deinit();
|
||||
alloc.destroy(self);
|
||||
}
|
||||
|
||||
fn init(self: *Inspector, surface: *Surface, location: Location) !void {
|
||||
self.* = .{
|
||||
.surface = surface,
|
||||
.location = undefined,
|
||||
};
|
||||
|
||||
// Activate the inspector. If it doesn't work we ignore the error
|
||||
// because we can just show an error in the inspector window.
|
||||
self.surface.core_surface.activateInspector() catch |err| {
|
||||
log.err("failed to activate inspector err={}", .{err});
|
||||
};
|
||||
|
||||
switch (location) {
|
||||
.hidden => self.location = .{ .hidden = {} },
|
||||
.window => try self.initWindow(),
|
||||
}
|
||||
}
|
||||
|
||||
fn deinit(self: *Inspector) void {
|
||||
self.surface.core_surface.deactivateInspector();
|
||||
}
|
||||
|
||||
/// Request the inspector is closed.
|
||||
pub fn close(self: *Inspector) void {
|
||||
switch (self.location) {
|
||||
.hidden => self.locationDidClose(),
|
||||
.window => |v| v.close(),
|
||||
}
|
||||
}
|
||||
|
||||
fn locationDidClose(self: *Inspector) void {
|
||||
self.location = .{ .hidden = {} };
|
||||
if (self.destroy_on_close) self.destroy();
|
||||
}
|
||||
|
||||
pub fn queueRender(self: *const Inspector) void {
|
||||
switch (self.location) {
|
||||
.hidden => {},
|
||||
.window => |v| v.imgui_widget.queueRender(),
|
||||
}
|
||||
}
|
||||
|
||||
fn allocator(self: *const Inspector) Allocator {
|
||||
return self.surface.app.core_app.alloc;
|
||||
}
|
||||
|
||||
fn initWindow(self: *Inspector) !void {
|
||||
self.location = .{ .window = undefined };
|
||||
try self.location.window.init(self);
|
||||
}
|
||||
};
|
||||
|
||||
/// A dedicated window to hold an inspector instance.
|
||||
const Window = struct {
|
||||
inspector: *Inspector,
|
||||
window: *c.GtkWindow,
|
||||
icon: icon.Icon,
|
||||
imgui_widget: ImguiWidget,
|
||||
|
||||
pub fn init(self: *Window, inspector: *Inspector) !void {
|
||||
// Initialize to undefined
|
||||
self.* = .{
|
||||
.inspector = inspector,
|
||||
.icon = undefined,
|
||||
.window = undefined,
|
||||
.imgui_widget = undefined,
|
||||
};
|
||||
|
||||
// Create the window
|
||||
const window = c.gtk_application_window_new(inspector.surface.app.app);
|
||||
const gtk_window: *c.GtkWindow = @ptrCast(window);
|
||||
errdefer c.gtk_window_destroy(gtk_window);
|
||||
self.window = gtk_window;
|
||||
c.gtk_window_set_title(gtk_window, "Ghostty: Terminal Inspector");
|
||||
c.gtk_window_set_default_size(gtk_window, 1000, 600);
|
||||
self.icon = try icon.appIcon(self.inspector.surface.app, window);
|
||||
c.gtk_window_set_icon_name(gtk_window, self.icon.name);
|
||||
|
||||
// Initialize our imgui widget
|
||||
try self.imgui_widget.init();
|
||||
errdefer self.imgui_widget.deinit();
|
||||
self.imgui_widget.render_callback = &imguiRender;
|
||||
self.imgui_widget.render_userdata = self;
|
||||
CoreInspector.setup();
|
||||
|
||||
// Signals
|
||||
_ = c.g_signal_connect_data(window, "destroy", c.G_CALLBACK(>kDestroy), self, null, c.G_CONNECT_DEFAULT);
|
||||
|
||||
// Show the window
|
||||
c.gtk_window_set_child(gtk_window, @ptrCast(self.imgui_widget.gl_area));
|
||||
c.gtk_widget_show(window);
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Window) void {
|
||||
self.icon.deinit(self.inspector.surface.app);
|
||||
self.inspector.locationDidClose();
|
||||
}
|
||||
|
||||
pub fn close(self: *const Window) void {
|
||||
c.gtk_window_destroy(self.window);
|
||||
}
|
||||
|
||||
fn imguiRender(ud: ?*anyopaque) void {
|
||||
const self: *Window = @ptrCast(@alignCast(ud orelse return));
|
||||
const surface = &self.inspector.surface.core_surface;
|
||||
const inspector = surface.inspector orelse return;
|
||||
inspector.render();
|
||||
}
|
||||
|
||||
/// "destroy" signal for the window
|
||||
fn gtkDestroy(v: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void {
|
||||
_ = v;
|
||||
log.debug("window destroy", .{});
|
||||
|
||||
const self: *Window = @ptrCast(@alignCast(ud.?));
|
||||
self.deinit();
|
||||
}
|
||||
};
|
2
src/apprt/none.zig
Normal file
2
src/apprt/none.zig
Normal file
@ -0,0 +1,2 @@
|
||||
pub const App = struct {};
|
||||
pub const Surface = struct {};
|
@ -1,8 +1,7 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const trace = @import("tracy").trace;
|
||||
const fastmem = @import("../fastmem.zig");
|
||||
const fastmem = @import("fastmem.zig");
|
||||
|
||||
/// Returns a circular buffer containing type T.
|
||||
pub fn CircBuf(comptime T: type, comptime default: T) type {
|
||||
@ -25,6 +24,29 @@ pub fn CircBuf(comptime T: type, comptime default: T) type {
|
||||
// this minor overhead is not worth optimizing out.
|
||||
full: bool,
|
||||
|
||||
pub const Iterator = struct {
|
||||
buf: Self,
|
||||
idx: usize,
|
||||
direction: Direction,
|
||||
|
||||
pub const Direction = enum { forward, reverse };
|
||||
|
||||
pub fn next(self: *Iterator) ?*T {
|
||||
if (self.idx >= self.buf.len()) return null;
|
||||
|
||||
// Get our index from the tail
|
||||
const tail_idx = switch (self.direction) {
|
||||
.forward => self.idx,
|
||||
.reverse => self.buf.len() - self.idx - 1,
|
||||
};
|
||||
|
||||
// Translate the tail index to a storage index
|
||||
const storage_idx = (self.buf.tail + tail_idx) % self.buf.capacity();
|
||||
self.idx += 1;
|
||||
return &self.buf.storage[storage_idx];
|
||||
}
|
||||
};
|
||||
|
||||
/// Initialize a new circular buffer that can store size elements.
|
||||
pub fn init(alloc: Allocator, size: usize) !Self {
|
||||
var buf = try alloc.alloc(T, size);
|
||||
@ -43,12 +65,35 @@ pub fn CircBuf(comptime T: type, comptime default: T) type {
|
||||
self.* = undefined;
|
||||
}
|
||||
|
||||
/// Append a single value to the buffer. If the buffer is full,
|
||||
/// an error will be returned.
|
||||
pub fn append(self: *Self, v: T) !void {
|
||||
if (self.full) return error.OutOfMemory;
|
||||
self.storage[self.head] = v;
|
||||
self.head += 1;
|
||||
if (self.head >= self.storage.len) self.head = 0;
|
||||
self.full = self.head == self.tail;
|
||||
}
|
||||
|
||||
/// Clear the buffer.
|
||||
pub fn clear(self: *Self) void {
|
||||
self.head = 0;
|
||||
self.tail = 0;
|
||||
self.full = false;
|
||||
}
|
||||
|
||||
/// Iterate over the circular buffer.
|
||||
pub fn iterator(self: Self, direction: Iterator.Direction) Iterator {
|
||||
return Iterator{
|
||||
.buf = self,
|
||||
.idx = 0,
|
||||
.direction = direction,
|
||||
};
|
||||
}
|
||||
|
||||
/// Resize the buffer to the given size (larger or smaller).
|
||||
/// If larger, new values will be set to the default value.
|
||||
pub fn resize(self: *Self, alloc: Allocator, size: usize) !void {
|
||||
const tracy = trace(@src());
|
||||
defer tracy.end();
|
||||
|
||||
// Rotate to zero so it is aligned.
|
||||
try self.rotateToZero(alloc);
|
||||
|
||||
@ -72,9 +117,6 @@ pub fn CircBuf(comptime T: type, comptime default: T) type {
|
||||
|
||||
/// Rotate the data so that it is zero-aligned.
|
||||
fn rotateToZero(self: *Self, alloc: Allocator) !void {
|
||||
const tracy = trace(@src());
|
||||
defer tracy.end();
|
||||
|
||||
// TODO: this does this in the worst possible way by allocating.
|
||||
// rewrite to not allocate, its possible, I'm just lazy right now.
|
||||
|
||||
@ -122,9 +164,6 @@ pub fn CircBuf(comptime T: type, comptime default: T) type {
|
||||
pub fn deleteOldest(self: *Self, n: usize) void {
|
||||
assert(n <= self.storage.len);
|
||||
|
||||
const tracy = trace(@src());
|
||||
defer tracy.end();
|
||||
|
||||
// Clear the values back to default
|
||||
const slices = self.getPtrSlice(0, n);
|
||||
inline for (slices) |slice| @memset(slice, default);
|
||||
@ -141,9 +180,6 @@ pub fn CircBuf(comptime T: type, comptime default: T) type {
|
||||
/// the end of our buffer. This never "rotates" the buffer because
|
||||
/// the offset can only be within the size of the buffer.
|
||||
pub fn getPtrSlice(self: *Self, offset: usize, slice_len: usize) [2][]T {
|
||||
const tracy = trace(@src());
|
||||
defer tracy.end();
|
||||
|
||||
// Note: this assertion is very important, it hints the compiler
|
||||
// which generates ~10% faster code than without it.
|
||||
assert(offset + slice_len <= self.capacity());
|
||||
@ -220,6 +256,115 @@ test {
|
||||
try testing.expectEqual(@as(usize, 0), buf.len());
|
||||
}
|
||||
|
||||
test "append" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
const Buf = CircBuf(u8, 0);
|
||||
var buf = try Buf.init(alloc, 3);
|
||||
defer buf.deinit(alloc);
|
||||
|
||||
try buf.append(1);
|
||||
try buf.append(2);
|
||||
try buf.append(3);
|
||||
try testing.expectError(error.OutOfMemory, buf.append(4));
|
||||
buf.deleteOldest(1);
|
||||
try buf.append(4);
|
||||
try testing.expectError(error.OutOfMemory, buf.append(5));
|
||||
}
|
||||
|
||||
test "forward iterator" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
const Buf = CircBuf(u8, 0);
|
||||
var buf = try Buf.init(alloc, 3);
|
||||
defer buf.deinit(alloc);
|
||||
|
||||
// Empty
|
||||
{
|
||||
var it = buf.iterator(.forward);
|
||||
try testing.expect(it.next() == null);
|
||||
}
|
||||
|
||||
// Partially full
|
||||
try buf.append(1);
|
||||
try buf.append(2);
|
||||
{
|
||||
var it = buf.iterator(.forward);
|
||||
try testing.expect(it.next().?.* == 1);
|
||||
try testing.expect(it.next().?.* == 2);
|
||||
try testing.expect(it.next() == null);
|
||||
}
|
||||
|
||||
// Full
|
||||
try buf.append(3);
|
||||
{
|
||||
var it = buf.iterator(.forward);
|
||||
try testing.expect(it.next().?.* == 1);
|
||||
try testing.expect(it.next().?.* == 2);
|
||||
try testing.expect(it.next().?.* == 3);
|
||||
try testing.expect(it.next() == null);
|
||||
}
|
||||
|
||||
// Delete and add
|
||||
buf.deleteOldest(1);
|
||||
try buf.append(4);
|
||||
{
|
||||
var it = buf.iterator(.forward);
|
||||
try testing.expect(it.next().?.* == 2);
|
||||
try testing.expect(it.next().?.* == 3);
|
||||
try testing.expect(it.next().?.* == 4);
|
||||
try testing.expect(it.next() == null);
|
||||
}
|
||||
}
|
||||
|
||||
test "reverse iterator" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
const Buf = CircBuf(u8, 0);
|
||||
var buf = try Buf.init(alloc, 3);
|
||||
defer buf.deinit(alloc);
|
||||
|
||||
// Empty
|
||||
{
|
||||
var it = buf.iterator(.reverse);
|
||||
try testing.expect(it.next() == null);
|
||||
}
|
||||
|
||||
// Partially full
|
||||
try buf.append(1);
|
||||
try buf.append(2);
|
||||
{
|
||||
var it = buf.iterator(.reverse);
|
||||
try testing.expect(it.next().?.* == 2);
|
||||
try testing.expect(it.next().?.* == 1);
|
||||
try testing.expect(it.next() == null);
|
||||
}
|
||||
|
||||
// Full
|
||||
try buf.append(3);
|
||||
{
|
||||
var it = buf.iterator(.reverse);
|
||||
try testing.expect(it.next().?.* == 3);
|
||||
try testing.expect(it.next().?.* == 2);
|
||||
try testing.expect(it.next().?.* == 1);
|
||||
try testing.expect(it.next() == null);
|
||||
}
|
||||
|
||||
// Delete and add
|
||||
buf.deleteOldest(1);
|
||||
try buf.append(4);
|
||||
{
|
||||
var it = buf.iterator(.reverse);
|
||||
try testing.expect(it.next().?.* == 4);
|
||||
try testing.expect(it.next().?.* == 3);
|
||||
try testing.expect(it.next().?.* == 2);
|
||||
try testing.expect(it.next() == null);
|
||||
}
|
||||
}
|
||||
|
||||
test "getPtrSlice fits" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
@ -8,6 +8,7 @@ pub const keycodes = @import("input/keycodes.zig");
|
||||
pub const kitty = @import("input/kitty.zig");
|
||||
pub const Binding = @import("input/Binding.zig");
|
||||
pub const KeyEncoder = @import("input/KeyEncoder.zig");
|
||||
pub const InspectorMode = Binding.Action.InspectorMode;
|
||||
pub const SplitDirection = Binding.Action.SplitDirection;
|
||||
pub const SplitFocusDirection = Binding.Action.SplitFocusDirection;
|
||||
|
||||
|
@ -176,6 +176,10 @@ pub const Action = union(enum) {
|
||||
/// zoom/unzoom the current split.
|
||||
toggle_split_zoom: void,
|
||||
|
||||
/// Show, hide, or toggle the terminal inspector for the currently
|
||||
/// focused terminal.
|
||||
inspector: InspectorMode,
|
||||
|
||||
/// Reload the configuration. The exact meaning depends on the app runtime
|
||||
/// in use but this usually involves re-reading the configuration file
|
||||
/// and applying any changes. Note that not all changes can be applied at
|
||||
@ -220,6 +224,13 @@ pub const Action = union(enum) {
|
||||
right,
|
||||
};
|
||||
|
||||
// Extern because it is used in the embedded runtime ABI.
|
||||
pub const InspectorMode = enum(c_int) {
|
||||
toggle,
|
||||
show,
|
||||
hide,
|
||||
};
|
||||
|
||||
/// Parse an action in the format of "key=value" where key is the
|
||||
/// action name and value is the action parameter. The parameter
|
||||
/// is optional depending on the action.
|
||||
|
@ -1,5 +1,6 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const cimgui = @import("cimgui");
|
||||
|
||||
/// A generic key input event. This is the information that is necessary
|
||||
/// regardless of apprt in order to generate the proper terminal
|
||||
@ -377,6 +378,137 @@ pub const Key = enum(c_int) {
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns the cimgui key constant for this key.
|
||||
pub fn imguiKey(self: Key) ?c_uint {
|
||||
return switch (self) {
|
||||
.a => cimgui.c.ImGuiKey_A,
|
||||
.b => cimgui.c.ImGuiKey_B,
|
||||
.c => cimgui.c.ImGuiKey_C,
|
||||
.d => cimgui.c.ImGuiKey_D,
|
||||
.e => cimgui.c.ImGuiKey_E,
|
||||
.f => cimgui.c.ImGuiKey_F,
|
||||
.g => cimgui.c.ImGuiKey_G,
|
||||
.h => cimgui.c.ImGuiKey_H,
|
||||
.i => cimgui.c.ImGuiKey_I,
|
||||
.j => cimgui.c.ImGuiKey_J,
|
||||
.k => cimgui.c.ImGuiKey_K,
|
||||
.l => cimgui.c.ImGuiKey_L,
|
||||
.m => cimgui.c.ImGuiKey_M,
|
||||
.n => cimgui.c.ImGuiKey_N,
|
||||
.o => cimgui.c.ImGuiKey_O,
|
||||
.p => cimgui.c.ImGuiKey_P,
|
||||
.q => cimgui.c.ImGuiKey_Q,
|
||||
.r => cimgui.c.ImGuiKey_R,
|
||||
.s => cimgui.c.ImGuiKey_S,
|
||||
.t => cimgui.c.ImGuiKey_T,
|
||||
.u => cimgui.c.ImGuiKey_U,
|
||||
.v => cimgui.c.ImGuiKey_V,
|
||||
.w => cimgui.c.ImGuiKey_W,
|
||||
.x => cimgui.c.ImGuiKey_X,
|
||||
.y => cimgui.c.ImGuiKey_Y,
|
||||
.z => cimgui.c.ImGuiKey_Z,
|
||||
|
||||
.zero => cimgui.c.ImGuiKey_0,
|
||||
.one => cimgui.c.ImGuiKey_1,
|
||||
.two => cimgui.c.ImGuiKey_2,
|
||||
.three => cimgui.c.ImGuiKey_3,
|
||||
.four => cimgui.c.ImGuiKey_4,
|
||||
.five => cimgui.c.ImGuiKey_5,
|
||||
.six => cimgui.c.ImGuiKey_6,
|
||||
.seven => cimgui.c.ImGuiKey_7,
|
||||
.eight => cimgui.c.ImGuiKey_8,
|
||||
.nine => cimgui.c.ImGuiKey_9,
|
||||
|
||||
.semicolon => cimgui.c.ImGuiKey_Semicolon,
|
||||
.space => cimgui.c.ImGuiKey_Space,
|
||||
.apostrophe => cimgui.c.ImGuiKey_Apostrophe,
|
||||
.comma => cimgui.c.ImGuiKey_Comma,
|
||||
.grave_accent => cimgui.c.ImGuiKey_GraveAccent,
|
||||
.period => cimgui.c.ImGuiKey_Period,
|
||||
.slash => cimgui.c.ImGuiKey_Slash,
|
||||
.minus => cimgui.c.ImGuiKey_Minus,
|
||||
.equal => cimgui.c.ImGuiKey_Equal,
|
||||
.left_bracket => cimgui.c.ImGuiKey_LeftBracket,
|
||||
.right_bracket => cimgui.c.ImGuiKey_RightBracket,
|
||||
.backslash => cimgui.c.ImGuiKey_Backslash,
|
||||
|
||||
.up => cimgui.c.ImGuiKey_UpArrow,
|
||||
.down => cimgui.c.ImGuiKey_DownArrow,
|
||||
.left => cimgui.c.ImGuiKey_LeftArrow,
|
||||
.right => cimgui.c.ImGuiKey_RightArrow,
|
||||
.home => cimgui.c.ImGuiKey_Home,
|
||||
.end => cimgui.c.ImGuiKey_End,
|
||||
.insert => cimgui.c.ImGuiKey_Insert,
|
||||
.delete => cimgui.c.ImGuiKey_Delete,
|
||||
.caps_lock => cimgui.c.ImGuiKey_CapsLock,
|
||||
.scroll_lock => cimgui.c.ImGuiKey_ScrollLock,
|
||||
.num_lock => cimgui.c.ImGuiKey_NumLock,
|
||||
.page_up => cimgui.c.ImGuiKey_PageUp,
|
||||
.page_down => cimgui.c.ImGuiKey_PageDown,
|
||||
.escape => cimgui.c.ImGuiKey_Escape,
|
||||
.enter => cimgui.c.ImGuiKey_Enter,
|
||||
.tab => cimgui.c.ImGuiKey_Tab,
|
||||
.backspace => cimgui.c.ImGuiKey_Backspace,
|
||||
.print_screen => cimgui.c.ImGuiKey_PrintScreen,
|
||||
.pause => cimgui.c.ImGuiKey_Pause,
|
||||
|
||||
.f1 => cimgui.c.ImGuiKey_F1,
|
||||
.f2 => cimgui.c.ImGuiKey_F2,
|
||||
.f3 => cimgui.c.ImGuiKey_F3,
|
||||
.f4 => cimgui.c.ImGuiKey_F4,
|
||||
.f5 => cimgui.c.ImGuiKey_F5,
|
||||
.f6 => cimgui.c.ImGuiKey_F6,
|
||||
.f7 => cimgui.c.ImGuiKey_F7,
|
||||
.f8 => cimgui.c.ImGuiKey_F8,
|
||||
.f9 => cimgui.c.ImGuiKey_F9,
|
||||
.f10 => cimgui.c.ImGuiKey_F10,
|
||||
.f11 => cimgui.c.ImGuiKey_F11,
|
||||
.f12 => cimgui.c.ImGuiKey_F12,
|
||||
|
||||
.kp_0 => cimgui.c.ImGuiKey_Keypad0,
|
||||
.kp_1 => cimgui.c.ImGuiKey_Keypad1,
|
||||
.kp_2 => cimgui.c.ImGuiKey_Keypad2,
|
||||
.kp_3 => cimgui.c.ImGuiKey_Keypad3,
|
||||
.kp_4 => cimgui.c.ImGuiKey_Keypad4,
|
||||
.kp_5 => cimgui.c.ImGuiKey_Keypad5,
|
||||
.kp_6 => cimgui.c.ImGuiKey_Keypad6,
|
||||
.kp_7 => cimgui.c.ImGuiKey_Keypad7,
|
||||
.kp_8 => cimgui.c.ImGuiKey_Keypad8,
|
||||
.kp_9 => cimgui.c.ImGuiKey_Keypad9,
|
||||
.kp_decimal => cimgui.c.ImGuiKey_KeypadDecimal,
|
||||
.kp_divide => cimgui.c.ImGuiKey_KeypadDivide,
|
||||
.kp_multiply => cimgui.c.ImGuiKey_KeypadMultiply,
|
||||
.kp_subtract => cimgui.c.ImGuiKey_KeypadSubtract,
|
||||
.kp_add => cimgui.c.ImGuiKey_KeypadAdd,
|
||||
.kp_enter => cimgui.c.ImGuiKey_KeypadEnter,
|
||||
.kp_equal => cimgui.c.ImGuiKey_KeypadEqual,
|
||||
|
||||
.left_shift => cimgui.c.ImGuiKey_LeftShift,
|
||||
.left_control => cimgui.c.ImGuiKey_LeftCtrl,
|
||||
.left_alt => cimgui.c.ImGuiKey_LeftAlt,
|
||||
.left_super => cimgui.c.ImGuiKey_LeftSuper,
|
||||
.right_shift => cimgui.c.ImGuiKey_RightShift,
|
||||
.right_control => cimgui.c.ImGuiKey_RightCtrl,
|
||||
.right_alt => cimgui.c.ImGuiKey_RightAlt,
|
||||
.right_super => cimgui.c.ImGuiKey_RightSuper,
|
||||
|
||||
.invalid,
|
||||
.f13,
|
||||
.f14,
|
||||
.f15,
|
||||
.f16,
|
||||
.f17,
|
||||
.f18,
|
||||
.f19,
|
||||
.f20,
|
||||
.f21,
|
||||
.f22,
|
||||
.f23,
|
||||
.f24,
|
||||
.f25,
|
||||
=> null,
|
||||
};
|
||||
}
|
||||
test "fromASCII should not return keypad keys" {
|
||||
const testing = std.testing;
|
||||
try testing.expect(Key.fromASCII('0').? == .zero);
|
||||
|
1203
src/inspector/Inspector.zig
Normal file
1203
src/inspector/Inspector.zig
Normal file
File diff suppressed because it is too large
Load Diff
95
src/inspector/cursor.zig
Normal file
95
src/inspector/cursor.zig
Normal file
@ -0,0 +1,95 @@
|
||||
const std = @import("std");
|
||||
const cimgui = @import("cimgui");
|
||||
const terminal = @import("../terminal/main.zig");
|
||||
|
||||
/// Render cursor information with a table already open.
|
||||
pub fn renderInTable(cursor: *const terminal.Screen.Cursor) void {
|
||||
{
|
||||
cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0);
|
||||
{
|
||||
_ = cimgui.c.igTableSetColumnIndex(0);
|
||||
cimgui.c.igText("Position (x, y)");
|
||||
}
|
||||
{
|
||||
_ = cimgui.c.igTableSetColumnIndex(1);
|
||||
cimgui.c.igText("(%d, %d)", cursor.x, cursor.y);
|
||||
}
|
||||
}
|
||||
|
||||
if (cursor.pending_wrap) {
|
||||
cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0);
|
||||
{
|
||||
_ = cimgui.c.igTableSetColumnIndex(0);
|
||||
cimgui.c.igText("Pending Wrap");
|
||||
}
|
||||
{
|
||||
_ = cimgui.c.igTableSetColumnIndex(1);
|
||||
cimgui.c.igText("%s", if (cursor.pending_wrap) "true".ptr else "false".ptr);
|
||||
}
|
||||
}
|
||||
|
||||
// If we have a color then we show the color
|
||||
if (cursor.pen.attrs.has_fg) color: {
|
||||
cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0);
|
||||
_ = cimgui.c.igTableSetColumnIndex(0);
|
||||
cimgui.c.igText("Foreground Color");
|
||||
_ = cimgui.c.igTableSetColumnIndex(1);
|
||||
if (!cursor.pen.attrs.has_fg) {
|
||||
cimgui.c.igText("default");
|
||||
break :color;
|
||||
}
|
||||
|
||||
var color: [3]f32 = .{
|
||||
@as(f32, @floatFromInt(cursor.pen.fg.r)) / 255,
|
||||
@as(f32, @floatFromInt(cursor.pen.fg.g)) / 255,
|
||||
@as(f32, @floatFromInt(cursor.pen.fg.b)) / 255,
|
||||
};
|
||||
_ = cimgui.c.igColorEdit3(
|
||||
"color_fg",
|
||||
&color,
|
||||
cimgui.c.ImGuiColorEditFlags_NoPicker |
|
||||
cimgui.c.ImGuiColorEditFlags_NoLabel,
|
||||
);
|
||||
}
|
||||
if (cursor.pen.attrs.has_bg) color: {
|
||||
cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0);
|
||||
_ = cimgui.c.igTableSetColumnIndex(0);
|
||||
cimgui.c.igText("Background Color");
|
||||
_ = cimgui.c.igTableSetColumnIndex(1);
|
||||
if (!cursor.pen.attrs.has_bg) {
|
||||
cimgui.c.igText("default");
|
||||
break :color;
|
||||
}
|
||||
|
||||
var color: [3]f32 = .{
|
||||
@as(f32, @floatFromInt(cursor.pen.bg.r)) / 255,
|
||||
@as(f32, @floatFromInt(cursor.pen.bg.g)) / 255,
|
||||
@as(f32, @floatFromInt(cursor.pen.bg.b)) / 255,
|
||||
};
|
||||
_ = cimgui.c.igColorEdit3(
|
||||
"color_bg",
|
||||
&color,
|
||||
cimgui.c.ImGuiColorEditFlags_NoPicker |
|
||||
cimgui.c.ImGuiColorEditFlags_NoLabel,
|
||||
);
|
||||
}
|
||||
|
||||
// Boolean styles
|
||||
const styles = .{
|
||||
"bold", "italic", "faint", "blink",
|
||||
"inverse", "invisible", "protected", "strikethrough",
|
||||
};
|
||||
inline for (styles) |style| style: {
|
||||
if (!@field(cursor.pen.attrs, style)) break :style;
|
||||
|
||||
cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0);
|
||||
{
|
||||
_ = cimgui.c.igTableSetColumnIndex(0);
|
||||
cimgui.c.igText(style.ptr);
|
||||
}
|
||||
{
|
||||
_ = cimgui.c.igTableSetColumnIndex(1);
|
||||
cimgui.c.igText("true");
|
||||
}
|
||||
}
|
||||
}
|
229
src/inspector/key.zig
Normal file
229
src/inspector/key.zig
Normal file
@ -0,0 +1,229 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const input = @import("../input.zig");
|
||||
const CircBuf = @import("../circ_buf.zig").CircBuf;
|
||||
const cimgui = @import("cimgui");
|
||||
|
||||
/// Circular buffer of key events.
|
||||
pub const EventRing = CircBuf(Event, undefined);
|
||||
|
||||
/// Represents a recorded keyboard event.
|
||||
pub const Event = struct {
|
||||
/// The input event.
|
||||
event: input.KeyEvent,
|
||||
|
||||
/// The binding that was triggered as a result of this event.
|
||||
binding: ?input.Binding.Action = null,
|
||||
|
||||
/// The data sent to the pty as a result of this keyboard event.
|
||||
/// This is allocated using the inspector allocator.
|
||||
pty: []const u8 = "",
|
||||
|
||||
/// State for the inspector GUI. Do not set this unless you're the inspector.
|
||||
imgui_state: struct {
|
||||
selected: bool = false,
|
||||
} = .{},
|
||||
|
||||
pub fn init(alloc: Allocator, event: input.KeyEvent) !Event {
|
||||
var copy = event;
|
||||
copy.utf8 = "";
|
||||
if (event.utf8.len > 0) copy.utf8 = try alloc.dupe(u8, event.utf8);
|
||||
return .{ .event = copy };
|
||||
}
|
||||
|
||||
pub fn deinit(self: *const Event, alloc: Allocator) void {
|
||||
if (self.event.utf8.len > 0) alloc.free(self.event.utf8);
|
||||
if (self.pty.len > 0) alloc.free(self.pty);
|
||||
}
|
||||
|
||||
/// Returns a label that can be used for this event. This is null-terminated
|
||||
/// so it can be easily used with C APIs.
|
||||
pub fn label(self: *const Event, buf: []u8) ![:0]const u8 {
|
||||
var buf_stream = std.io.fixedBufferStream(buf);
|
||||
const writer = buf_stream.writer();
|
||||
|
||||
switch (self.event.action) {
|
||||
.press => try writer.writeAll("Press: "),
|
||||
.release => try writer.writeAll("Release: "),
|
||||
.repeat => try writer.writeAll("Repeat: "),
|
||||
}
|
||||
|
||||
if (self.event.mods.shift) try writer.writeAll("Shift+");
|
||||
if (self.event.mods.ctrl) try writer.writeAll("Ctrl+");
|
||||
if (self.event.mods.alt) try writer.writeAll("Alt+");
|
||||
if (self.event.mods.super) try writer.writeAll("Super+");
|
||||
try writer.writeAll(@tagName(self.event.key));
|
||||
|
||||
// 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.igBeginTable(
|
||||
"##event",
|
||||
2,
|
||||
cimgui.c.ImGuiTableFlags_None,
|
||||
.{ .x = 0, .y = 0 },
|
||||
0,
|
||||
);
|
||||
defer cimgui.c.igEndTable();
|
||||
|
||||
if (self.binding) |binding| {
|
||||
cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0);
|
||||
_ = cimgui.c.igTableSetColumnIndex(0);
|
||||
cimgui.c.igText("Triggered Binding");
|
||||
_ = cimgui.c.igTableSetColumnIndex(1);
|
||||
cimgui.c.igText("%s", @tagName(binding).ptr);
|
||||
}
|
||||
|
||||
pty: {
|
||||
cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0);
|
||||
_ = cimgui.c.igTableSetColumnIndex(0);
|
||||
cimgui.c.igText("Encoding to Pty");
|
||||
_ = cimgui.c.igTableSetColumnIndex(1);
|
||||
if (self.pty.len == 0) {
|
||||
cimgui.c.igTextDisabled("(no data)");
|
||||
break :pty;
|
||||
}
|
||||
|
||||
self.renderPty() catch {
|
||||
cimgui.c.igTextDisabled("(error rendering pty data)");
|
||||
break :pty;
|
||||
};
|
||||
}
|
||||
|
||||
{
|
||||
cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0);
|
||||
_ = cimgui.c.igTableSetColumnIndex(0);
|
||||
cimgui.c.igText("Action");
|
||||
_ = cimgui.c.igTableSetColumnIndex(1);
|
||||
cimgui.c.igText("%s", @tagName(self.event.action).ptr);
|
||||
}
|
||||
{
|
||||
cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0);
|
||||
_ = cimgui.c.igTableSetColumnIndex(0);
|
||||
cimgui.c.igText("Key");
|
||||
_ = cimgui.c.igTableSetColumnIndex(1);
|
||||
cimgui.c.igText("%s", @tagName(self.event.key).ptr);
|
||||
}
|
||||
if (self.event.physical_key != self.event.key) {
|
||||
cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0);
|
||||
_ = cimgui.c.igTableSetColumnIndex(0);
|
||||
cimgui.c.igText("Physical Key");
|
||||
_ = cimgui.c.igTableSetColumnIndex(1);
|
||||
cimgui.c.igText("%s", @tagName(self.event.physical_key).ptr);
|
||||
}
|
||||
if (!self.event.mods.empty()) {
|
||||
cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0);
|
||||
_ = cimgui.c.igTableSetColumnIndex(0);
|
||||
cimgui.c.igText("Mods");
|
||||
_ = cimgui.c.igTableSetColumnIndex(1);
|
||||
if (self.event.mods.shift) cimgui.c.igText("shift ");
|
||||
if (self.event.mods.ctrl) cimgui.c.igText("ctrl ");
|
||||
if (self.event.mods.alt) cimgui.c.igText("alt ");
|
||||
if (self.event.mods.super) cimgui.c.igText("super ");
|
||||
}
|
||||
if (self.event.composing) {
|
||||
cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0);
|
||||
_ = cimgui.c.igTableSetColumnIndex(0);
|
||||
cimgui.c.igText("Composing");
|
||||
_ = cimgui.c.igTableSetColumnIndex(1);
|
||||
cimgui.c.igText("true");
|
||||
}
|
||||
utf8: {
|
||||
cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0);
|
||||
_ = cimgui.c.igTableSetColumnIndex(0);
|
||||
cimgui.c.igText("UTF-8");
|
||||
_ = cimgui.c.igTableSetColumnIndex(1);
|
||||
if (self.event.utf8.len == 0) {
|
||||
cimgui.c.igTextDisabled("(empty)");
|
||||
break :utf8;
|
||||
}
|
||||
|
||||
self.renderUtf8(self.event.utf8) catch {
|
||||
cimgui.c.igTextDisabled("(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.igInputText(
|
||||
"##utf8",
|
||||
&buf,
|
||||
buf_stream.getWritten().len - 1,
|
||||
cimgui.c.ImGuiInputTextFlags_ReadOnly,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
||||
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.igInputText(
|
||||
"##pty",
|
||||
&buf,
|
||||
buf_stream.getWritten().len - 1,
|
||||
cimgui.c.ImGuiInputTextFlags_ReadOnly,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
test "event string" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var event = try Event.init(alloc, .{ .key = .a });
|
||||
defer event.deinit(alloc);
|
||||
|
||||
var buf: [1024]u8 = undefined;
|
||||
try testing.expectEqualStrings("Press: a", try event.label(&buf));
|
||||
}
|
8
src/inspector/main.zig
Normal file
8
src/inspector/main.zig
Normal file
@ -0,0 +1,8 @@
|
||||
pub const cursor = @import("cursor.zig");
|
||||
pub const key = @import("key.zig");
|
||||
pub const termio = @import("termio.zig");
|
||||
pub const Inspector = @import("Inspector.zig");
|
||||
|
||||
test {
|
||||
@import("std").testing.refAllDecls(@This());
|
||||
}
|
343
src/inspector/termio.zig
Normal file
343
src/inspector/termio.zig
Normal file
@ -0,0 +1,343 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const cimgui = @import("cimgui");
|
||||
const terminal = @import("../terminal/main.zig");
|
||||
const CircBuf = @import("../circ_buf.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);
|
||||
|
||||
/// Initiaze 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.ArrayList(u8).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.screen.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: *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: anytype,
|
||||
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("{}", .{action}),
|
||||
}
|
||||
}
|
||||
|
||||
fn encodePrint(writer: anytype, action: terminal.Parser.Action) !void {
|
||||
const ch = action.print;
|
||||
try writer.print("'{u}' (U+{X})", .{ ch, ch });
|
||||
}
|
||||
|
||||
fn encodeExecute(writer: anytype, 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: anytype, 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: anytype, 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: anytype,
|
||||
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 => 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),
|
||||
);
|
||||
},
|
||||
|
||||
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)),
|
||||
),
|
||||
|
||||
else => switch (Value) {
|
||||
u8 => try md.put(
|
||||
key,
|
||||
try std.fmt.allocPrintZ(alloc, "{}", .{value}),
|
||||
),
|
||||
|
||||
[]const u8 => try md.put(key, try alloc.dupeZ(u8, value)),
|
||||
|
||||
else => |T| {
|
||||
@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 = 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,
|
||||
.filter_text = cimgui.c.ImGuiTextFilter_ImGuiTextFilter(""),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *VTHandler) void {
|
||||
cimgui.c.ImGuiTextFilter_destroy(self.filter_text);
|
||||
}
|
||||
|
||||
/// 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;
|
||||
}
|
||||
};
|
@ -283,6 +283,7 @@ pub const GlobalState = struct {
|
||||
}
|
||||
};
|
||||
test {
|
||||
_ = @import("circ_buf.zig");
|
||||
_ = @import("Pty.zig");
|
||||
_ = @import("Command.zig");
|
||||
_ = @import("font/main.zig");
|
||||
@ -294,6 +295,7 @@ test {
|
||||
|
||||
// Libraries
|
||||
_ = @import("segmented_pool.zig");
|
||||
_ = @import("inspector/main.zig");
|
||||
_ = @import("terminal/main.zig");
|
||||
_ = @import("terminfo/main.zig");
|
||||
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const Inspector = @import("../inspector/main.zig").Inspector;
|
||||
const terminal = @import("../terminal/main.zig");
|
||||
const renderer = @import("../renderer.zig");
|
||||
|
||||
@ -14,6 +15,10 @@ mutex: *std.Thread.Mutex,
|
||||
/// The terminal data.
|
||||
terminal: *terminal.Terminal,
|
||||
|
||||
/// The terminal inspector, if any. This will be null while the inspector
|
||||
/// is not active and will be set when it is active.
|
||||
inspector: ?*Inspector = null,
|
||||
|
||||
/// Dead key state. This will render the current dead key preedit text
|
||||
/// over the cursor. This currently only ever renders a single codepoint.
|
||||
/// Preedit can in theory be multiple codepoints long but that is left as
|
||||
|
@ -48,11 +48,6 @@ cursor_h: xev.Timer,
|
||||
cursor_c: xev.Completion = .{},
|
||||
cursor_c_cancel: xev.Completion = .{},
|
||||
|
||||
/// This is true when a blinking cursor should be visible and false
|
||||
/// when it should not be visible. This is toggled on a timer by the
|
||||
/// thread automatically.
|
||||
cursor_blink_visible: bool = false,
|
||||
|
||||
/// The surface we're rendering to.
|
||||
surface: *apprt.Surface,
|
||||
|
||||
@ -69,6 +64,16 @@ mailbox: *Mailbox,
|
||||
/// Mailbox to send messages to the app thread
|
||||
app_mailbox: App.Mailbox,
|
||||
|
||||
flags: packed struct {
|
||||
/// This is true when a blinking cursor should be visible and false
|
||||
/// when it should not be visible. This is toggled on a timer by the
|
||||
/// thread automatically.
|
||||
cursor_blink_visible: bool = false,
|
||||
|
||||
/// This is true when the inspector is active.
|
||||
has_inspector: bool = false,
|
||||
} = .{},
|
||||
|
||||
/// Initialize the thread. This does not START the thread. This only sets
|
||||
/// up all the internal state necessary prior to starting the thread. It
|
||||
/// is up to the caller to start the thread with the threadMain entrypoint.
|
||||
@ -225,7 +230,7 @@ fn drainMailbox(self: *Thread) !void {
|
||||
// If we're focused, we immediately show the cursor again
|
||||
// and then restart the timer.
|
||||
if (self.cursor_c.state() != .active) {
|
||||
self.cursor_blink_visible = true;
|
||||
self.flags.cursor_blink_visible = true;
|
||||
self.cursor_h.run(
|
||||
&self.loop,
|
||||
&self.cursor_c,
|
||||
@ -239,7 +244,7 @@ fn drainMailbox(self: *Thread) !void {
|
||||
},
|
||||
|
||||
.reset_cursor_blink => {
|
||||
self.cursor_blink_visible = true;
|
||||
self.flags.cursor_blink_visible = true;
|
||||
if (self.cursor_c.state() == .active) {
|
||||
self.cursor_h.reset(
|
||||
&self.loop,
|
||||
@ -265,6 +270,8 @@ fn drainMailbox(self: *Thread) !void {
|
||||
defer config.alloc.destroy(config.ptr);
|
||||
try self.renderer.changeConfig(config.ptr);
|
||||
},
|
||||
|
||||
.inspector => |v| self.flags.has_inspector = v,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -322,10 +329,15 @@ fn renderCallback(
|
||||
return .disarm;
|
||||
};
|
||||
|
||||
// If we have an inspector, let the app know we want to rerender that.
|
||||
if (t.flags.has_inspector) {
|
||||
_ = t.app_mailbox.push(.{ .redraw_inspector = t.surface }, .{ .instant = {} });
|
||||
}
|
||||
|
||||
t.renderer.render(
|
||||
t.surface,
|
||||
t.state,
|
||||
t.cursor_blink_visible,
|
||||
t.flags.cursor_blink_visible,
|
||||
) catch |err|
|
||||
log.warn("error rendering err={}", .{err});
|
||||
|
||||
@ -365,7 +377,7 @@ fn cursorTimerCallback(
|
||||
return .disarm;
|
||||
};
|
||||
|
||||
t.cursor_blink_visible = !t.cursor_blink_visible;
|
||||
t.flags.cursor_blink_visible = !t.flags.cursor_blink_visible;
|
||||
t.wakeup.notify() catch {};
|
||||
|
||||
t.cursor_h.run(&t.loop, &t.cursor_c, CURSOR_BLINK_INTERVAL, Thread, t, cursorTimerCallback);
|
||||
|
@ -34,4 +34,7 @@ pub const Message = union(enum) {
|
||||
alloc: Allocator,
|
||||
ptr: *renderer.Renderer.DerivedConfig,
|
||||
},
|
||||
|
||||
/// Activate or deactivate the inspector.
|
||||
inspector: bool,
|
||||
};
|
||||
|
@ -54,6 +54,8 @@ pub const TransitionAction = enum {
|
||||
/// Action is the action that a caller of the parser is expected to
|
||||
/// take as a result of some input character.
|
||||
pub const Action = union(enum) {
|
||||
pub const Tag = std.meta.FieldEnum(Action);
|
||||
|
||||
/// Draw character to the screen. This is a unicode codepoint.
|
||||
print: u21,
|
||||
|
||||
|
@ -62,7 +62,7 @@ const sgr = @import("sgr.zig");
|
||||
const color = @import("color.zig");
|
||||
const kitty = @import("kitty.zig");
|
||||
const point = @import("point.zig");
|
||||
const CircBuf = @import("circ_buf.zig").CircBuf;
|
||||
const CircBuf = @import("../circ_buf.zig").CircBuf;
|
||||
const Selection = @import("Selection.zig");
|
||||
const fastmem = @import("../fastmem.zig");
|
||||
const charsets = @import("charsets.zig");
|
||||
|
@ -136,7 +136,7 @@ pub const MouseFormat = enum(u3) {
|
||||
|
||||
/// Scrolling region is the area of the screen designated where scrolling
|
||||
/// occurs. Wen scrolling the screen, only this viewport is scrolled.
|
||||
const ScrollingRegion = struct {
|
||||
pub const ScrollingRegion = struct {
|
||||
// Top and bottom of the scroll region (0-indexed)
|
||||
// Precondition: top < bottom
|
||||
top: usize,
|
||||
|
@ -120,8 +120,8 @@ pub const Mode = mode_enum: {
|
||||
|
||||
/// The tag type for our enum is a u16 but we use a packed struct
|
||||
/// in order to pack the ansi bit into the tag.
|
||||
const ModeTag = packed struct(u16) {
|
||||
const Backing = @typeInfo(@This()).Struct.backing_integer.?;
|
||||
pub const ModeTag = packed struct(u16) {
|
||||
pub const Backing = @typeInfo(@This()).Struct.backing_integer.?;
|
||||
value: u15,
|
||||
ansi: bool = false,
|
||||
|
||||
|
@ -58,11 +58,29 @@ pub fn Stream(comptime Handler: type) type {
|
||||
// log.debug("char: {c}", .{c});
|
||||
const actions = self.parser.next(c);
|
||||
for (actions) |action_opt| {
|
||||
const action = action_opt orelse continue;
|
||||
|
||||
// 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: {
|
||||
log.warn("error handling action manually err={} action={}", .{
|
||||
err,
|
||||
action,
|
||||
});
|
||||
|
||||
break :err false;
|
||||
};
|
||||
|
||||
if (processed) continue;
|
||||
}
|
||||
|
||||
// if (action_opt) |action| {
|
||||
// if (action != .print)
|
||||
// log.info("action: {}", .{action});
|
||||
// }
|
||||
switch (action_opt orelse continue) {
|
||||
|
||||
switch (action) {
|
||||
.print => |p| if (@hasDecl(T, "print")) try self.handler.print(p),
|
||||
.execute => |code| try self.execute(code),
|
||||
.csi_dispatch => |csi_action| try self.csiDispatch(csi_action),
|
||||
|
@ -1133,43 +1133,58 @@ const ReadThread = struct {
|
||||
// Schedule a render
|
||||
ev.queueRender() catch unreachable;
|
||||
|
||||
// Process the terminal data. This is an extremely hot part of the
|
||||
// terminal emulator, so we do some abstraction leakage to avoid
|
||||
// function calls and unnecessary logic.
|
||||
//
|
||||
// The ground state is the only state that we can see and print/execute
|
||||
// ASCII, so we only execute this hot path if we're already in the ground
|
||||
// state.
|
||||
//
|
||||
// Empirically, this alone improved throughput of large text output by ~20%.
|
||||
var i: usize = 0;
|
||||
const end = buf.len;
|
||||
if (ev.terminal_stream.parser.state == .ground) {
|
||||
for (buf[i..end]) |ch| {
|
||||
switch (terminal.parse_table.table[ch][@intFromEnum(terminal.Parser.State.ground)].action) {
|
||||
// Print, call directly.
|
||||
.print => ev.terminal_stream.handler.print(@intCast(ch)) catch |err|
|
||||
log.err("error processing terminal data: {}", .{err}),
|
||||
// If we have an inspector, we enter SLOW MODE because we need to
|
||||
// process a byte at a time alternating between the inspector handler
|
||||
// and the termio handler. This is very slow compared to our optimizations
|
||||
// below but at least users only pay for it if they're using the inspector.
|
||||
if (ev.renderer_state.inspector) |insp| {
|
||||
for (buf, 0..) |byte, i| {
|
||||
insp.recordPtyRead(buf[i .. i + 1]) catch |err| {
|
||||
log.err("error recording pty read in inspector err={}", .{err});
|
||||
};
|
||||
|
||||
// C0 execute, let our stream handle this one but otherwise
|
||||
// continue since we're guaranteed to be back in ground.
|
||||
.execute => ev.terminal_stream.execute(ch) catch |err|
|
||||
log.err("error processing terminal data: {}", .{err}),
|
||||
|
||||
// Otherwise, break out and go the slow path until we're
|
||||
// back in ground. There is a slight optimization here where
|
||||
// could try to find the next transition to ground but when
|
||||
// I implemented that it didn't materially change performance.
|
||||
else => break,
|
||||
}
|
||||
|
||||
i += 1;
|
||||
ev.terminal_stream.next(byte) catch |err|
|
||||
log.err("error processing terminal data: {}", .{err});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Process the terminal data. This is an extremely hot part of the
|
||||
// terminal emulator, so we do some abstraction leakage to avoid
|
||||
// function calls and unnecessary logic.
|
||||
//
|
||||
// The ground state is the only state that we can see and print/execute
|
||||
// ASCII, so we only execute this hot path if we're already in the ground
|
||||
// state.
|
||||
//
|
||||
// Empirically, this alone improved throughput of large text output by ~20%.
|
||||
var i: usize = 0;
|
||||
const end = buf.len;
|
||||
if (ev.terminal_stream.parser.state == .ground) {
|
||||
for (buf[i..end]) |ch| {
|
||||
switch (terminal.parse_table.table[ch][@intFromEnum(terminal.Parser.State.ground)].action) {
|
||||
// Print, call directly.
|
||||
.print => ev.terminal_stream.handler.print(@intCast(ch)) catch |err|
|
||||
log.err("error processing terminal data: {}", .{err}),
|
||||
|
||||
if (i < end) {
|
||||
ev.terminal_stream.nextSlice(buf[i..end]) catch |err|
|
||||
log.err("error processing terminal data: {}", .{err});
|
||||
// C0 execute, let our stream handle this one but otherwise
|
||||
// continue since we're guaranteed to be back in ground.
|
||||
.execute => ev.terminal_stream.execute(ch) catch |err|
|
||||
log.err("error processing terminal data: {}", .{err}),
|
||||
|
||||
// Otherwise, break out and go the slow path until we're
|
||||
// back in ground. There is a slight optimization here where
|
||||
// could try to find the next transition to ground but when
|
||||
// I implemented that it didn't materially change performance.
|
||||
else => break,
|
||||
}
|
||||
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (i < end) {
|
||||
ev.terminal_stream.nextSlice(buf[i..end]) catch |err|
|
||||
log.err("error processing terminal data: {}", .{err});
|
||||
}
|
||||
}
|
||||
|
||||
// If our stream handling caused messages to be sent to the writer
|
||||
|
@ -62,14 +62,19 @@ sync_reset_cancel_c: xev.Completion = .{},
|
||||
/// The underlying IO implementation.
|
||||
impl: *termio.Impl,
|
||||
|
||||
/// True if linefeed mode is enabled. This is duplicated here so that the
|
||||
/// write thread doesn't need to grab a lock to check this on every write.
|
||||
linefeed_mode: bool = false,
|
||||
|
||||
/// The mailbox that can be used to send this thread messages. Note
|
||||
/// this is a blocking queue so if it is full you will get errors (or block).
|
||||
mailbox: *Mailbox,
|
||||
|
||||
flags: packed struct {
|
||||
/// True if linefeed mode is enabled. This is duplicated here so that the
|
||||
/// write thread doesn't need to grab a lock to check this on every write.
|
||||
linefeed_mode: bool = false,
|
||||
|
||||
/// This is true when the inspector is active.
|
||||
has_inspector: bool = false,
|
||||
} = .{},
|
||||
|
||||
/// Initialize the thread. This does not START the thread. This only sets
|
||||
/// up all the internal state necessary prior to starting the thread. It
|
||||
/// is up to the caller to start the thread with the threadMain entrypoint.
|
||||
@ -174,17 +179,18 @@ fn drainMailbox(self: *Thread) !void {
|
||||
defer config.alloc.destroy(config.ptr);
|
||||
try self.impl.changeConfig(config.ptr);
|
||||
},
|
||||
.inspector => |v| self.flags.has_inspector = v,
|
||||
.resize => |v| self.handleResize(v),
|
||||
.clear_screen => |v| try self.impl.clearScreen(v.history),
|
||||
.scroll_viewport => |v| try self.impl.scrollViewport(v),
|
||||
.jump_to_prompt => |v| try self.impl.jumpToPrompt(v),
|
||||
.start_synchronized_output => self.startSynchronizedOutput(),
|
||||
.linefeed_mode => |v| self.linefeed_mode = v,
|
||||
.write_small => |v| try self.impl.queueWrite(v.data[0..v.len], self.linefeed_mode),
|
||||
.write_stable => |v| try self.impl.queueWrite(v, self.linefeed_mode),
|
||||
.linefeed_mode => |v| self.flags.linefeed_mode = v,
|
||||
.write_small => |v| try self.impl.queueWrite(v.data[0..v.len], self.flags.linefeed_mode),
|
||||
.write_stable => |v| try self.impl.queueWrite(v, self.flags.linefeed_mode),
|
||||
.write_alloc => |v| {
|
||||
defer v.alloc.free(v.data);
|
||||
try self.impl.queueWrite(v.data, self.linefeed_mode);
|
||||
try self.impl.queueWrite(v.data, self.flags.linefeed_mode);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -36,6 +36,9 @@ pub const Message = union(enum) {
|
||||
ptr: *termio.Impl.DerivedConfig,
|
||||
},
|
||||
|
||||
/// Activate or deactivate the inspector.
|
||||
inspector: bool,
|
||||
|
||||
/// Resize the window.
|
||||
resize: Resize,
|
||||
|
||||
|
Reference in New Issue
Block a user