mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-15 08:16:13 +03:00

The default keybinds for showing the GTK inspector (`ctrl+shift+i` and `ctrl+shift+d`) don't work reliably in Ghostty due to the way Ghostty handles input. You can show the GTK inspector by setting the environment variable `GTK_DEBUG` to `interactive` before starting Ghostty but that's not always convenient. This adds a keybind action that will show the GTK inspector. Due to API limitations toggling the GTK inspector using the keybind action is impractical because GTK does not provide a convenient API to determine if the GTK inspector is already showing. Thus we limit ourselves to strictly showing the GTK inspector. To close the GTK inspector the user must click the close button on the GTK inspector window. If the GTK inspector window is already visible but is hidden, calling the keybind action will not bring the GTK inspector window to the front.
107 lines
3.8 KiB
Swift
107 lines
3.8 KiB
Swift
import SwiftUI
|
|
import GhosttyKit
|
|
|
|
struct TerminalCommandPaletteView: View {
|
|
/// The surface that this command palette represents.
|
|
let surfaceView: Ghostty.SurfaceView
|
|
|
|
/// Set this to true to show the view, this will be set to false if any actions
|
|
/// result in the view disappearing.
|
|
@Binding var isPresented: Bool
|
|
|
|
/// The configuration so we can lookup keyboard shortcuts.
|
|
@ObservedObject var ghosttyConfig: Ghostty.Config
|
|
|
|
/// The callback when an action is submitted.
|
|
var onAction: ((String) -> Void)
|
|
|
|
// The commands available to the command palette.
|
|
private var commandOptions: [CommandOption] {
|
|
guard let surface = surfaceView.surface else { return [] }
|
|
|
|
var ptr: UnsafeMutablePointer<ghostty_command_s>? = nil
|
|
var count: Int = 0
|
|
ghostty_surface_commands(surface, &ptr, &count)
|
|
guard let ptr else { return [] }
|
|
|
|
let buffer = UnsafeBufferPointer(start: ptr, count: count)
|
|
return Array(buffer).filter { c in
|
|
let key = String(cString: c.action_key)
|
|
switch (key) {
|
|
case "toggle_tab_overview",
|
|
"toggle_window_decorations",
|
|
"show_gtk_inspector":
|
|
return false
|
|
default:
|
|
return true
|
|
}
|
|
}.map { c in
|
|
let action = String(cString: c.action)
|
|
return CommandOption(
|
|
title: String(cString: c.title),
|
|
description: String(cString: c.description),
|
|
symbols: ghosttyConfig.keyboardShortcut(for: action)?.keyList
|
|
) {
|
|
onAction(action)
|
|
}
|
|
}
|
|
}
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
if isPresented {
|
|
GeometryReader { geometry in
|
|
VStack {
|
|
Spacer().frame(height: geometry.size.height * 0.05)
|
|
|
|
ResponderChainInjector(responder: surfaceView)
|
|
.frame(width: 0, height: 0)
|
|
|
|
CommandPaletteView(
|
|
isPresented: $isPresented,
|
|
backgroundColor: ghosttyConfig.backgroundColor,
|
|
options: commandOptions
|
|
)
|
|
.transition(
|
|
.move(edge: .top)
|
|
.combined(with: .opacity)
|
|
.animation(.spring(response: 0.4, dampingFraction: 0.8))
|
|
) // Spring animation
|
|
.zIndex(1) // Ensure it's on top
|
|
|
|
Spacer()
|
|
}
|
|
.frame(width: geometry.size.width, height: geometry.size.height, alignment: .top)
|
|
}
|
|
}
|
|
}
|
|
.onChange(of: isPresented) { newValue in
|
|
// When the command palette disappears we need to send focus back to the
|
|
// surface view we were overlaid on top of. There's probably a better way
|
|
// to handle the first responder state here but I don't know it.
|
|
if !newValue {
|
|
// Has to be on queue because onChange happens on a user-interactive
|
|
// thread and Xcode is mad about this call on that.
|
|
DispatchQueue.main.async {
|
|
surfaceView.window?.makeFirstResponder(surfaceView)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// This is done to ensure that the given view is in the responder chain.
|
|
fileprivate struct ResponderChainInjector: NSViewRepresentable {
|
|
let responder: NSResponder
|
|
|
|
func makeNSView(context: Context) -> NSView {
|
|
let dummy = NSView()
|
|
DispatchQueue.main.async {
|
|
dummy.nextResponder = responder
|
|
}
|
|
return dummy
|
|
}
|
|
|
|
func updateNSView(_ nsView: NSView, context: Context) {}
|
|
}
|