mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-08-24 16:11:43 +00:00
macOS: drop the cross-platform check and abstraction
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -102,7 +102,6 @@
|
||||
"Features/Custom App Icon/Extensions/UserDefaults+AppIcon.swift",
|
||||
Ghostty/Ghostty.ConfigTypes.swift,
|
||||
Ghostty/GhosttyPackageMeta.swift,
|
||||
Helpers/CrossKit.swift,
|
||||
"Helpers/Extensions/NSImage+Extension.swift",
|
||||
"Helpers/Extensions/OSColor+Extension.swift",
|
||||
"Helpers/Extensions/OSPasteboard+Extension.swift",
|
||||
@@ -113,7 +112,6 @@
|
||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||
membershipExceptions = (
|
||||
"Features/Custom App Icon/DockTilePlugin.swift",
|
||||
"Ghostty/Surface View/SurfaceView_UIKit.swift",
|
||||
);
|
||||
target = A5B30530299BEAAA0047F10C /* Ghostty */;
|
||||
};
|
||||
|
||||
@@ -45,7 +45,6 @@ struct AboutView: View {
|
||||
|
||||
private var copyright: String? { Bundle.main.infoDictionary?["NSHumanReadableCopyright"] as? String }
|
||||
|
||||
#if os(macOS)
|
||||
// This creates a background style similar to the Apple "About My Mac" Window
|
||||
private struct VisualEffectBackground: NSViewRepresentable {
|
||||
let material: NSVisualEffectView.Material
|
||||
@@ -72,7 +71,6 @@ struct AboutView: View {
|
||||
return visualEffect
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .center) {
|
||||
@@ -141,9 +139,7 @@ struct AboutView: View {
|
||||
.padding(.top, 8)
|
||||
.padding(32)
|
||||
.frame(minWidth: 256)
|
||||
#if os(macOS)
|
||||
.background(VisualEffectBackground(material: .underWindowBackground).ignoresSafeArea())
|
||||
#endif
|
||||
}
|
||||
|
||||
private struct PropertyRow: View {
|
||||
|
||||
@@ -90,7 +90,7 @@ struct CommandPaletteView: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
let scheme: ColorScheme = if OSColor(backgroundColor).isLightColor {
|
||||
let scheme: ColorScheme = if NSColor(backgroundColor).isLightColor {
|
||||
.light
|
||||
} else {
|
||||
.dark
|
||||
|
||||
@@ -3,7 +3,6 @@ import SwiftUI
|
||||
extension View {
|
||||
/// Returns the ghostty icon to use for views.
|
||||
func ghosttyIconImage() -> Image {
|
||||
#if os(macOS)
|
||||
// Grab the icon from the running application. This is the best way
|
||||
// I've found so far to get the proper icon for our current icon
|
||||
// tinting and so on with macOS Tahoe
|
||||
@@ -15,7 +14,6 @@ extension View {
|
||||
if let nsImage = NSApp.applicationIconImage {
|
||||
return Image(nsImage: nsImage)
|
||||
}
|
||||
#endif
|
||||
|
||||
// Fall back to a static representation
|
||||
return Image("AppIconImage")
|
||||
|
||||
@@ -147,8 +147,8 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow {
|
||||
}
|
||||
|
||||
// Update our window light/darkness based on our updated background color
|
||||
let themeChanged = isLightTheme != OSColor(surfaceConfig.backgroundColor).isLightColor
|
||||
isLightTheme = OSColor(surfaceConfig.backgroundColor).isLightColor
|
||||
let themeChanged = isLightTheme != NSColor(surfaceConfig.backgroundColor).isLightColor
|
||||
isLightTheme = NSColor(surfaceConfig.backgroundColor).isLightColor
|
||||
|
||||
// Update our titlebar color
|
||||
if let preferredBackgroundColor {
|
||||
|
||||
@@ -2,22 +2,15 @@ import SwiftUI
|
||||
import UniformTypeIdentifiers
|
||||
import UserNotifications
|
||||
import GhosttyKit
|
||||
|
||||
#if os(macOS)
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
protocol GhosttyAppDelegate: AnyObject {
|
||||
#if os(macOS)
|
||||
/// Called when a callback needs access to a specific surface. This should return nil
|
||||
/// when the surface is no longer valid.
|
||||
func findSurface(forUUID uuid: UUID) -> Ghostty.SurfaceView?
|
||||
#endif
|
||||
}
|
||||
|
||||
extension Ghostty {
|
||||
// IMPORTANT: THIS IS NOT DONE.
|
||||
// This is a refactor/redo of Ghostty.AppState so that it supports both macOS and iOS
|
||||
class App: ObservableObject {
|
||||
enum Readiness: String {
|
||||
case loading, error, ready
|
||||
@@ -81,8 +74,6 @@ extension Ghostty {
|
||||
return
|
||||
}
|
||||
self.app = app
|
||||
|
||||
#if os(macOS)
|
||||
// Set our initial focus state
|
||||
ghostty_app_set_focus(app, NSApp.isActive)
|
||||
|
||||
@@ -102,18 +93,13 @@ extension Ghostty {
|
||||
selector: #selector(applicationDidResignActive(notification:)),
|
||||
name: NSApplication.didResignActiveNotification,
|
||||
object: nil)
|
||||
#endif
|
||||
|
||||
self.readiness = .ready
|
||||
}
|
||||
|
||||
deinit {
|
||||
// This will force the didSet callbacks to run which free.
|
||||
self.app = nil
|
||||
|
||||
#if os(macOS)
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
#endif
|
||||
}
|
||||
|
||||
// MARK: App Operations
|
||||
@@ -132,7 +118,6 @@ extension Ghostty {
|
||||
func openConfig() {
|
||||
let str = configPath ?? Ghostty.AllocatedString(ghostty_config_open_path()).string
|
||||
guard !str.isEmpty else { return }
|
||||
#if os(macOS)
|
||||
let fileURL = URL(fileURLWithPath: str).absoluteString
|
||||
var action = ghostty_action_open_url_s()
|
||||
action.kind = GHOSTTY_ACTION_OPEN_URL_KIND_TEXT
|
||||
@@ -141,9 +126,6 @@ extension Ghostty {
|
||||
action.len = UInt(fileURL.count)
|
||||
_ = App.openURL(action)
|
||||
}
|
||||
#else
|
||||
fatalError("Unsupported platform for opening config file")
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Reload the configuration.
|
||||
@@ -271,39 +253,6 @@ extension Ghostty {
|
||||
}
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
// MARK: Ghostty Callbacks (iOS)
|
||||
|
||||
static func wakeup(_ userdata: UnsafeMutableRawPointer?) {}
|
||||
static func action(_ app: ghostty_app_t, target: ghostty_target_s, action: ghostty_action_s) -> Bool { return false }
|
||||
static func readClipboard(
|
||||
_ userdata: UnsafeMutableRawPointer?,
|
||||
location: ghostty_clipboard_e,
|
||||
state: UnsafeMutableRawPointer?
|
||||
) -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
static func confirmReadClipboard(
|
||||
_ userdata: UnsafeMutableRawPointer?,
|
||||
string: UnsafePointer<CChar>?,
|
||||
state: UnsafeMutableRawPointer?,
|
||||
request: ghostty_clipboard_request_e
|
||||
) {}
|
||||
|
||||
static func writeClipboard(
|
||||
_ userdata: UnsafeMutableRawPointer?,
|
||||
location: ghostty_clipboard_e,
|
||||
content: UnsafePointer<ghostty_clipboard_content_s>?,
|
||||
len: Int,
|
||||
confirm: Bool
|
||||
) {}
|
||||
|
||||
static func closeSurface(_ userdata: UnsafeMutableRawPointer?, processAlive: Bool) {}
|
||||
#endif
|
||||
|
||||
#if os(macOS)
|
||||
|
||||
// MARK: Notifications
|
||||
|
||||
// Called when the selected keyboard changes. We have to notify Ghostty so that
|
||||
@@ -700,18 +649,8 @@ extension Ghostty {
|
||||
}
|
||||
|
||||
private static func quit(_ app: ghostty_app_t) {
|
||||
// On iOS, applications do not terminate programmatically like they do
|
||||
// on macOS. On iOS, applications are only terminated when a user physically
|
||||
// closes the application (i.e. going to the home screen). If we request
|
||||
// exit on iOS we ignore it.
|
||||
#if os(iOS)
|
||||
logger.info("quit request received, ignoring on iOS")
|
||||
#endif
|
||||
|
||||
#if os(macOS)
|
||||
// We want to quit, start that process
|
||||
NSApplication.shared.terminate(nil)
|
||||
#endif
|
||||
}
|
||||
|
||||
private static func checkForUpdates(
|
||||
@@ -2369,7 +2308,5 @@ extension Ghostty {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,9 +65,6 @@ extension Ghostty {
|
||||
}
|
||||
|
||||
// Load our configuration from files, CLI args, and then any referenced files.
|
||||
// We only do this on macOS because other Apple platforms do not have the
|
||||
// same filesystem concept.
|
||||
#if os(macOS)
|
||||
if let path {
|
||||
ghostty_config_load_file(cfg, path)
|
||||
} else {
|
||||
@@ -81,7 +78,6 @@ extension Ghostty {
|
||||
}
|
||||
|
||||
ghostty_config_load_recursive_files(cfg)
|
||||
#endif
|
||||
|
||||
// TODO: we'd probably do some config loading here... for now we'd
|
||||
// have to do this synchronously. When we support config updating we can do
|
||||
@@ -108,7 +104,6 @@ extension Ghostty {
|
||||
return cfg
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
// MARK: - Keybindings
|
||||
|
||||
/// Return the key equivalent for the given action. The action is the name of the action
|
||||
@@ -122,7 +117,6 @@ extension Ghostty {
|
||||
let trigger = ghostty_config_trigger(cfg, action, UInt(action.lengthOfBytes(using: .utf8)))
|
||||
return Ghostty.keyboardShortcut(for: trigger)
|
||||
}
|
||||
#endif
|
||||
|
||||
// MARK: - Configuration Values
|
||||
|
||||
@@ -287,7 +281,6 @@ extension Ghostty {
|
||||
/// Returns the fullscreen mode if fullscreen is enabled, or nil if disabled.
|
||||
/// This parses the `fullscreen` enum config which supports both
|
||||
/// native and non-native fullscreen modes.
|
||||
#if canImport(AppKit)
|
||||
var windowFullscreen: FullscreenMode? {
|
||||
guard let config = self.config else { return nil }
|
||||
var v: UnsafePointer<Int8>?
|
||||
@@ -310,21 +303,9 @@ extension Ghostty {
|
||||
nil
|
||||
}
|
||||
}
|
||||
#else
|
||||
var windowFullscreen: Bool {
|
||||
guard let config = self.config else { return false }
|
||||
var v: UnsafePointer<Int8>?
|
||||
let key = "fullscreen"
|
||||
guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return false }
|
||||
guard let ptr = v else { return false }
|
||||
let str = String(cString: ptr)
|
||||
return str != "false"
|
||||
}
|
||||
#endif
|
||||
|
||||
/// Returns the fullscreen mode for toggle actions (keybindings).
|
||||
/// This is controlled by `macos-non-native-fullscreen` config.
|
||||
#if canImport(AppKit)
|
||||
var windowFullscreenMode: FullscreenMode {
|
||||
let defaultValue: FullscreenMode = .native
|
||||
guard let config = self.config else { return defaultValue }
|
||||
@@ -346,7 +327,6 @@ extension Ghostty {
|
||||
defaultValue
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
var windowTitleFontFamily: String? {
|
||||
guard let config = self.config else { return nil }
|
||||
@@ -420,7 +400,6 @@ extension Ghostty {
|
||||
}
|
||||
|
||||
var macosCustomIcon: String {
|
||||
#if os(macOS)
|
||||
let defaultValue = NSString("~/.config/ghostty/Ghostty.icns").expandingTildeInPath
|
||||
guard let config = self.config else { return defaultValue }
|
||||
var v: UnsafePointer<Int8>?
|
||||
@@ -429,9 +408,6 @@ extension Ghostty {
|
||||
guard let ptr = v else { return defaultValue }
|
||||
guard let path = NSString(utf8String: ptr) else { return defaultValue }
|
||||
return path.expandingTildeInPath
|
||||
#else
|
||||
return ""
|
||||
#endif
|
||||
}
|
||||
|
||||
var macosIconFrame: MacOSIconFrame {
|
||||
@@ -445,7 +421,7 @@ extension Ghostty {
|
||||
return MacOSIconFrame(rawValue: str) ?? defaultValue
|
||||
}
|
||||
|
||||
var macosIconGhostColor: OSColor? {
|
||||
var macosIconGhostColor: NSColor? {
|
||||
guard let config = self.config else { return nil }
|
||||
var v: ghostty_config_color_s = .init()
|
||||
let key = "macos-icon-ghost-color"
|
||||
@@ -453,7 +429,7 @@ extension Ghostty {
|
||||
return .init(ghostty: v)
|
||||
}
|
||||
|
||||
var macosIconScreenColor: [OSColor]? {
|
||||
var macosIconScreenColor: [NSColor]? {
|
||||
guard let config = self.config else { return nil }
|
||||
var v: ghostty_config_color_list_s = .init()
|
||||
let key = "macos-icon-screen-color"
|
||||
@@ -485,13 +461,7 @@ extension Ghostty {
|
||||
var color: ghostty_config_color_s = .init()
|
||||
let bg_key = "background"
|
||||
if !ghostty_config_get(config, &color, bg_key, UInt(bg_key.lengthOfBytes(using: .utf8))) {
|
||||
#if os(macOS)
|
||||
return Color(NSColor.windowBackgroundColor)
|
||||
#elseif os(iOS)
|
||||
return Color(UIColor.systemBackground)
|
||||
#else
|
||||
#error("unsupported")
|
||||
#endif
|
||||
}
|
||||
|
||||
return .init(
|
||||
@@ -543,7 +513,7 @@ extension Ghostty {
|
||||
}
|
||||
|
||||
var splitDividerColor: Color {
|
||||
let backgroundColor = OSColor(backgroundColor)
|
||||
let backgroundColor = NSColor(backgroundColor)
|
||||
let isLightBackground = backgroundColor.isLightColor
|
||||
let newColor = isLightBackground ? backgroundColor.darken(by: 0.08) : backgroundColor.darken(by: 0.4)
|
||||
|
||||
@@ -562,7 +532,6 @@ extension Ghostty {
|
||||
)
|
||||
}
|
||||
|
||||
#if canImport(AppKit)
|
||||
var quickTerminalPosition: QuickTerminalPosition {
|
||||
guard let config = self.config else { return .top }
|
||||
var v: UnsafePointer<Int8>?
|
||||
@@ -616,7 +585,6 @@ extension Ghostty {
|
||||
guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return QuickTerminalSize() }
|
||||
return QuickTerminalSize(from: v)
|
||||
}
|
||||
#endif
|
||||
|
||||
var resizeOverlay: ResizeOverlay {
|
||||
guard let config = self.config else { return .after_first }
|
||||
|
||||
@@ -518,7 +518,6 @@ extension Ghostty.Input.Momentum: AppEnum {
|
||||
]
|
||||
}
|
||||
|
||||
#if canImport(AppKit)
|
||||
import AppKit
|
||||
|
||||
extension Ghostty.Input.Momentum {
|
||||
@@ -535,7 +534,6 @@ extension Ghostty.Input.Momentum {
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// MARK: Ghostty.Input.Mods
|
||||
|
||||
|
||||
@@ -216,7 +216,6 @@ extension Ghostty {
|
||||
}
|
||||
}
|
||||
|
||||
#if canImport(AppKit)
|
||||
// MARK: SplitFocusDirection Extensions
|
||||
|
||||
extension Ghostty.SplitFocusDirection {
|
||||
@@ -243,7 +242,6 @@ extension Ghostty.SplitFocusDirection {
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
extension Ghostty {
|
||||
struct ClipboardContent {
|
||||
|
||||
@@ -3,7 +3,7 @@ import GhosttyKit
|
||||
import SwiftUI
|
||||
|
||||
extension Ghostty {
|
||||
class OSSurfaceView: OSView, ObservableObject {
|
||||
class OSSurfaceView: NSView, ObservableObject {
|
||||
typealias ID = UUID
|
||||
|
||||
/// Unique ID per surface
|
||||
@@ -129,7 +129,7 @@ extension Ghostty.OSSurfaceView {
|
||||
/// The pasteboard used to persist the search needle.
|
||||
///
|
||||
/// The `.find` pasteboard lets us sync our needle across the system and other find bars.
|
||||
private let pasteboard: OSPasteboard
|
||||
private let pasteboard: NSPasteboard
|
||||
|
||||
@Published var needle = Needle.empty
|
||||
|
||||
@@ -138,7 +138,7 @@ extension Ghostty.OSSurfaceView {
|
||||
|
||||
init(
|
||||
from startSearch: Ghostty.Action.StartSearch,
|
||||
pasteboard: OSPasteboard = OSPasteboard.find
|
||||
pasteboard: NSPasteboard = NSPasteboard.find
|
||||
) {
|
||||
self.pasteboard = pasteboard
|
||||
if let needle = startSearch.needle, !needle.isEmpty {
|
||||
@@ -173,9 +173,7 @@ extension Ghostty.OSSurfaceView {
|
||||
guard let surface = self.surface else { return false }
|
||||
let action = "navigate_search:next"
|
||||
if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) {
|
||||
#if canImport(AppKit)
|
||||
AppDelegate.logger.warning("action failed action=\(action, privacy: .public)")
|
||||
#endif
|
||||
return false
|
||||
}
|
||||
return true
|
||||
@@ -185,9 +183,7 @@ extension Ghostty.OSSurfaceView {
|
||||
guard let surface = self.surface else { return false }
|
||||
let action = "navigate_search:previous"
|
||||
if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) {
|
||||
#if canImport(AppKit)
|
||||
AppDelegate.logger.warning("action failed action=\(action, privacy: .public)")
|
||||
#endif
|
||||
return false
|
||||
}
|
||||
return true
|
||||
|
||||
@@ -186,7 +186,7 @@ class SurfaceScrollView: NSView {
|
||||
private func synchronizeAppearance() {
|
||||
let scrollbarConfig = surfaceView.derivedConfig.scrollbar
|
||||
scrollView.hasVerticalScroller = scrollbarConfig != .never
|
||||
let hasLightBackground = OSColor(surfaceView.derivedConfig.backgroundColor).isLightColor
|
||||
let hasLightBackground = NSColor(surfaceView.derivedConfig.backgroundColor).isLightColor
|
||||
// Make sure the scroller’s appearance matches the surface's background color.
|
||||
scrollView.appearance = NSAppearance(named: hasLightBackground ? .aqua : .darkAqua)
|
||||
updateTrackingAreas()
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
#if canImport(AppKit)
|
||||
import AppKit
|
||||
#elseif canImport(UIKit)
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
extension Ghostty.SurfaceView {
|
||||
#if canImport(AppKit)
|
||||
/// A snapshot image of the current surface view.
|
||||
var asImage: NSImage? {
|
||||
guard let bitmapRep = bitmapImageRepForCachingDisplay(in: bounds) else {
|
||||
@@ -16,13 +11,4 @@ extension Ghostty.SurfaceView {
|
||||
image.addRepresentation(bitmapRep)
|
||||
return image
|
||||
}
|
||||
#elseif canImport(UIKit)
|
||||
/// A snapshot image of the current surface view.
|
||||
var asImage: UIImage? {
|
||||
let renderer = UIGraphicsImageRenderer(bounds: bounds)
|
||||
return renderer.image { _ in
|
||||
drawHierarchy(in: bounds, afterScreenUpdates: true)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
#if canImport(AppKit)
|
||||
import AppKit
|
||||
#endif
|
||||
import CoreTransferable
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
@@ -32,15 +30,8 @@ extension Ghostty.SurfaceView: Transferable {
|
||||
|
||||
@MainActor
|
||||
static func find(uuid: UUID) -> Self? {
|
||||
#if canImport(AppKit)
|
||||
guard let del = NSApp.delegate as? Ghostty.Delegate else { return nil }
|
||||
return del.ghosttySurface(id: uuid) as? Self
|
||||
#elseif canImport(UIKit)
|
||||
// We should be able to use UIApplication here.
|
||||
return nil
|
||||
#else
|
||||
return nil
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,9 +41,7 @@ extension UTType {
|
||||
static let ghosttySurfaceId = UTType(exportedAs: "com.mitchellh.ghosttySurfaceId")
|
||||
}
|
||||
|
||||
#if canImport(AppKit)
|
||||
extension NSPasteboard.PasteboardType {
|
||||
/// Pasteboard type for dragging surface IDs.
|
||||
static let ghosttySurfaceId = NSPasteboard.PasteboardType(UTType.ghosttySurfaceId.identifier)
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -19,10 +19,8 @@ extension Ghostty {
|
||||
// Maintain whether our window has focus (is key) or not
|
||||
@State private var windowFocus: Bool = true
|
||||
|
||||
#if canImport(AppKit)
|
||||
// Observe SecureInput to detect when its enabled
|
||||
@ObservedObject private var secureInput = SecureInput.shared
|
||||
#endif
|
||||
|
||||
@EnvironmentObject private var ghostty: Ghostty.App
|
||||
@Environment(\.ghosttyLastFocusedSurface) private var lastFocusedSurface
|
||||
@@ -39,17 +37,14 @@ extension Ghostty {
|
||||
// is up to date. See TerminalSurfaceView for why we don't use the NSView
|
||||
// resize callback.
|
||||
GeometryReader { geo in
|
||||
#if canImport(AppKit)
|
||||
let pubBecomeKey = center.publisher(for: NSWindow.didBecomeKeyNotification)
|
||||
let pubResign = center.publisher(for: NSWindow.didResignKeyNotification)
|
||||
#endif
|
||||
|
||||
SurfaceRepresentable(view: surfaceView, size: geo.size)
|
||||
.focused($surfaceFocus)
|
||||
.focusedValue(\.ghosttySurfacePwd, surfaceView.pwd)
|
||||
.focusedValue(\.ghosttySurfaceView, surfaceView)
|
||||
.focusedValue(\.ghosttySurfaceCellSize, surfaceView.cellSize)
|
||||
#if canImport(AppKit)
|
||||
.onReceive(pubBecomeKey) { notification in
|
||||
guard let window = notification.object as? NSWindow else { return }
|
||||
guard let surfaceWindow = surfaceView.window else { return }
|
||||
@@ -62,7 +57,6 @@ extension Ghostty {
|
||||
windowFocus = false
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// If our geo size changed then we show the resize overlay as configured.
|
||||
if let surfaceSize = surfaceView.surfaceSize {
|
||||
@@ -89,7 +83,6 @@ extension Ghostty {
|
||||
.transition(.opacity)
|
||||
}
|
||||
|
||||
#if canImport(AppKit)
|
||||
// Readonly indicator badge
|
||||
if surfaceView.readonly {
|
||||
ReadonlyBadge {
|
||||
@@ -103,7 +96,6 @@ extension Ghostty {
|
||||
keySequence: surfaceView.keySequence
|
||||
)
|
||||
.zIndex(1)
|
||||
#endif
|
||||
|
||||
VStack(spacing: 0) {
|
||||
// If we have a URL from hovering a link, we show that.
|
||||
@@ -119,7 +111,6 @@ extension Ghostty {
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom)
|
||||
|
||||
#if canImport(AppKit)
|
||||
// If we have secure input enabled and we're the focused surface and window
|
||||
// then we want to show the secure input overlay.
|
||||
if ghostty.config.secureInputIndication &&
|
||||
@@ -128,7 +119,6 @@ extension Ghostty {
|
||||
windowFocus {
|
||||
SecureInputOverlay()
|
||||
}
|
||||
#endif
|
||||
|
||||
// Search overlay
|
||||
if let searchState = surfaceView.searchState {
|
||||
@@ -171,16 +161,12 @@ extension Ghostty {
|
||||
}
|
||||
}
|
||||
|
||||
#if canImport(AppKit)
|
||||
// Grab handle for dragging the window. We want this to appear at the very
|
||||
// top Z-index os it isn't faded by the unfocused overlay.
|
||||
//
|
||||
// This is disabled except on macOS because it uses AppKit drag/drop APIs.
|
||||
SurfaceGrabHandle(
|
||||
surfaceView: surfaceView,
|
||||
dragHandle: ghostty.config.dragHandle,
|
||||
)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -384,7 +370,7 @@ extension Ghostty {
|
||||
}
|
||||
.onReceive(
|
||||
NotificationCenter.default.publisher(
|
||||
for: OSApplication.didBecomeActiveNotification
|
||||
for: NSApplication.didBecomeActiveNotification
|
||||
)
|
||||
) { _ in
|
||||
// When the app becomes active, we want to check for external changes
|
||||
@@ -394,7 +380,6 @@ extension Ghostty {
|
||||
.onSubmit {
|
||||
_ = surfaceView.navigateSearchToNext()
|
||||
}
|
||||
#if canImport(AppKit)
|
||||
.onExitCommand {
|
||||
if searchState.needle.text.isEmpty {
|
||||
onClose()
|
||||
@@ -402,7 +387,6 @@ extension Ghostty {
|
||||
Ghostty.moveFocus(to: surfaceView)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
.backport.onKeyPress(.return) { modifiers in
|
||||
if modifiers.contains(.shift) {
|
||||
_ = surfaceView.navigateSearchToPrevious()
|
||||
@@ -477,7 +461,7 @@ extension Ghostty {
|
||||
}
|
||||
|
||||
private var clipShape: some Shape {
|
||||
if #available(iOS 26.0, macOS 26.0, *) {
|
||||
if #available(macOS 26.0, *) {
|
||||
return ConcentricRectangle(corners: .concentric(minimum: 8), isUniform: true)
|
||||
} else {
|
||||
return RoundedRectangle(cornerRadius: 8)
|
||||
@@ -557,7 +541,7 @@ extension Ghostty {
|
||||
/// A surface is terminology in Ghostty for a terminal surface, or a place where a terminal is actually drawn
|
||||
/// and interacted with. The word "surface" is used because a surface may represent a window, a tab,
|
||||
/// a split, a small preview pane, etc. It is ANYTHING that has a terminal drawn to it.
|
||||
struct SurfaceRepresentable: OSViewRepresentable {
|
||||
struct SurfaceRepresentable: NSViewRepresentable {
|
||||
/// The view to render for the terminal surface.
|
||||
let view: SurfaceView
|
||||
|
||||
@@ -571,13 +555,11 @@ extension Ghostty {
|
||||
/// The best approach is to wrap this view in a GeometryReader and pass in the geo.size.
|
||||
let size: CGSize
|
||||
|
||||
#if canImport(AppKit)
|
||||
func makeOSView(context: Context) -> SurfaceScrollView {
|
||||
// On macOS, wrap the surface view in a scroll view
|
||||
func makeNSView(context: Context) -> SurfaceScrollView {
|
||||
return SurfaceScrollView(contentSize: size, surfaceView: view)
|
||||
}
|
||||
|
||||
func updateOSView(_ scrollView: SurfaceScrollView, context: Context) {
|
||||
func updateNSView(_ scrollView: SurfaceScrollView, context: Context) {
|
||||
// SwiftUI may defer frame updates under system load (e.g., memory
|
||||
// pressure, heavy I/O) or when external window managers trigger rapid
|
||||
// layout changes. When that happens, the scroll view's bounds can
|
||||
@@ -586,16 +568,6 @@ extension Ghostty {
|
||||
guard scrollView.bounds.size != size else { return }
|
||||
scrollView.needsLayout = true
|
||||
}
|
||||
#else
|
||||
func makeOSView(context: Context) -> SurfaceView {
|
||||
// On iOS, return the surface view directly
|
||||
return view
|
||||
}
|
||||
|
||||
func updateOSView(_ view: SurfaceView, context: Context) {
|
||||
view.sizeDidChange(size)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/// The configuration for a surface. For any configuration not set, defaults will be chosen from
|
||||
@@ -656,25 +628,11 @@ extension Ghostty {
|
||||
func withCValue<T>(view: SurfaceView, _ body: (inout ghostty_surface_config_s) throws -> T) rethrows -> T {
|
||||
var config = ghostty_surface_config_new()
|
||||
config.userdata = Unmanaged.passUnretained(view).toOpaque()
|
||||
#if os(macOS)
|
||||
config.platform_tag = GHOSTTY_PLATFORM_MACOS
|
||||
config.platform = ghostty_platform_u(macos: ghostty_platform_macos_s(
|
||||
nsview: Unmanaged.passUnretained(view).toOpaque()
|
||||
))
|
||||
config.scale_factor = NSScreen.main!.backingScaleFactor
|
||||
#elseif os(iOS)
|
||||
config.platform_tag = GHOSTTY_PLATFORM_IOS
|
||||
config.platform = ghostty_platform_u(ios: ghostty_platform_ios_s(
|
||||
uiview: Unmanaged.passUnretained(view).toOpaque()
|
||||
))
|
||||
// Note that UIScreen.main is deprecated and we're supposed to get the
|
||||
// screen through the view hierarchy instead. This means that we should
|
||||
// probably set this to some default, then modify the scale factor through
|
||||
// libghostty APIs when a UIView is attached to a window/scene. TODO.
|
||||
config.scale_factor = UIScreen.main.scale
|
||||
#else
|
||||
#error("unsupported target")
|
||||
#endif
|
||||
|
||||
// Zero is our default value that means to inherit the font size.
|
||||
config.font_size = fontSize ?? 0
|
||||
@@ -725,7 +683,6 @@ extension Ghostty {
|
||||
}
|
||||
}
|
||||
|
||||
#if canImport(AppKit)
|
||||
/// Floating indicator that shows active key tables and pending key sequences.
|
||||
/// Displayed as a compact draggable pill that can be positioned at the top or bottom.
|
||||
struct KeyStateIndicator: View {
|
||||
@@ -943,7 +900,6 @@ extension Ghostty {
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
/// Visual overlay that shows a border around the edges when the bell rings with border feature enabled.
|
||||
struct BellBorderOverlay: View {
|
||||
@@ -1107,7 +1063,6 @@ extension Ghostty {
|
||||
}
|
||||
}
|
||||
|
||||
#if canImport(AppKit)
|
||||
/// When changing the split state, or going full screen (native or non), the terminal view
|
||||
/// will lose focus. There has to be some nice SwiftUI-native way to fix this but I can't
|
||||
/// figure it out so we're going to do this hacky thing to bring focus back to the terminal
|
||||
@@ -1161,7 +1116,6 @@ extension Ghostty {
|
||||
queue.async(execute: work)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// MARK: Surface Environment Keys
|
||||
@@ -1220,12 +1174,12 @@ extension FocusedValues {
|
||||
typealias Value = String
|
||||
}
|
||||
|
||||
var ghosttySurfaceCellSize: OSSize? {
|
||||
var ghosttySurfaceCellSize: CGSize? {
|
||||
get { self[FocusedGhosttySurfaceCellSize.self] }
|
||||
set { self[FocusedGhosttySurfaceCellSize.self] = newValue }
|
||||
}
|
||||
|
||||
struct FocusedGhosttySurfaceCellSize: FocusedValueKey {
|
||||
typealias Value = OSSize
|
||||
typealias Value = CGSize
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2043,7 +2043,7 @@ extension Ghostty.SurfaceView: NSTextInputClient {
|
||||
x += cellSize.width * Double(range.location + range.length)
|
||||
}
|
||||
// Ghostty coordinates are in top-left (0, 0) so we have to convert to
|
||||
// bottom-left since that is what UIKit expects
|
||||
// bottom-left since that is what AppKit expects
|
||||
// when there's is no characters selected,
|
||||
// width should be 0 so that dictation indicator
|
||||
// can start in the right place
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
import SwiftUI
|
||||
import GhosttyKit
|
||||
|
||||
extension Ghostty {
|
||||
/// The UIView implementation for a terminal surface.
|
||||
class SurfaceView: OSSurfaceView {
|
||||
// The current title of the surface as defined by the pty. This can be
|
||||
// changed with escape codes.
|
||||
@Published private(set) var title: String = "👻"
|
||||
|
||||
/// True when the bell is active. This is set inactive on focus or event.
|
||||
@Published var bell: Bool = false
|
||||
|
||||
private(set) var _surface: ghostty_surface_t?
|
||||
|
||||
override var surface: ghostty_surface_t? {
|
||||
_surface
|
||||
}
|
||||
|
||||
init(_ app: ghostty_app_t, baseConfig: SurfaceConfiguration? = nil, uuid: UUID? = nil) {
|
||||
|
||||
// Initialize with some default frame size. The important thing is that this
|
||||
// is non-zero so that our layer bounds are non-zero so that our renderer
|
||||
// can do SOMETHING.
|
||||
super.init(id: uuid, frame: CGRect(x: 0, y: 0, width: 800, height: 600))
|
||||
|
||||
// Setup our surface. This will also initialize all the terminal IO.
|
||||
let surface_cfg = baseConfig ?? SurfaceConfiguration()
|
||||
let surface = surface_cfg.withCValue(view: self) { surface_cfg_c in
|
||||
ghostty_surface_new(app, &surface_cfg_c)
|
||||
}
|
||||
guard let surface = surface else {
|
||||
// TODO
|
||||
return
|
||||
}
|
||||
self._surface = surface
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) is not supported for this view")
|
||||
}
|
||||
|
||||
deinit {
|
||||
guard let surface = self.surface else { return }
|
||||
ghostty_surface_free(surface)
|
||||
}
|
||||
|
||||
override func focusDidChange(_ focused: Bool) {
|
||||
guard let surface = self.surface else { return }
|
||||
ghostty_surface_set_focus(surface, focused)
|
||||
|
||||
// On macOS 13+ we can store our continuous clock...
|
||||
if focused {
|
||||
focusInstant = ContinuousClock.now
|
||||
}
|
||||
}
|
||||
|
||||
override func sizeDidChange(_ size: CGSize) {
|
||||
guard let surface = self.surface else { return }
|
||||
|
||||
// Ghostty wants to know the actual framebuffer size... It is very important
|
||||
// here that we use "size" and NOT the view frame. If we're in the middle of
|
||||
// an animation (i.e. a fullscreen animation), the frame will not yet be updated.
|
||||
// The size represents our final size we're going for.
|
||||
let scale = self.contentScaleFactor
|
||||
ghostty_surface_set_content_scale(surface, scale, scale)
|
||||
ghostty_surface_set_size(
|
||||
surface,
|
||||
UInt32(size.width * scale),
|
||||
UInt32(size.height * scale)
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: UIView
|
||||
|
||||
override class var layerClass: AnyClass {
|
||||
return CAMetalLayer.self
|
||||
}
|
||||
|
||||
override func didMoveToWindow() {
|
||||
sizeDidChange(frame.size)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,32 +26,23 @@ enum BackportKeyPressResult {
|
||||
|
||||
extension Backport where Content: View {
|
||||
func pointerVisibility(_ v: BackportVisibility) -> some View {
|
||||
#if canImport(AppKit)
|
||||
if #available(macOS 15, *) {
|
||||
return content.pointerVisibility(v.official)
|
||||
} else {
|
||||
return content
|
||||
}
|
||||
#else
|
||||
return content
|
||||
#endif
|
||||
}
|
||||
|
||||
func pointerStyle(_ style: BackportPointerStyle?) -> some View {
|
||||
#if canImport(AppKit)
|
||||
if #available(macOS 15, *) {
|
||||
return content.pointerStyle(style?.official)
|
||||
} else {
|
||||
return content
|
||||
}
|
||||
#else
|
||||
return content
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Backported onKeyPress that works on macOS 14+ and is a no-op on macOS 13.
|
||||
func onKeyPress(_ key: KeyEquivalent, action: @escaping (EventModifiers) -> BackportKeyPressResult) -> some View {
|
||||
#if canImport(AppKit)
|
||||
if #available(macOS 14, *) {
|
||||
return content.onKeyPress(key, phases: .down, action: { keyPress in
|
||||
switch action(keyPress.modifiers) {
|
||||
@@ -62,9 +53,6 @@ extension Backport where Content: View {
|
||||
} else {
|
||||
return content
|
||||
}
|
||||
#else
|
||||
return content
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,7 +85,6 @@ enum BackportPointerStyle {
|
||||
case resizeUpDown
|
||||
case resizeLeftRight
|
||||
|
||||
#if canImport(AppKit)
|
||||
@available(macOS 15, *)
|
||||
var official: PointerStyle {
|
||||
switch self {
|
||||
@@ -115,13 +102,11 @@ enum BackportPointerStyle {
|
||||
case .resizeLeftRight: return .columnResize
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
enum BackportNSGlassStyle {
|
||||
case regular, clear
|
||||
|
||||
#if canImport(AppKit)
|
||||
@available(macOS 26, *)
|
||||
var official: NSGlassEffectView.Style {
|
||||
switch self {
|
||||
@@ -129,11 +114,10 @@ enum BackportNSGlassStyle {
|
||||
case .clear: return .clear
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Backported `TextField` that supports text selection on macOS 26/iOS 18 and up. The `selection`
|
||||
/// has no effect on versions below macOS 26/iOS 18.
|
||||
/// Backported `TextField` that supports text selection on macOS 26 and up. The `selection`
|
||||
/// has no effect on versions below macOS 26.
|
||||
///
|
||||
/// Although the API is available from macOS 15, we force it to be 26. Because on macOS 15,
|
||||
/// SwiftUI will crash when deleting texts, even for this simple example.
|
||||
@@ -161,7 +145,7 @@ struct BackportSelectionTextField: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if #available(iOS 18.0, macOS 26, *) {
|
||||
if #available(macOS 26, *) {
|
||||
TextField(
|
||||
titleKey,
|
||||
text: _text,
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
// This file is a helper to bridge some types that are effectively identical
|
||||
// between AppKit and UIKit.
|
||||
|
||||
import SwiftUI
|
||||
|
||||
#if canImport(AppKit)
|
||||
|
||||
import AppKit
|
||||
|
||||
typealias OSView = NSView
|
||||
typealias OSColor = NSColor
|
||||
typealias OSSize = NSSize
|
||||
typealias OSPasteboard = NSPasteboard
|
||||
typealias OSApplication = NSApplication
|
||||
|
||||
protocol OSViewRepresentable: NSViewRepresentable where NSViewType == OSViewType {
|
||||
associatedtype OSViewType: NSView
|
||||
func makeOSView(context: Context) -> OSViewType
|
||||
func updateOSView(_ osView: OSViewType, context: Context)
|
||||
}
|
||||
|
||||
extension OSViewRepresentable {
|
||||
func makeNSView(context: Context) -> OSViewType {
|
||||
makeOSView(context: context)
|
||||
}
|
||||
|
||||
func updateNSView(_ nsView: OSViewType, context: Context) {
|
||||
updateOSView(nsView, context: context)
|
||||
}
|
||||
}
|
||||
|
||||
#elseif canImport(UIKit)
|
||||
|
||||
import UIKit
|
||||
|
||||
typealias OSView = UIView
|
||||
typealias OSColor = UIColor
|
||||
typealias OSSize = CGSize
|
||||
typealias OSPasteboard = UIPasteboard
|
||||
typealias OSApplication = UIApplication
|
||||
|
||||
protocol OSViewRepresentable: UIViewRepresentable {
|
||||
associatedtype OSViewType: UIView
|
||||
func makeOSView(context: Context) -> OSViewType
|
||||
func updateOSView(_ osView: OSViewType, context: Context)
|
||||
}
|
||||
|
||||
extension OSViewRepresentable {
|
||||
func makeUIView(context: Context) -> OSViewType {
|
||||
makeOSView(context: context)
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: OSViewType, context: Context) {
|
||||
updateOSView(uiView, context: context)
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -1,4 +1,4 @@
|
||||
import Cocoa
|
||||
import AppKit
|
||||
|
||||
extension NSAppearance {
|
||||
/// Returns true if the appearance is some kind of dark.
|
||||
@@ -17,7 +17,7 @@ extension NSAppearance {
|
||||
self.init(named: .aqua)
|
||||
|
||||
case "auto":
|
||||
let color = OSColor(config.backgroundColor)
|
||||
let color = NSColor(config.backgroundColor)
|
||||
if color.isLightColor {
|
||||
self.init(named: .aqua)
|
||||
} else {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import Foundation
|
||||
import AppKit
|
||||
#if !DOCK_TILE_PLUGIN
|
||||
import GhosttyKit
|
||||
#endif
|
||||
|
||||
extension OSColor {
|
||||
extension NSColor {
|
||||
var isLightColor: Bool {
|
||||
return self.luminance > 0.5
|
||||
}
|
||||
@@ -15,42 +16,20 @@ extension OSColor {
|
||||
var a: CGFloat = 0
|
||||
|
||||
// getRed:green:blue:alpha requires sRGB space
|
||||
#if canImport(AppKit)
|
||||
guard let rgb = self.usingColorSpace(.sRGB) else { return 0 }
|
||||
#else
|
||||
let rgb = self
|
||||
#endif
|
||||
rgb.getRed(&r, green: &g, blue: &b, alpha: &a)
|
||||
return (0.299 * r) + (0.587 * g) + (0.114 * b)
|
||||
}
|
||||
|
||||
var hexString: String? {
|
||||
#if canImport(AppKit)
|
||||
guard let rgb = usingColorSpace(.deviceRGB) else { return nil }
|
||||
let red = Int(rgb.redComponent * 255)
|
||||
let green = Int(rgb.greenComponent * 255)
|
||||
let blue = Int(rgb.blueComponent * 255)
|
||||
return String(format: "#%02X%02X%02X", red, green, blue)
|
||||
#elseif canImport(UIKit)
|
||||
var red: CGFloat = 0
|
||||
var green: CGFloat = 0
|
||||
var blue: CGFloat = 0
|
||||
var alpha: CGFloat = 0
|
||||
guard self.getRed(&red, green: &green, blue: &blue, alpha: &alpha) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert to 0–255 range
|
||||
let r = Int(red * 255)
|
||||
let g = Int(green * 255)
|
||||
let b = Int(blue * 255)
|
||||
|
||||
// Format to hexadecimal
|
||||
return String(format: "#%02X%02X%02X", r, g, b)
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Create an OSColor from a hex string.
|
||||
/// Create an NSColor from a hex string.
|
||||
convenience init?(hex: String) {
|
||||
var cleanedHex = hex.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
@@ -81,10 +60,10 @@ extension OSColor {
|
||||
self.init(red: red, green: green, blue: blue, alpha: alpha)
|
||||
}
|
||||
|
||||
func darken(by amount: CGFloat) -> OSColor {
|
||||
func darken(by amount: CGFloat) -> NSColor {
|
||||
var h: CGFloat = 0, s: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0
|
||||
self.getHue(&h, saturation: &s, brightness: &b, alpha: &a)
|
||||
return OSColor(
|
||||
return NSColor(
|
||||
hue: h,
|
||||
saturation: s,
|
||||
brightness: min(b * (1 - amount), 1),
|
||||
@@ -95,7 +74,7 @@ extension OSColor {
|
||||
|
||||
// MARK: Ghostty Types
|
||||
#if !DOCK_TILE_PLUGIN
|
||||
extension OSColor {
|
||||
extension NSColor {
|
||||
/// Create a color from a Ghostty color.
|
||||
convenience init(ghostty: ghostty_config_color_s) {
|
||||
let red = Double(ghostty.r) / 255
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
#if canImport(AppKit)
|
||||
import AppKit
|
||||
|
||||
/// Normalizes the interface between NSPasteboard and UIPasteboard for working with pasteboard
|
||||
/// strings.
|
||||
extension OSPasteboard {
|
||||
@MainActor static let find = OSPasteboard(name: .find)
|
||||
/// Convenience interface for working with pasteboard strings.
|
||||
extension NSPasteboard {
|
||||
@MainActor static let find = NSPasteboard(name: .find)
|
||||
|
||||
/// The pasteboard's current string value.
|
||||
@MainActor var string: String? {
|
||||
@@ -18,11 +17,3 @@ extension OSPasteboard {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#elseif canImport(UIKit)
|
||||
|
||||
extension OSPasteboard {
|
||||
static let find = OSPasteboard.withUniqueName()
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -7,7 +7,6 @@ extension String {
|
||||
return self.prefix(maxLength) + trailing
|
||||
}
|
||||
|
||||
#if canImport(AppKit)
|
||||
func temporaryFile(_ filename: String = "temp") -> URL {
|
||||
let url = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent(filename)
|
||||
@@ -25,7 +24,6 @@ extension String {
|
||||
}
|
||||
return self
|
||||
}
|
||||
#endif
|
||||
|
||||
/// Converts a four-character ASCII string to its `FourCharCode` (`UInt32`) value.
|
||||
var fourCharCode: UInt32 {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#if os(macOS)
|
||||
import AppKit
|
||||
|
||||
/// Presents decisions for untrusted URLs at the AppKit boundary.
|
||||
@@ -103,4 +102,3 @@ enum UntrustedURLAlert {
|
||||
return scrollView
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -8,7 +8,7 @@ import Testing
|
||||
typealias StartSearch = Ghostty.Action.StartSearch
|
||||
|
||||
/// A unique pasteboard for each test case prevents flakiness.
|
||||
let pasteboard = OSPasteboard.withUniqueName()
|
||||
let pasteboard = NSPasteboard.withUniqueName()
|
||||
|
||||
init() {
|
||||
pasteboard.setString("pb", forType: .string)
|
||||
|
||||
Reference in New Issue
Block a user