Files
wehub-resource-sync bd44b5aa08
Test / build-libghostty-vt (x86_64-windows-gnu) (push) Has been cancelled
Test / build-libghostty-vt-macos (aarch64-ios) (push) Has been cancelled
Test / build-libghostty-vt-macos (aarch64-macos) (push) Has been cancelled
Test / build-libghostty-vt-macos (x86_64-macos) (push) Has been cancelled
Test / build-nix (namespace-profile-ghostty-md-arm64) (push) Has been cancelled
Test / build-dist (push) Has been cancelled
Test / GTK x11=true wayland=true (push) Has been cancelled
Test / Build -Dsimd=false (push) Has been cancelled
Test / Build -Dsimd=true (push) Has been cancelled
Test / Build -Dsentry=false (push) Has been cancelled
Test / Build -Dsentry=true (push) Has been cancelled
Test / zig-fmt (push) Has been cancelled
Test / GitHub Actions Pins (push) Has been cancelled
Test / prettier (push) Has been cancelled
Test / swiftlint (push) Has been cancelled
Test / alejandra (push) Has been cancelled
Test / typos (push) Has been cancelled
Nix / Required Checks: Nix (push) Has been cancelled
Nix / check-zig-cache-hash (push) Has been cancelled
Test / skip (push) Has been cancelled
Test / Required Checks: Test (push) Has been cancelled
Test / build-bench (push) Has been cancelled
Test / list-examples (push) Has been cancelled
Test / Example ${{ matrix.dir }} (Windows) (push) Has been cancelled
Test / build-cmake (push) Has been cancelled
Test / test-lib-vt-pkgconfig (push) Has been cancelled
Test / build-flatpak (push) Has been cancelled
Test / build-snap (push) Has been cancelled
Test / build-libghostty-vt (aarch64-linux) (push) Has been cancelled
Test / build-libghostty-vt (aarch64-macos) (push) Has been cancelled
Test / build-libghostty-vt (wasm32-freestanding) (push) Has been cancelled
Test / build-libghostty-vt (x86_64-linux) (push) Has been cancelled
Test / build-libghostty-vt (x86_64-linux-musl) (push) Has been cancelled
Test / build-libghostty-vt (x86_64-macos) (push) Has been cancelled
Test / build-libghostty-vt-android (aarch64-linux-android) (push) Has been cancelled
Test / build-libghostty-vt-android (arm-linux-androideabi) (push) Has been cancelled
Test / build-libghostty-vt-android (x86_64-linux-android) (push) Has been cancelled
Test / build-libghostty-vt-windows (push) Has been cancelled
Test / build-libghostty-windows-gnu (push) Has been cancelled
Test / build-linux (namespace-profile-ghostty-md) (push) Has been cancelled
Test / build-linux (namespace-profile-ghostty-md-arm64) (push) Has been cancelled
Test / build-linux-libghostty (push) Has been cancelled
Test / build-nix (namespace-profile-ghostty-md) (push) Has been cancelled
Test / build-dist-lib-vt (push) Has been cancelled
Test / trigger-snap (push) Has been cancelled
Test / trigger-flatpak (push) Has been cancelled
Test / build-macos (push) Has been cancelled
Test / build-macos-freetype (push) Has been cancelled
Test / test (push) Has been cancelled
Test / test-lib-vt (push) Has been cancelled
Test / GTK x11=false wayland=false (push) Has been cancelled
Test / GTK x11=true wayland=false (push) Has been cancelled
Test / GTK x11=false wayland=true (push) Has been cancelled
Test / test-macos (push) Has been cancelled
Test / test-windows (push) Has been cancelled
Test / Build -Di18n=false (push) Has been cancelled
Test / Build -Di18n=true (push) Has been cancelled
Test / Build test/fuzz-libghostty (push) Has been cancelled
Test / shellcheck (push) Has been cancelled
Test / translations (push) Has been cancelled
Test / blueprint-compiler (push) Has been cancelled
Test / Test pkg/wuffs (push) Has been cancelled
Test / Test build on Debian 13 (push) Has been cancelled
Test / valgrind (push) Has been cancelled
Test / Example ${{ matrix.dir }} (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:02:48 +08:00

194 lines
7.2 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 update view model for showing update commands.
var updateViewModel: UpdateViewModel?
/// The callback when an action is submitted.
var onAction: ((String) -> Void)
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
)
.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)
}
}
}
}
/// All commands available in the command palette, combining update and terminal options.
private var commandOptions: [CommandOption] {
var options: [CommandOption] = []
// Updates always appear first
options.append(contentsOf: updateOptions)
// Sort the rest. We replace ":" with a character that sorts before space
// so that "Foo:" sorts before "Foo Bar:". Use sortKey as a tie-breaker
// for stable ordering when titles are equal.
options.append(contentsOf: (jumpOptions + terminalOptions).sorted { a, b in
let aNormalized = a.title.replacingOccurrences(of: ":", with: "\t")
let bNormalized = b.title.replacingOccurrences(of: ":", with: "\t")
let comparison = aNormalized.localizedCaseInsensitiveCompare(bNormalized)
if comparison != .orderedSame {
return comparison == .orderedAscending
}
// Tie-breaker: use sortKey if both have one
if let aSortKey = a.sortKey, let bSortKey = b.sortKey {
return aSortKey < bSortKey
}
return false
})
return options
}
/// Commands for installing or canceling available updates.
private var updateOptions: [CommandOption] {
var options: [CommandOption] = []
guard let updateViewModel, updateViewModel.state.isInstallable else {
return options
}
// We override the update available one only because we want to properly
// convey it'll go all the way through.
let title: String
if case .updateAvailable = updateViewModel.state {
title = "Update Ghostty and Restart"
} else {
title = updateViewModel.text
}
options.append(CommandOption(
title: title,
description: updateViewModel.description,
leadingIcon: updateViewModel.iconName ?? "shippingbox.fill",
badge: updateViewModel.badge,
emphasis: true
) {
(NSApp.delegate as? AppDelegate)?.updateController.installUpdate()
})
options.append(CommandOption(
title: "Cancel or Skip Update",
description: "Dismiss the current update process"
) {
updateViewModel.state.cancel()
})
return options
}
/// Custom commands from the command-palette-entry configuration.
private var terminalOptions: [CommandOption] {
guard let appDelegate = NSApp.delegate as? AppDelegate else { return [] }
return appDelegate.ghostty.config.commandPaletteEntries
.filter(\.isSupported)
.map { c in
let symbols = appDelegate.ghostty.config.keyboardShortcut(for: c.action)?.keyList
return CommandOption(
title: c.title,
description: c.description,
symbols: symbols
) {
onAction(c.action)
}
}
}
/// Commands for jumping to other terminal surfaces.
private var jumpOptions: [CommandOption] {
TerminalController.all.flatMap { controller -> [CommandOption] in
guard let window = controller.window else { return [] }
let color = (window as? TerminalWindow)?.tabColor
let displayColor = color != TerminalTabColor.none ? color : nil
return controller.surfaceTree.map { surface in
let terminalTitle = surface.title.isEmpty ? window.title : surface.title
let displayTitle: String
if let override = controller.titleOverride, !override.isEmpty {
displayTitle = override
} else if !terminalTitle.isEmpty {
displayTitle = terminalTitle
} else {
displayTitle = "Untitled"
}
let pwd = surface.pwd?.abbreviatedPath
let subtitle: String? = if let pwd, !displayTitle.contains(pwd) {
pwd
} else {
nil
}
return CommandOption(
title: "Focus: \(displayTitle)",
subtitle: subtitle,
leadingIcon: "rectangle.on.rectangle",
leadingColor: displayColor?.displayColor.map { Color($0) },
sortKey: AnySortKey(ObjectIdentifier(surface))
) {
NotificationCenter.default.post(
name: Ghostty.Notification.ghosttyPresentTerminal,
object: surface
)
}
}
}
}
}
/// This is done to ensure that the given view is in the responder chain.
private 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) {}
}