macOS: extract TerminalCommandPalette

This commit is contained in:
Mitchell Hashimoto 2025-04-21 08:34:46 -07:00
parent 5fab6faf04
commit 0915a7af46
No known key found for this signature in database
GPG Key ID: 523D5DC389D273BC
4 changed files with 98 additions and 47 deletions

View File

@ -34,9 +34,10 @@
A5333E242B5A22D9008AEFF7 /* Ghostty.Shell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A56D58852ACDDB4100508D2C /* Ghostty.Shell.swift */; };
A53426352A7DA53D00EBB7A2 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53426342A7DA53D00EBB7A2 /* AppDelegate.swift */; };
A535B9DA299C569B0017E2E4 /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A535B9D9299C569B0017E2E4 /* ErrorView.swift */; };
A53A297B2DB2E49700B6E02C /* CommandPalette.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53A297A2DB2E49400B6E02C /* CommandPalette.swift */; };
A53A297F2DB4480F00B6E02C /* EventModifiers+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53A297E2DB4480A00B6E02C /* EventModifiers+Extension.swift */; };
A53A29812DB44A6100B6E02C /* KeyboardShortcut+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53A29802DB44A5E00B6E02C /* KeyboardShortcut+Extension.swift */; };
A53A297B2DB2E49700B6E02C /* CommandPalette.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53A297A2DB2E49400B6E02C /* CommandPalette.swift */; };
A53A29882DB69D2F00B6E02C /* TerminalCommandPalette.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53A29872DB69D2C00B6E02C /* TerminalCommandPalette.swift */; };
A53A6C032CCC1B7F00943E98 /* Ghostty.Action.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53A6C022CCC1B7D00943E98 /* Ghostty.Action.swift */; };
A53D0C8E2B53B0EA00305CE6 /* GhosttyKit.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A5D495A1299BEC7E00DD1313 /* GhosttyKit.xcframework */; };
A53D0C942B53B43700305CE6 /* iOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53D0C932B53B43700305CE6 /* iOSApp.swift */; };
@ -141,9 +142,10 @@
A5333E212B5A2128008AEFF7 /* SurfaceView_AppKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurfaceView_AppKit.swift; sourceTree = "<group>"; };
A53426342A7DA53D00EBB7A2 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
A535B9D9299C569B0017E2E4 /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = "<group>"; };
A53A297A2DB2E49400B6E02C /* CommandPalette.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandPalette.swift; sourceTree = "<group>"; };
A53A297E2DB4480A00B6E02C /* EventModifiers+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EventModifiers+Extension.swift"; sourceTree = "<group>"; };
A53A29802DB44A5E00B6E02C /* KeyboardShortcut+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KeyboardShortcut+Extension.swift"; sourceTree = "<group>"; };
A53A297A2DB2E49400B6E02C /* CommandPalette.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandPalette.swift; sourceTree = "<group>"; };
A53A29872DB69D2C00B6E02C /* TerminalCommandPalette.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalCommandPalette.swift; sourceTree = "<group>"; };
A53A6C022CCC1B7D00943E98 /* Ghostty.Action.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Action.swift; sourceTree = "<group>"; };
A53D0C932B53B43700305CE6 /* iOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSApp.swift; sourceTree = "<group>"; };
A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.App.swift; sourceTree = "<group>"; };
@ -332,6 +334,7 @@
isa = PBXGroup;
children = (
A53A297A2DB2E49400B6E02C /* CommandPalette.swift */,
A53A29872DB69D2C00B6E02C /* TerminalCommandPalette.swift */,
);
path = "Command Palette";
sourceTree = "<group>";
@ -726,6 +729,7 @@
A52FFF572CA90484000C6A5B /* QuickTerminalScreen.swift in Sources */,
A5CC36132C9CD72D004D6760 /* SecureInputOverlay.swift in Sources */,
A535B9DA299C569B0017E2E4 /* ErrorView.swift in Sources */,
A53A29882DB69D2F00B6E02C /* TerminalCommandPalette.swift in Sources */,
A51BFC202B2FB64F00E92F16 /* AboutController.swift in Sources */,
A5CEAFFF29C2410700646FDA /* Backport.swift in Sources */,
A5E112952AF73E8A00C6E0C2 /* ClipboardConfirmationController.swift in Sources */,

View File

@ -0,0 +1,77 @@
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).map { c in
let action = String(cString: c.action)
return CommandOption(
title: String(cString: c.title),
shortcut: ghosttyConfig.keyboardShortcut(for: action)?.description
) {
onAction(action)
}
}
}
var body: some View {
ZStack {
if isPresented {
GeometryReader { geometry in
VStack {
Spacer().frame(height: geometry.size.height * 0.1)
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)
}
}
}
}
}

View File

@ -45,6 +45,9 @@ class BaseTerminalController: NSWindowController,
didSet { surfaceTreeDidChange(from: oldValue, to: surfaceTree) }
}
/// This can be set to show/hide the command palette.
@Published var commandPaletteIsShowing: Bool = false
/// Whether the terminal surface should focus when the mouse is over it.
var focusFollowsMouse: Bool {
self.derivedConfig.focusFollowsMouse
@ -209,12 +212,12 @@ class BaseTerminalController: NSWindowController,
// We only care if the configuration is a global configuration, not a
// surface-specific one.
guard notification.object == nil else { return }
// Get our managed configuration object out
guard let config = notification.userInfo?[
Notification.Name.GhosttyConfigChangeKey
] as? Ghostty.Config else { return }
// Update our derived config
self.derivedConfig = DerivedConfig(config)
}

View File

@ -32,6 +32,9 @@ protocol TerminalViewModel: ObservableObject {
/// The tree of terminal surfaces (splits) within the view. This is mutated by TerminalView
/// and children. This should be @Published.
var surfaceTree: Ghostty.SplitNode? { get set }
/// The command palette state.
var commandPaletteIsShowing: Bool { get set }
}
/// The main terminal view. This terminal view supports splits.
@ -72,27 +75,6 @@ struct TerminalView<ViewModel: TerminalViewModel>: View {
return URL(fileURLWithPath: surfacePwd)
}
// The commands available to the command palette.
private var commandOptions: [CommandOption] {
guard let surface = lastFocusedSurface.value?.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).map { c in
let action = String(cString: c.action)
return CommandOption(
title: String(cString: c.title),
shortcut: ghostty.config.keyEquivalent(for: action)?.description
) {}
}
}
@State var showingCommandPalette = false
var body: some View {
switch ghostty.readiness {
case .loading:
@ -111,7 +93,7 @@ struct TerminalView<ViewModel: TerminalViewModel>: View {
HStack {
Spacer()
Button("Command Palette") {
showingCommandPalette.toggle()
viewModel.commandPaletteIsShowing.toggle()
}
Spacer()
}
@ -154,27 +136,12 @@ struct TerminalView<ViewModel: TerminalViewModel>: View {
// Ignore safe area to extend up in to the titlebar region if we have the "hidden" titlebar style
.ignoresSafeArea(.container, edges: ghostty.config.macosTitlebarStyle == "hidden" ? .top : [])
if showingCommandPalette {
// The Palette View Itself
GeometryReader { geometry in
VStack {
Spacer().frame(height: geometry.size.height * 0.1)
CommandPaletteView(
isPresented: $showingCommandPalette,
backgroundColor: ghostty.config.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)
if let surfaceView = lastFocusedSurface.value {
TerminalCommandPaletteView(
surfaceView: surfaceView,
isPresented: $viewModel.commandPaletteIsShowing,
ghosttyConfig: ghostty.config) { action in
print(action)
}
}
}