diff --git a/macos/.swiftlint.yml b/macos/.swiftlint.yml index e9767a43f..8f76034af 100644 --- a/macos/.swiftlint.yml +++ b/macos/.swiftlint.yml @@ -13,37 +13,15 @@ disabled_rules: - type_body_length # TODO - - colon - - comma - - comment_spacing - - control_statement - deployment_target - - empty_enum_arguments - - empty_parentheses_with_trailing_closure - for_where - force_cast - - implicit_getter - - implicit_optional_initialization - - legacy_constant - - legacy_constructor - line_length - mark - multiple_closures_with_trailing_closure - no_fallthrough_only - - opening_brace - - orphaned_doc_comment - - private_over_fileprivate - - shorthand_operator - switch_case_alignment - - syntactic_sugar - - trailing_semicolon - - trailing_whitespace - - unneeded_synthesized_initializer - - unused_closure_parameter - unused_enumerated - - unused_optional_binding - - vertical_parameter_alignment - - vertical_whitespace identifier_name: min_length: 1 diff --git a/macos/Sources/App/macOS/AppDelegate+Ghostty.swift b/macos/Sources/App/macOS/AppDelegate+Ghostty.swift index 4d798a1a5..66b95e06e 100644 --- a/macos/Sources/App/macOS/AppDelegate+Ghostty.swift +++ b/macos/Sources/App/macOS/AppDelegate+Ghostty.swift @@ -10,14 +10,14 @@ extension AppDelegate: Ghostty.Delegate { guard let controller = window.windowController as? BaseTerminalController else { continue } - + for surface in controller.surfaceTree { if surface.id == id { return surface } } } - + return nil } } diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index 2ca7d4813..1aa597a25 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -9,8 +9,7 @@ class AppDelegate: NSObject, ObservableObject, NSApplicationDelegate, UNUserNotificationCenterDelegate, - GhosttyAppDelegate -{ + GhosttyAppDelegate { // The application logger. We should probably move this at some point to a dedicated // class/struct but for now it lives here! 🤷‍♂️ static let logger = Logger( @@ -110,7 +109,7 @@ class AppDelegate: NSObject, switch quickTerminalControllerState { case .initialized(let controller): return controller - + case .pendingRestore(let state): let controller = QuickTerminalController( ghostty, @@ -120,7 +119,7 @@ class AppDelegate: NSObject, ) quickTerminalControllerState = .initialized(controller) return controller - + case .uninitialized: let controller = QuickTerminalController( ghostty, @@ -144,16 +143,16 @@ class AppDelegate: NSObject, } /// Tracks the windows that we hid for toggleVisibility. - private(set) var hiddenState: ToggleVisibilityState? = nil + private(set) var hiddenState: ToggleVisibilityState? /// The observer for the app appearance. - private var appearanceObserver: NSKeyValueObservation? = nil + private var appearanceObserver: NSKeyValueObservation? /// Signals private var signals: [DispatchSourceSignal] = [] /// The custom app icon image that is currently in use. - @Published private(set) var appIcon: NSImage? = nil + @Published private(set) var appIcon: NSImage? override init() { #if DEBUG @@ -166,14 +165,14 @@ class AppDelegate: NSObject, ghostty.delegate = self } - //MARK: - NSApplicationDelegate + // MARK: - NSApplicationDelegate func applicationWillFinishLaunching(_ notification: Notification) { UserDefaults.standard.register(defaults: [ // Disable the automatic full screen menu item because we handle // it manually. "NSFullScreenMenuItemEverywhere": false, - + // On macOS 26 RC1, the autofill heuristic controller causes unusable levels // of slowdowns and CPU usage in the terminal window under certain [unknown] // conditions. We don't know exactly why/how. This disables the full heuristic @@ -197,7 +196,7 @@ class AppDelegate: NSObject, applicationLaunchTime = ProcessInfo.processInfo.systemUptime // Check if secure input was enabled when we last quit. - if (UserDefaults.standard.bool(forKey: "SecureInput") != SecureInput.shared.enabled) { + if UserDefaults.standard.bool(forKey: "SecureInput") != SecureInput.shared.enabled { toggleSecureInput(self) } @@ -280,7 +279,7 @@ class AppDelegate: NSObject, guard let appearance = change.newValue else { return } guard let app = self.ghostty.app else { return } let scheme: ghostty_color_scheme_e - if (appearance.isDark) { + if appearance.isDark { scheme = GHOSTTY_COLOR_SCHEME_DARK } else { scheme = GHOSTTY_COLOR_SCHEME_LIGHT @@ -299,12 +298,12 @@ class AppDelegate: NSObject, case .app: // Don't have to do anything. break - + case .zig_run, .cli: // Part of launch services (clicking an app, using `open`, etc.) activates // the application and brings it to the front. When using the CLI we don't // get this behavior, so we have to do it manually. - + // This never gets called until we click the dock icon. This forces it // activate immediately. applicationDidBecomeActive(.init(name: NSApplication.didBecomeActiveNotification)) @@ -332,7 +331,7 @@ class AppDelegate: NSObject, self.setDockBadge(nil) // First launch stuff - if (!applicationHasBecomeActive) { + if !applicationHasBecomeActive { applicationHasBecomeActive = true // Let's launch our first window. We only do this if we have no other windows. It @@ -353,8 +352,8 @@ class AppDelegate: NSObject, func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { let windows = NSApplication.shared.windows - if (windows.isEmpty) { return .terminateNow } - + if windows.isEmpty { return .terminateNow } + // If we've already accepted to install an update, then we don't need to // confirm quit. The user is already expecting the update to happen. if updateController.isInstalling { @@ -380,7 +379,7 @@ class AppDelegate: NSObject, guard let keyword = AEKeyword("why?") else { break why } if let why = event.attributeDescriptor(forKeyword: keyword) { - switch (why.typeCodeValue) { + switch why.typeCodeValue { case kAEShutDown: fallthrough @@ -397,7 +396,7 @@ class AppDelegate: NSObject, } // If our app says we don't need to confirm, we can exit now. - if (!ghostty.needsConfirmQuit) { return .terminateNow } + if !ghostty.needsConfirmQuit { return .terminateNow } // We have some visible window. Show an app-wide modal to confirm quitting. let alert = NSAlert() @@ -406,7 +405,7 @@ class AppDelegate: NSObject, alert.addButton(withTitle: "Close Ghostty") alert.addButton(withTitle: "Cancel") alert.alertStyle = .warning - switch (alert.runModal()) { + switch alert.runModal() { case .alertFirstButtonReturn: return .terminateNow @@ -449,18 +448,18 @@ class AppDelegate: NSObject, // Ghostty will validate as well but we can avoid creating an entirely new // surface by doing our own validation here. We can also show a useful error // this way. - + var isDirectory = ObjCBool(true) guard FileManager.default.fileExists(atPath: filename, isDirectory: &isDirectory) else { return false } - + // Set to true if confirmation is required before starting up the // new terminal. var requiresConfirm: Bool = false - + // Initialize the surface config which will be used to create the tab or window for the opened file. var config = Ghostty.SurfaceConfiguration() - - if (isDirectory.boolValue) { + + if isDirectory.boolValue { // When opening a directory, check the configuration to decide // whether to open in a new tab or new window. config.workingDirectory = filename @@ -471,24 +470,24 @@ class AppDelegate: NSObject, // because there is a sandbox escape possible if a sandboxed application // somehow is tricked into `open`-ing a non-sandboxed application. requiresConfirm = true - + // When opening a file, we want to execute the file. To do this, we // don't override the command directly, because it won't load the // profile/rc files for the shell, which is super important on macOS // due to things like Homebrew. Instead, we set the command to // `; exit` which is what Terminal and iTerm2 do. config.initialInput = "\(Ghostty.Shell.quote(filename)); exit\n" - + // For commands executed directly, we want to ensure we wait after exit // because in most cases scripts don't block on exit and we don't want // the window to just flash closed once complete. config.waitAfterCommand = true - + // Set the parent directory to our working directory so that relative // paths in scripts work. config.workingDirectory = (filename as NSString).deletingLastPathComponent } - + if requiresConfirm { // Confirmation required. We use an app-wide NSAlert for now. In the future we // may want to show this as a sheet on the focused window (especially if we're @@ -498,15 +497,15 @@ class AppDelegate: NSObject, alert.addButton(withTitle: "Allow") alert.addButton(withTitle: "Cancel") alert.alertStyle = .warning - switch (alert.runModal()) { + switch alert.runModal() { case .alertFirstButtonReturn: break - + default: return false } } - + switch ghostty.config.macosDockDropBehavior { case .new_tab: _ = TerminalController.newTab( @@ -516,7 +515,7 @@ class AppDelegate: NSObject, ) case .new_window: _ = TerminalController.newWindow(ghostty, withBaseConfig: config) } - + return true } @@ -746,7 +745,7 @@ class AppDelegate: NSObject, guard let ghostty = self.ghostty.app else { return event } // Build our event input and call ghostty - if (ghostty_app_key(ghostty, event.ghosttyKeyEvent(GHOSTTY_ACTION_PRESS))) { + if ghostty_app_key(ghostty, event.ghosttyKeyEvent(GHOSTTY_ACTION_PRESS)) { // The key was used so we want to stop it from going to our Mac app Ghostty.logger.debug("local key event handled event=\(event)") return nil @@ -761,7 +760,7 @@ class AppDelegate: NSObject, @objc private func quickTerminalDidChangeVisibility(_ notification: Notification) { guard let quickController = notification.object as? QuickTerminalController else { return } - self.menuQuickTerminal?.state = if (quickController.visible) { .on } else { .off } + self.menuQuickTerminal?.state = if quickController.visible { .on } else { .off } } @objc private func ghosttyConfigDidChange(_ notification: Notification) { @@ -777,11 +776,11 @@ class AppDelegate: NSObject, } @objc private func ghosttyBellDidRing(_ notification: Notification) { - if (ghostty.config.bellFeatures.contains(.system)) { + if ghostty.config.bellFeatures.contains(.system) { NSSound.beep() } - if (ghostty.config.bellFeatures.contains(.attention)) { + if ghostty.config.bellFeatures.contains(.attention) { // Bounce the dock icon if we're not focused. NSApp.requestUserAttention(.informationalRequest) @@ -861,7 +860,7 @@ class AppDelegate: NSObject, // Depending on the "window-save-state" setting we have to set the NSQuitAlwaysKeepsWindows // configuration. This is the only way to carefully control whether macOS invokes the // state restoration system. - switch (config.windowSaveState) { + switch config.windowSaveState { case "never": UserDefaults.standard.setValue(false, forKey: "NSQuitAlwaysKeepsWindows") case "always": UserDefaults.standard.setValue(true, forKey: "NSQuitAlwaysKeepsWindows") case "default": fallthrough @@ -880,14 +879,14 @@ class AppDelegate: NSObject, autoUpdate == .check || autoUpdate == .download updateController.updater.automaticallyDownloadsUpdates = autoUpdate == .download - /** + /* To test `auto-update` easily, uncomment the line below and delete `SUEnableAutomaticChecks` in Ghostty-Info.plist. Note: When `auto-update = download`, you may need to `Clean Build Folder` if a background install has already begun. */ - //updateController.updater.checkForUpdatesInBackground() + // updateController.updater.checkForUpdatesInBackground() } // Config could change keybindings, so update everything that depends on that @@ -900,7 +899,7 @@ class AppDelegate: NSObject, DispatchQueue.main.async { self.syncAppearance(config: config) } // Decide whether to hide/unhide app from dock and app switcher - switch (config.macosHidden) { + switch config.macosHidden { case .never: NSApp.setActivationPolicy(.regular) @@ -911,16 +910,16 @@ class AppDelegate: NSObject, // If we have configuration errors, we need to show them. let c = ConfigurationErrorsController.sharedInstance c.errors = config.errors - if (c.errors.count > 0) { - if (c.window == nil || !c.window!.isVisible) { + if c.errors.count > 0 { + if c.window == nil || !c.window!.isVisible { c.showWindow(self) } } // We need to handle our global event tap depending on if there are global // events that we care about in Ghostty. - if (ghostty_app_has_global_keybinds(ghostty.app!)) { - if (timeSinceLaunch > 5) { + if ghostty_app_has_global_keybinds(ghostty.app!) { + if timeSinceLaunch > 5 { // If the process has been running for awhile we enable right away // because no windows are likely to pop up. GlobalEventTap.shared.enable() @@ -948,11 +947,11 @@ class AppDelegate: NSObject, // Using AppIconActor to ensure this work // happens synchronously in the background @AppIconActor - private func updateAppIcon(from config: Ghostty.Config) async { + private func updateAppIcon(from config: Ghostty.Config) async { var appIcon: NSImage? var appIconName: String? = config.macosIcon.rawValue - switch (config.macosIcon) { + switch config.macosIcon { case let icon where icon.assetName != nil: appIcon = NSImage(named: icon.assetName!)! @@ -1022,7 +1021,7 @@ class AppDelegate: NSObject, UserDefaults.standard.set(currentBuild, forKey: "CustomGhosttyIconBuild") } - //MARK: - Restorable State + // MARK: - Restorable State /// We support NSSecureCoding for restorable state. Required as of macOS Sonoma (14) but a good idea anyways. func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { @@ -1031,18 +1030,18 @@ class AppDelegate: NSObject, func application(_ app: NSApplication, willEncodeRestorableState coder: NSCoder) { Self.logger.debug("application will save window state") - + guard ghostty.config.windowSaveState != "never" else { return } - + // Encode our quick terminal state if we have it. switch quickTerminalControllerState { case .initialized(let controller) where controller.restorable: let data = QuickTerminalRestorableState(from: controller) data.encode(with: coder) - + case .pendingRestore(let state): state.encode(with: coder) - + default: break } @@ -1050,7 +1049,7 @@ class AppDelegate: NSObject, func application(_ app: NSApplication, didDecodeRestorableState coder: NSCoder) { Self.logger.debug("application will restore window state") - + // Decode our quick terminal state. if ghostty.config.windowSaveState != "never", let state = QuickTerminalRestorableState(coder: coder) { @@ -1058,7 +1057,7 @@ class AppDelegate: NSObject, } } - //MARK: - UNUserNotificationCenterDelegate + // MARK: - UNUserNotificationCenterDelegate func userNotificationCenter( _ center: UNUserNotificationCenter, @@ -1079,7 +1078,7 @@ class AppDelegate: NSObject, withCompletionHandler(options) } - //MARK: - GhosttyAppDelegate + // MARK: - GhosttyAppDelegate func findSurface(forUUID uuid: UUID) -> Ghostty.SurfaceView? { for c in TerminalController.all { @@ -1093,7 +1092,7 @@ class AppDelegate: NSObject, return nil } - //MARK: - Dock Menu + // MARK: - Dock Menu private func reloadDockMenu() { let newWindow = NSMenuItem(title: "New Window", action: #selector(newWindow), keyEquivalent: "") @@ -1104,11 +1103,11 @@ class AppDelegate: NSObject, dockMenu.addItem(newTab) } - //MARK: - Global State + // MARK: - Global State func setSecureInput(_ mode: Ghostty.SetSecureInput) { let input = SecureInput.shared - switch (mode) { + switch mode { case .on: input.global = true @@ -1118,11 +1117,11 @@ class AppDelegate: NSObject, case .toggle: input.global.toggle() } - self.menuSecureInput?.state = if (input.global) { .on } else { .off } + self.menuSecureInput?.state = if input.global { .on } else { .off } UserDefaults.standard.set(input.global, forKey: "SecureInput") } - //MARK: - IB Actions + // MARK: - IB Actions @IBAction func openConfig(_ sender: Any?) { Ghostty.App.openConfig() @@ -1134,7 +1133,7 @@ class AppDelegate: NSObject, @IBAction func checkForUpdates(_ sender: Any?) { updateController.checkForUpdates() - //UpdateSimulator.happyPath.simulate(with: updateViewModel) + // UpdateSimulator.happyPath.simulate(with: updateViewModel) } @IBAction func newWindow(_ sender: Any?) { @@ -1288,7 +1287,7 @@ extension AppDelegate { @IBAction func useAsDefault(_ sender: NSMenuItem) { let ud = UserDefaults.standard let key = TerminalWindow.defaultLevelKey - if (menuFloatOnTop?.state == .on) { + if menuFloatOnTop?.state == .on { ud.set(NSWindow.Level.floating, forKey: key) } else { ud.removeObject(forKey: key) @@ -1360,6 +1359,6 @@ private enum QuickTerminalState { } @globalActor -fileprivate actor AppIconActor: GlobalActor { +private actor AppIconActor: GlobalActor { static let shared = AppIconActor() } diff --git a/macos/Sources/App/macOS/main.swift b/macos/Sources/App/macOS/main.swift index ad32f4e70..ade9bf3f0 100644 --- a/macos/Sources/App/macOS/main.swift +++ b/macos/Sources/App/macOS/main.swift @@ -7,7 +7,7 @@ import GhosttyKit // rest of the app. if ghostty_init(UInt(CommandLine.argc), CommandLine.unsafeArgv) != GHOSTTY_SUCCESS { Ghostty.logger.critical("ghostty_init failed") - + // We also write to stderr if this is executed from the CLI or zig run switch Ghostty.launchSource { case .cli, .zig_run: @@ -18,7 +18,7 @@ if ghostty_init(UInt(CommandLine.argc), CommandLine.unsafeArgv) != GHOSTTY_SUCCE "Actions start with the `+` character.\n\n" + "View all available actions by running `ghostty +help`.\n") exit(1) - + case .app: // For the app we exit immediately. We should handle this case more // gracefully in the future. @@ -28,6 +28,6 @@ if ghostty_init(UInt(CommandLine.argc), CommandLine.unsafeArgv) != GHOSTTY_SUCCE // This will run the CLI action and exit if one was specified. A CLI // action is a command starting with a `+`, such as `ghostty +boo`. -ghostty_cli_try_action(); +ghostty_cli_try_action() _ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv) diff --git a/macos/Sources/Features/About/AboutController.swift b/macos/Sources/Features/About/AboutController.swift index efd7a515a..2f494f12c 100644 --- a/macos/Sources/Features/About/AboutController.swift +++ b/macos/Sources/Features/About/AboutController.swift @@ -24,7 +24,7 @@ class AboutController: NSWindowController, NSWindowDelegate { window?.close() } - //MARK: - First Responder + // MARK: - First Responder @IBAction func close(_ sender: Any) { self.window?.performClose(sender) diff --git a/macos/Sources/Features/About/AboutView.swift b/macos/Sources/Features/About/AboutView.swift index 967eb16b0..d9a12e4dc 100644 --- a/macos/Sources/Features/About/AboutView.swift +++ b/macos/Sources/Features/About/AboutView.swift @@ -21,8 +21,7 @@ struct AboutView: View { init(material: NSVisualEffectView.Material, blendingMode: NSVisualEffectView.BlendingMode = .behindWindow, - isEmphasized: Bool = false) - { + isEmphasized: Bool = false) { self.material = material self.blendingMode = blendingMode self.isEmphasized = isEmphasized diff --git a/macos/Sources/Features/App Intents/CloseTerminalIntent.swift b/macos/Sources/Features/App Intents/CloseTerminalIntent.swift index 0155cf855..c3cca2514 100644 --- a/macos/Sources/Features/App Intents/CloseTerminalIntent.swift +++ b/macos/Sources/Features/App Intents/CloseTerminalIntent.swift @@ -22,7 +22,7 @@ struct CloseTerminalIntent: AppIntent { guard await requestIntentPermission() else { throw GhosttyIntentError.permissionDenied } - + guard let surfaceView = terminal.surfaceView else { throw GhosttyIntentError.surfaceNotFound } diff --git a/macos/Sources/Features/App Intents/CommandPaletteIntent.swift b/macos/Sources/Features/App Intents/CommandPaletteIntent.swift index 2f07d7861..de6063564 100644 --- a/macos/Sources/Features/App Intents/CommandPaletteIntent.swift +++ b/macos/Sources/Features/App Intents/CommandPaletteIntent.swift @@ -29,7 +29,7 @@ struct CommandPaletteIntent: AppIntent { guard await requestIntentPermission() else { throw GhosttyIntentError.permissionDenied } - + guard let surface = terminal.surfaceModel else { throw GhosttyIntentError.surfaceNotFound } diff --git a/macos/Sources/Features/App Intents/Entities/CommandEntity.swift b/macos/Sources/Features/App Intents/Entities/CommandEntity.swift index 3c7745e7c..f05b5d9b9 100644 --- a/macos/Sources/Features/App Intents/Entities/CommandEntity.swift +++ b/macos/Sources/Features/App Intents/Entities/CommandEntity.swift @@ -25,11 +25,6 @@ struct CommandEntity: AppEntity { struct ID: Hashable { let terminalId: TerminalEntity.ID let actionKey: String - - init(terminalId: TerminalEntity.ID, actionKey: String) { - self.terminalId = terminalId - self.actionKey = actionKey - } } static var typeDisplayRepresentation: TypeDisplayRepresentation { @@ -79,7 +74,7 @@ extension CommandEntity.ID: EntityIdentifierConvertible { static func entityIdentifier(for entityIdentifierString: String) -> CommandEntity.ID? { .init(rawValue: entityIdentifierString) } - + var entityIdentifierString: String { rawValue } diff --git a/macos/Sources/Features/App Intents/Entities/TerminalEntity.swift b/macos/Sources/Features/App Intents/Entities/TerminalEntity.swift index e805466a2..a2c4abea0 100644 --- a/macos/Sources/Features/App Intents/Entities/TerminalEntity.swift +++ b/macos/Sources/Features/App Intents/Entities/TerminalEntity.swift @@ -52,7 +52,7 @@ struct TerminalEntity: AppEntity { if let nsImage = ImageRenderer(content: view.screenshot()).nsImage { self.screenshot = nsImage } - + // Determine the kind based on the window controller type if view.window?.windowController is QuickTerminalController { self.kind = .quick @@ -66,9 +66,9 @@ extension TerminalEntity { enum Kind: String, AppEnum { case normal case quick - + static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Terminal Kind") - + static var caseDisplayRepresentations: [Self: DisplayRepresentation] = [ .normal: .init(title: "Normal"), .quick: .init(title: "Quick") @@ -112,7 +112,7 @@ struct TerminalQuery: EntityStringQuery, EnumerableEntityQuery { let controllers = NSApp.windows.compactMap { $0.windowController as? BaseTerminalController } - + // Get all our surfaces return controllers.flatMap { $0.surfaceTree.root?.leaves() ?? [] diff --git a/macos/Sources/Features/App Intents/GetTerminalDetailsIntent.swift b/macos/Sources/Features/App Intents/GetTerminalDetailsIntent.swift index 563e3719b..99d6e39ba 100644 --- a/macos/Sources/Features/App Intents/GetTerminalDetailsIntent.swift +++ b/macos/Sources/Features/App Intents/GetTerminalDetailsIntent.swift @@ -31,7 +31,7 @@ struct GetTerminalDetailsIntent: AppIntent { guard await requestIntentPermission() else { throw GhosttyIntentError.permissionDenied } - + switch detail { case .title: return .result(value: terminal.title) case .workingDirectory: return .result(value: terminal.workingDirectory) diff --git a/macos/Sources/Features/App Intents/InputIntent.swift b/macos/Sources/Features/App Intents/InputIntent.swift index d169b3a8c..b77945ccc 100644 --- a/macos/Sources/Features/App Intents/InputIntent.swift +++ b/macos/Sources/Features/App Intents/InputIntent.swift @@ -34,7 +34,7 @@ struct InputTextIntent: AppIntent { guard await requestIntentPermission() else { throw GhosttyIntentError.permissionDenied } - + guard let surface = terminal.surfaceModel else { throw GhosttyIntentError.surfaceNotFound } @@ -86,7 +86,7 @@ struct KeyEventIntent: AppIntent { guard await requestIntentPermission() else { throw GhosttyIntentError.permissionDenied } - + guard let surface = terminal.surfaceModel else { throw GhosttyIntentError.surfaceNotFound } @@ -95,7 +95,7 @@ struct KeyEventIntent: AppIntent { let ghosttyMods = mods.reduce(Ghostty.Input.Mods()) { result, mod in result.union(mod.ghosttyMod) } - + let keyEvent = Ghostty.Input.KeyEvent( key: key, action: action, @@ -150,7 +150,7 @@ struct MouseButtonIntent: AppIntent { guard await requestIntentPermission() else { throw GhosttyIntentError.permissionDenied } - + guard let surface = terminal.surfaceModel else { throw GhosttyIntentError.surfaceNotFound } @@ -159,7 +159,7 @@ struct MouseButtonIntent: AppIntent { let ghosttyMods = mods.reduce(Ghostty.Input.Mods()) { result, mod in result.union(mod.ghosttyMod) } - + let mouseEvent = Ghostty.Input.MouseButtonEvent( action: action, button: button, @@ -184,7 +184,7 @@ struct MousePosIntent: AppIntent { var x: Double @Parameter( - title: "Y Position", + title: "Y Position", description: "The vertical position of the mouse cursor in pixels.", default: 0 ) @@ -213,7 +213,7 @@ struct MousePosIntent: AppIntent { guard await requestIntentPermission() else { throw GhosttyIntentError.permissionDenied } - + guard let surface = terminal.surfaceModel else { throw GhosttyIntentError.surfaceNotFound } @@ -222,7 +222,7 @@ struct MousePosIntent: AppIntent { let ghosttyMods = mods.reduce(Ghostty.Input.Mods()) { result, mod in result.union(mod.ghosttyMod) } - + let mousePosEvent = Ghostty.Input.MousePosEvent( x: x, y: y, @@ -283,7 +283,7 @@ struct MouseScrollIntent: AppIntent { guard await requestIntentPermission() else { throw GhosttyIntentError.permissionDenied } - + guard let surface = terminal.surfaceModel else { throw GhosttyIntentError.surfaceNotFound } @@ -306,16 +306,16 @@ enum KeyEventMods: String, AppEnum, CaseIterable { case control case option case command - + static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Modifier Key") - - static var caseDisplayRepresentations: [KeyEventMods : DisplayRepresentation] = [ + + static var caseDisplayRepresentations: [KeyEventMods: DisplayRepresentation] = [ .shift: "Shift", .control: "Control", .option: "Option", .command: "Command" ] - + var ghosttyMod: Ghostty.Input.Mods { switch self { case .shift: .shift diff --git a/macos/Sources/Features/App Intents/IntentPermission.swift b/macos/Sources/Features/App Intents/IntentPermission.swift index 210d2cb2e..26a21e70b 100644 --- a/macos/Sources/Features/App Intents/IntentPermission.swift +++ b/macos/Sources/Features/App Intents/IntentPermission.swift @@ -28,7 +28,7 @@ func requestIntentPermission() async -> Bool { await withCheckedContinuation { continuation in Task { @MainActor in if let delegate = NSApp.delegate as? AppDelegate { - switch (delegate.ghostty.config.macosShortcuts) { + switch delegate.ghostty.config.macosShortcuts { case .allow: continuation.resume(returning: true) return @@ -43,7 +43,6 @@ func requestIntentPermission() async -> Bool { } } - PermissionRequest.show( "com.mitchellh.ghostty.shortcutsPermission", message: "Allow Shortcuts to interact with Ghostty?", diff --git a/macos/Sources/Features/App Intents/KeybindIntent.swift b/macos/Sources/Features/App Intents/KeybindIntent.swift index a8cea8561..e4f41ebbd 100644 --- a/macos/Sources/Features/App Intents/KeybindIntent.swift +++ b/macos/Sources/Features/App Intents/KeybindIntent.swift @@ -26,7 +26,7 @@ struct KeybindIntent: AppIntent { guard await requestIntentPermission() else { throw GhosttyIntentError.permissionDenied } - + guard let surface = terminal.surfaceModel else { throw GhosttyIntentError.surfaceNotFound } diff --git a/macos/Sources/Features/App Intents/NewTerminalIntent.swift b/macos/Sources/Features/App Intents/NewTerminalIntent.swift index 6de9e1e7e..858d5ceb0 100644 --- a/macos/Sources/Features/App Intents/NewTerminalIntent.swift +++ b/macos/Sources/Features/App Intents/NewTerminalIntent.swift @@ -152,7 +152,7 @@ enum NewTerminalLocation: String { case splitRight = "split:right" case splitUp = "split:up" case splitDown = "split:down" - + var splitDirection: SplitTree.NewDirection? { switch self { case .splitLeft: return .left diff --git a/macos/Sources/Features/App Intents/QuickTerminalIntent.swift b/macos/Sources/Features/App Intents/QuickTerminalIntent.swift index 2048a3b88..df0fe17a5 100644 --- a/macos/Sources/Features/App Intents/QuickTerminalIntent.swift +++ b/macos/Sources/Features/App Intents/QuickTerminalIntent.swift @@ -15,7 +15,7 @@ struct QuickTerminalIntent: AppIntent { guard await requestIntentPermission() else { throw GhosttyIntentError.permissionDenied } - + guard let delegate = NSApp.delegate as? AppDelegate else { throw GhosttyIntentError.appUnavailable } diff --git a/macos/Sources/Features/ClipboardConfirmation/ClipboardConfirmationController.swift b/macos/Sources/Features/ClipboardConfirmation/ClipboardConfirmationController.swift index 2040dcfae..37b20afb0 100644 --- a/macos/Sources/Features/ClipboardConfirmation/ClipboardConfirmationController.swift +++ b/macos/Sources/Features/ClipboardConfirmation/ClipboardConfirmationController.swift @@ -13,7 +13,7 @@ class ClipboardConfirmationController: NSWindowController { let contents: String let request: Ghostty.ClipboardRequest let state: UnsafeMutableRawPointer? - weak private var delegate: ClipboardConfirmationViewDelegate? = nil + weak private var delegate: ClipboardConfirmationViewDelegate? init(surface: ghostty_surface_t, contents: String, request: Ghostty.ClipboardRequest, state: UnsafeMutableRawPointer?, delegate: ClipboardConfirmationViewDelegate) { self.surface = surface @@ -28,12 +28,12 @@ class ClipboardConfirmationController: NSWindowController { fatalError("init(coder:) is not supported for this view") } - //MARK: - NSWindowController + // MARK: - NSWindowController override func windowDidLoad() { guard let window = window else { return } - switch (request) { + switch request { case .paste: window.title = "Warning: Potentially Unsafe Paste" case .osc_52_read, .osc_52_write: diff --git a/macos/Sources/Features/ClipboardConfirmation/ClipboardConfirmationView.swift b/macos/Sources/Features/ClipboardConfirmation/ClipboardConfirmationView.swift index 6423e3cf6..17ab4aa24 100644 --- a/macos/Sources/Features/ClipboardConfirmation/ClipboardConfirmationView.swift +++ b/macos/Sources/Features/ClipboardConfirmation/ClipboardConfirmationView.swift @@ -7,7 +7,7 @@ protocol ClipboardConfirmationViewDelegate: AnyObject { /// The SwiftUI view for showing a clipboard confirmation dialog. struct ClipboardConfirmationView: View { - enum Action : String { + enum Action: String { case cancel case confirm @@ -32,7 +32,7 @@ struct ClipboardConfirmationView: View { let request: Ghostty.ClipboardRequest /// Optional delegate to get results. If this is nil, then this view will never close on its own. - weak var delegate: ClipboardConfirmationViewDelegate? = nil + weak var delegate: ClipboardConfirmationViewDelegate? /// Used to track if we should rehide on disappear @State private var cursorHiddenCount: UInt = 0 @@ -45,16 +45,16 @@ struct ClipboardConfirmationView: View { .font(.system(size: 42)) .padding() .frame(alignment: .center) - + Text(request.text()) .frame(maxWidth: .infinity, alignment: .leading) .padding() } - + TextEditor(text: .constant(contents)) .focusable(false) .font(.system(.body, design: .monospaced)) - + HStack { Spacer() Button(Action.text(.cancel, request)) { onCancel() } @@ -74,7 +74,7 @@ struct ClipboardConfirmationView: View { // If we didn't unhide anything, we just send an unhide to be safe. // I don't think the count can go negative on NSCursor so this handles // scenarios cursor is hidden outside of our own NSCursor usage. - if (cursorHiddenCount == 0) { + if cursorHiddenCount == 0 { _ = Cursor.unhide() } } diff --git a/macos/Sources/Features/Colorized Ghostty Icon/ColorizedGhosttyIcon.swift b/macos/Sources/Features/Colorized Ghostty Icon/ColorizedGhosttyIcon.swift index 58de8f771..e58699cff 100644 --- a/macos/Sources/Features/Colorized Ghostty Icon/ColorizedGhosttyIcon.swift +++ b/macos/Sources/Features/Colorized Ghostty Icon/ColorizedGhosttyIcon.swift @@ -19,7 +19,7 @@ struct ColorizedGhosttyIcon { guard let crt = NSImage(named: "CustomIconCRT") else { return nil } guard let gloss = NSImage(named: "CustomIconGloss") else { return nil } - let baseName = switch (frame) { + let baseName = switch frame { case .aluminum: "CustomIconBaseAluminum" case .beige: "CustomIconBaseBeige" case .chrome: "CustomIconBaseChrome" diff --git a/macos/Sources/Features/Command Palette/CommandPalette.swift b/macos/Sources/Features/Command Palette/CommandPalette.swift index 235881dde..10c56f8dd 100644 --- a/macos/Sources/Features/Command Palette/CommandPalette.swift +++ b/macos/Sources/Features/Command Palette/CommandPalette.swift @@ -23,7 +23,7 @@ struct CommandOption: Identifiable, Hashable { let sortKey: AnySortKey? /// The action to perform when this option is selected. let action: () -> Void - + init( title: String, subtitle: String? = nil, @@ -78,7 +78,7 @@ struct CommandPaletteView: View { ($0.subtitle?.localizedCaseInsensitiveContains(query) ?? false) || colorMatchScore(for: $0.leadingColor, query: query) > 0 } - + // Sort by color match score (higher scores first), then maintain original order return filtered.sorted { a, b in let scoreA = colorMatchScore(for: a.leadingColor, query: query) @@ -106,7 +106,7 @@ struct CommandPaletteView: View { VStack(alignment: .leading, spacing: 0) { CommandPaletteQuery(query: $query, isTextFieldFocused: _isTextFieldFocused) { event in - switch (event) { + switch event { case .exit: isPresented = false @@ -128,7 +128,7 @@ struct CommandPaletteView: View { ? 0 : current + 1 - case .move(_): + case .move: // Unknown, ignore break } @@ -200,20 +200,20 @@ struct CommandPaletteView: View { isTextFieldFocused = isPresented } } - + /// Returns a score (0.0 to 1.0) indicating how well a color matches a search query color name. /// Returns 0 if no color name in the query matches, or if the color is nil. private func colorMatchScore(for color: Color?, query: String) -> Double { guard let color = color else { return 0 } - + let queryLower = query.lowercased() let nsColor = NSColor(color) - + var bestScore: Double = 0 for name in NSColor.colorNames { guard queryLower.contains(name), let systemColor = NSColor(named: name) else { continue } - + let distance = nsColor.distance(to: systemColor) // Max distance in weighted RGB space is ~3.0, so normalize and invert // Use a threshold to determine "close enough" matches @@ -223,15 +223,15 @@ struct CommandPaletteView: View { bestScore = max(bestScore, score) } } - + return bestScore } } /// The text field for building the query for the command palette. -fileprivate struct CommandPaletteQuery: View { +private struct CommandPaletteQuery: View { @Binding var query: String - var onEvent: ((KeyboardEvent) -> Void)? = nil + var onEvent: ((KeyboardEvent) -> Void)? @FocusState private var isTextFieldFocused: Bool init(query: Binding, isTextFieldFocused: FocusState, onEvent: ((KeyboardEvent) -> Void)? = nil) { @@ -284,7 +284,7 @@ fileprivate struct CommandPaletteQuery: View { } } -fileprivate struct CommandTable: View { +private struct CommandTable: View { var options: [CommandOption] @Binding var selectedIndex: UInt? @Binding var hoveredOptionID: UUID? @@ -332,7 +332,7 @@ fileprivate struct CommandTable: View { } /// A single row in the command palette. -fileprivate struct CommandRow: View { +private struct CommandRow: View { let option: CommandOption var isSelected: Bool @Binding var hoveredID: UUID? @@ -346,26 +346,26 @@ fileprivate struct CommandRow: View { .fill(color) .frame(width: 8, height: 8) } - + if let icon = option.leadingIcon { Image(systemName: icon) .foregroundStyle(option.emphasis ? Color.accentColor : .secondary) .font(.system(size: 14, weight: .medium)) } - + VStack(alignment: .leading, spacing: 2) { Text(option.title) .fontWeight(option.emphasis ? .medium : .regular) - + if let subtitle = option.subtitle { Text(subtitle) .font(.caption) .foregroundStyle(.secondary) } } - + Spacer() - + if let badge = option.badge, !badge.isEmpty { Text(badge) .font(.caption2.weight(.medium)) @@ -376,7 +376,7 @@ fileprivate struct CommandRow: View { ) .foregroundStyle(Color.accentColor) } - + if let symbols = option.symbols { ShortcutSymbolsView(symbols: symbols) .foregroundStyle(.secondary) @@ -406,7 +406,7 @@ fileprivate struct CommandRow: View { } /// A row of Text representing a shortcut. -fileprivate struct ShortcutSymbolsView: View { +private struct ShortcutSymbolsView: View { let symbols: [String] var body: some View { diff --git a/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift b/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift index 9bdf4b4ff..70d1273a2 100644 --- a/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift +++ b/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift @@ -11,7 +11,7 @@ struct TerminalCommandPaletteView: View { /// The configuration so we can lookup keyboard shortcuts. @ObservedObject var ghosttyConfig: Ghostty.Config - + /// The update view model for showing update commands. var updateViewModel: UpdateViewModel? @@ -54,13 +54,13 @@ struct TerminalCommandPaletteView: View { } } } - + /// 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. @@ -83,11 +83,11 @@ struct TerminalCommandPaletteView: View { /// 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 @@ -96,7 +96,7 @@ struct TerminalCommandPaletteView: View { } else { title = updateViewModel.text } - + options.append(CommandOption( title: title, description: updateViewModel.description, @@ -106,14 +106,14 @@ struct TerminalCommandPaletteView: View { ) { (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 } @@ -171,7 +171,7 @@ struct TerminalCommandPaletteView: View { } /// This is done to ensure that the given view is in the responder chain. -fileprivate struct ResponderChainInjector: NSViewRepresentable { +private struct ResponderChainInjector: NSViewRepresentable { let responder: NSResponder func makeNSView(context: Context) -> NSView { diff --git a/macos/Sources/Features/Global Keybinds/GlobalEventTap.swift b/macos/Sources/Features/Global Keybinds/GlobalEventTap.swift index ae77535be..9d4023c2e 100644 --- a/macos/Sources/Features/Global Keybinds/GlobalEventTap.swift +++ b/macos/Sources/Features/Global Keybinds/GlobalEventTap.swift @@ -16,11 +16,11 @@ class GlobalEventTap { // The event tap used for global event listening. This is non-nil if it is // created. - private var eventTap: CFMachPort? = nil + private var eventTap: CFMachPort? // This is the timer used to retry enabling the global event tap if we // don't have permissions. - private var enableTimer: Timer? = nil + private var enableTimer: Timer? // Private init so it can't be constructed outside of our singleton private init() {} @@ -33,7 +33,7 @@ class GlobalEventTap { // If enabling fails due to permissions, this will start a timer to retry since // accessibility permissions take affect immediately. func enable() { - if (eventTap != nil) { + if eventTap != nil { // Already enabled return } @@ -44,7 +44,7 @@ class GlobalEventTap { } // Try to enable the event tap immediately. If this succeeds then we're done! - if (tryEnable()) { + if tryEnable() { return } @@ -117,7 +117,7 @@ class GlobalEventTap { } } -fileprivate func cgEventFlagsChangedHandler( +private func cgEventFlagsChangedHandler( proxy: CGEventTapProxy, type: CGEventType, cgEvent: CGEvent, @@ -142,7 +142,7 @@ fileprivate func cgEventFlagsChangedHandler( // Build our event input and call ghostty let key_ev = event.ghosttyKeyEvent(GHOSTTY_ACTION_PRESS) - if (ghostty_app_key(ghostty, key_ev)) { + if ghostty_app_key(ghostty, key_ev) { GlobalEventTap.logger.info("global key event handled event=\(event)") return nil } diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift index 07c0c4c19..de1ea903d 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift @@ -16,20 +16,20 @@ class QuickTerminalController: BaseTerminalController { /// The previously running application when the terminal is shown. This is NEVER Ghostty. /// If this is set then when the quick terminal is animated out then we will restore this /// application to the front. - private var previousApp: NSRunningApplication? = nil + private var previousApp: NSRunningApplication? // The active space when the quick terminal was last shown. - private var previousActiveSpace: CGSSpace? = nil + private var previousActiveSpace: CGSSpace? /// Cache for per-screen window state. let screenStateCache: QuickTerminalScreenStateCache /// Non-nil if we have hidden dock state. - private var hiddenDock: HiddenDock? = nil + private var hiddenDock: HiddenDock? /// The configuration derived from the Ghostty config so we don't need to rely on references. private var derivedConfig: DerivedConfig - + /// Tracks if we're currently handling a manual resize to prevent recursion private var isHandlingResize: Bool = false @@ -135,14 +135,14 @@ class QuickTerminalController: BaseTerminalController { if let qtWindow = window as? QuickTerminalWindow { qtWindow.initialFrame = window.frame } - + // Setup our content window.contentView = TerminalViewContainer( ghostty: self.ghostty, viewModel: self, delegate: self ) - + // Clear out our frame at this point, the fixup from above is complete. if let qtWindow = window as? QuickTerminalWindow { qtWindow.initialFrame = nil @@ -234,7 +234,7 @@ class QuickTerminalController: BaseTerminalController { // Prevent recursive loops isHandlingResize = true defer { isHandlingResize = false } - + switch position { case .top, .bottom, .center: // For centered positions (top, bottom, center), we need to recenter the window @@ -316,7 +316,7 @@ class QuickTerminalController: BaseTerminalController { // MARK: Methods func toggle() { - if (visible) { + if visible { animateOut() } else { animateIn() @@ -340,8 +340,7 @@ class QuickTerminalController: BaseTerminalController { // we want to store it so we can restore state later. if !NSApp.isActive { if let previousApp = NSWorkspace.shared.frontmostApplication, - previousApp.bundleIdentifier != Bundle.main.bundleIdentifier - { + previousApp.bundleIdentifier != Bundle.main.bundleIdentifier { self.previousApp = previousApp } } @@ -370,7 +369,7 @@ class QuickTerminalController: BaseTerminalController { } else { var config = Ghostty.SurfaceConfiguration() config.environmentVariables["GHOSTTY_QUICK_TERMINAL"] = "1" - + let view = Ghostty.SurfaceView(ghostty_app, baseConfig: config) surfaceTree = SplitTree(view: view) focusedSurface = view @@ -417,7 +416,7 @@ class QuickTerminalController: BaseTerminalController { private func animateWindowIn(window: NSWindow, from position: QuickTerminalPosition) { guard let screen = derivedConfig.quickTerminalScreen.screen else { return } - + // Grab our last closed frame to use from the cache. let closedFrame = screenStateCache.frame(for: screen) @@ -441,7 +440,7 @@ class QuickTerminalController: BaseTerminalController { // If our dock position would conflict with our target location then // we autohide the dock. if position.conflictsWithDock(on: screen) { - if (hiddenDock == nil) { + if hiddenDock == nil { hiddenDock = .init() } @@ -675,10 +674,10 @@ class QuickTerminalController: BaseTerminalController { // We ignore the configured fullscreen style and always use non-native // because the way the quick terminal works doesn't support native. let mode: FullscreenMode - if (NSApp.isFrontmost) { + if NSApp.isFrontmost { // If we're frontmost and we have a notch then we keep padding // so all lines of the terminal are visible. - if (window?.screen?.hasNotch ?? false) { + if window?.screen?.hasNotch ?? false { mode = .nonNativePaddedNotch } else { mode = .nonNative diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalPosition.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalPosition.swift index d7660f77a..8742a7836 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalPosition.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalPosition.swift @@ -1,6 +1,6 @@ import Cocoa -enum QuickTerminalPosition : String { +enum QuickTerminalPosition: String { case top case bottom case left @@ -64,7 +64,7 @@ enum QuickTerminalPosition : String { /// The initial point origin for this position. func initialOrigin(for window: NSWindow, on screen: NSScreen) -> CGPoint { - switch (self) { + switch self { case .top: return .init( x: round(screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2), @@ -86,13 +86,13 @@ enum QuickTerminalPosition : String { y: round(screen.visibleFrame.origin.y + (screen.visibleFrame.height - window.frame.height) / 2)) case .center: - return .init(x: round(screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2), y: screen.visibleFrame.height - window.frame.width) + return .init(x: round(screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2), y: screen.visibleFrame.height - window.frame.width) } } /// The final point origin for this position. func finalOrigin(for window: NSWindow, on screen: NSScreen) -> CGPoint { - switch (self) { + switch self { case .top: return .init( x: round(screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2), @@ -128,7 +128,7 @@ enum QuickTerminalPosition : String { // Depending on the orientation of the dock, we conflict if our quick terminal // would potentially "hit" the dock. In the future we should probably consider // the frame of the quick terminal. - return switch (orientation) { + return switch orientation { case .top: self == .top || self == .left || self == .right case .bottom: self == .bottom || self == .left || self == .right case .left: self == .top || self == .bottom @@ -144,25 +144,25 @@ enum QuickTerminalPosition : String { x: round(screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2), y: window.frame.origin.y // Keep the same Y position ) - + case .bottom: return CGPoint( x: round(screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2), y: window.frame.origin.y // Keep the same Y position ) - + case .center: return CGPoint( x: round(screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2), y: round(screen.visibleFrame.origin.y + (screen.visibleFrame.height - window.frame.height) / 2) ) - + case .left, .right: // For left/right positions, only adjust horizontal centering if needed return window.frame.origin } } - + /// Calculate the vertically centered origin for side-positioned windows func verticallyCenteredOrigin(for window: NSWindow, on screen: NSScreen) -> CGPoint { switch self { @@ -171,13 +171,13 @@ enum QuickTerminalPosition : String { x: window.frame.origin.x, // Keep the same X position y: round(screen.visibleFrame.origin.y + (screen.visibleFrame.height - window.frame.height) / 2) ) - + case .right: return CGPoint( x: window.frame.origin.x, // Keep the same X position y: round(screen.visibleFrame.origin.y + (screen.visibleFrame.height - window.frame.height) / 2) ) - + case .top, .bottom, .center: // These positions don't need vertical recentering during resize return window.frame.origin diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalScreen.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalScreen.swift index cd07a6f12..70af0a505 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalScreen.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalScreen.swift @@ -6,23 +6,23 @@ enum QuickTerminalScreen { case menuBar init?(fromGhosttyConfig string: String) { - switch (string) { + switch string { case "main": self = .main case "mouse": self = .mouse - + case "macos-menu-bar": self = .menuBar - + default: return nil } } var screen: NSScreen? { - switch (self) { + switch self { case .main: return NSScreen.main diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalScreenStateCache.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalScreenStateCache.swift index a1c17abb9..301865561 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalScreenStateCache.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalScreenStateCache.swift @@ -8,15 +8,15 @@ import Cocoa /// to survive NSScreen garbage collection and automatically prunes stale entries. class QuickTerminalScreenStateCache { typealias Entries = [UUID: DisplayEntry] - + /// The maximum number of saved screen states we retain. This is to avoid some kind of /// pathological memory growth in case we get our screen state serializing wrong. I don't /// know anyone with more than 10 screens, so let's just arbitrarily go with that. private static let maxSavedScreens = 10 - + /// Time-to-live for screen entries that are no longer present (14 days). private static let screenStaleTTL: TimeInterval = 14 * 24 * 60 * 60 - + /// Keyed by display UUID to survive NSScreen garbage collection. private(set) var stateByDisplay: Entries = [:] @@ -28,11 +28,11 @@ class QuickTerminalScreenStateCache { name: NSApplication.didChangeScreenParametersNotification, object: nil) } - + deinit { NotificationCenter.default.removeObserver(self) } - + /// Save the window frame for a screen. func save(frame: NSRect, for screen: NSScreen) { guard let key = screen.displayUUID else { return } @@ -45,27 +45,27 @@ class QuickTerminalScreenStateCache { stateByDisplay[key] = entry pruneCapacity() } - + /// Retrieve the last closed frame for a screen, if valid. func frame(for screen: NSScreen) -> NSRect? { guard let key = screen.displayUUID, var entry = stateByDisplay[key] else { return nil } - + // Drop on dimension/scale change that makes the entry invalid if !entry.isValid(for: screen) { stateByDisplay.removeValue(forKey: key) return nil } - + entry.lastSeen = Date() stateByDisplay[key] = entry return entry.frame } - + @objc private func onScreensChanged(_ note: Notification) { let screens = NSScreen.screens let now = Date() let currentIDs = Set(screens.compactMap { $0.displayUUID }) - + for screen in screens { guard let key = screen.displayUUID else { continue } if var entry = stateByDisplay[key] { @@ -80,15 +80,15 @@ class QuickTerminalScreenStateCache { } } } - + // TTL prune for non-present screens stateByDisplay = stateByDisplay.filter { key, entry in currentIDs.contains(key) || now.timeIntervalSince(entry.lastSeen) < Self.screenStaleTTL } - + pruneCapacity() } - + private func pruneCapacity() { guard stateByDisplay.count > Self.maxSavedScreens else { return } let toRemove = stateByDisplay @@ -98,13 +98,13 @@ class QuickTerminalScreenStateCache { stateByDisplay.removeValue(forKey: key) } } - + struct DisplayEntry: Codable { var frame: NSRect var screenSize: CGSize var scale: CGFloat var lastSeen: Date - + /// Returns true if this entry is still valid for the given screen. /// Valid if the scale matches and the cached size is not larger than the current screen size. /// This allows entries to persist when screens grow, but invalidates them when screens shrink. diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalSize.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalSize.swift index 08bbcb8d9..2cd11e42e 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalSize.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalSize.swift @@ -48,7 +48,6 @@ struct QuickTerminalSize { } } - /// This is an almost direct port of th Zig function QuickTerminalSize.calculate func calculate(position: QuickTerminalPosition, screenDimensions: CGSize) -> CGSize { let dims = CGSize(width: screenDimensions.width, height: screenDimensions.height) diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalSpaceBehavior.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalSpaceBehavior.swift index 0561aaa18..9f544b7e6 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalSpaceBehavior.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalSpaceBehavior.swift @@ -6,7 +6,7 @@ enum QuickTerminalSpaceBehavior { case move init?(fromGhosttyConfig string: String) { - switch (string) { + switch string { case "move": self = .move @@ -24,7 +24,7 @@ enum QuickTerminalSpaceBehavior { .fullScreenAuxiliary ] - switch (self) { + switch self { case .move: // We want this to move the window to the active space. return NSWindow.CollectionBehavior([.canJoinAllSpaces] + commonBehavior) diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalWindow.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalWindow.swift index 1a4170dbc..507ec1baf 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalWindow.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalWindow.swift @@ -5,18 +5,18 @@ class QuickTerminalWindow: NSPanel { // still become key/main and receive events. override var canBecomeKey: Bool { return true } override var canBecomeMain: Bool { return true } - + override func awakeFromNib() { super.awakeFromNib() // Note: almost all of this stuff can be done in the nib/xib directly // but I prefer to do it programmatically because the properties we // care about are less hidden. - + // Add a custom identifier so third party apps can use the Accessibility // API to apply special rules to the quick terminal. self.identifier = .init(rawValue: "com.mitchellh.ghostty.quickTerminal") - + // Set the correct AXSubrole of kAXFloatingWindowSubrole (allows // AeroSpace to treat the Quick Terminal as a floating window) self.setAccessibilitySubrole(.floatingWindow) @@ -32,8 +32,8 @@ class QuickTerminalWindow: NSPanel { /// This is set to the frame prior to setting `contentView`. This is purely a hack to workaround /// bugs in older macOS versions (Ventura): https://github.com/ghostty-org/ghostty/pull/8026 - var initialFrame: NSRect? = nil - + var initialFrame: NSRect? + override func setFrame(_ frameRect: NSRect, display flag: Bool) { // Upon first adding this Window to its host view, older SwiftUI // seems to have a "hiccup" and corrupts the frameRect, diff --git a/macos/Sources/Features/Secure Input/SecureInput.swift b/macos/Sources/Features/Secure Input/SecureInput.swift index f999ce5ca..261a38e5c 100644 --- a/macos/Sources/Features/Secure Input/SecureInput.swift +++ b/macos/Sources/Features/Secure Input/SecureInput.swift @@ -12,7 +12,7 @@ import OSLog // it. You have to yield secure input on application deactivation (because // it'll affect other apps) and reacquire on reactivation, and every enable // needs to be balanced with a disable. -class SecureInput : ObservableObject { +class SecureInput: ObservableObject { static let shared = SecureInput() private static let logger = Logger( @@ -90,12 +90,12 @@ class SecureInput : ObservableObject { guard enabled != desired else { return } let err: OSStatus - if (enabled) { + if enabled { err = DisableSecureEventInput() } else { err = EnableSecureEventInput() } - if (err == noErr) { + if err == noErr { enabled = desired Self.logger.debug("secure input state=\(self.enabled)") return @@ -111,7 +111,7 @@ class SecureInput : ObservableObject { // desire to be enabled. guard !enabled && desired else { return } let err = EnableSecureEventInput() - if (err == noErr) { + if err == noErr { enabled = true Self.logger.debug("secure input enabled on activation") return @@ -124,7 +124,7 @@ class SecureInput : ObservableObject { // We only want to disable if we're enabled. guard enabled else { return } let err = DisableSecureEventInput() - if (err == noErr) { + if err == noErr { enabled = false Self.logger.debug("secure input disabled on deactivation") return diff --git a/macos/Sources/Features/Secure Input/SecureInputOverlay.swift b/macos/Sources/Features/Secure Input/SecureInputOverlay.swift index 96f309de5..8d1332174 100644 --- a/macos/Sources/Features/Secure Input/SecureInputOverlay.swift +++ b/macos/Sources/Features/Secure Input/SecureInputOverlay.swift @@ -44,9 +44,9 @@ struct SecureInputOverlay: View { .padding(.trailing, 10) .popover(isPresented: $isPopover, arrowEdge: .bottom) { Text(""" - Secure Input is active. Secure Input is a macOS security feature that - prevents applications from reading keyboard events. This is enabled - automatically whenever Ghostty detects a password prompt in the terminal, + Secure Input is active. Secure Input is a macOS security feature that + prevents applications from reading keyboard events. This is enabled + automatically whenever Ghostty detects a password prompt in the terminal, or at all times if `Ghostty > Secure Keyboard Entry` is active. """) .padding(.all) diff --git a/macos/Sources/Features/Services/ServiceProvider.swift b/macos/Sources/Features/Services/ServiceProvider.swift index f165769a7..9bf46fcf9 100644 --- a/macos/Sources/Features/Services/ServiceProvider.swift +++ b/macos/Sources/Features/Services/ServiceProvider.swift @@ -50,7 +50,7 @@ class ServiceProvider: NSObject { var config = Ghostty.SurfaceConfiguration() config.workingDirectory = url.path(percentEncoded: false) - switch (target) { + switch target { case .window: _ = TerminalController.newWindow(delegate.ghostty, withBaseConfig: config) diff --git a/macos/Sources/Features/Settings/ConfigurationErrorsController.swift b/macos/Sources/Features/Settings/ConfigurationErrorsController.swift index b2550b94e..06fcebda3 100644 --- a/macos/Sources/Features/Settings/ConfigurationErrorsController.swift +++ b/macos/Sources/Features/Settings/ConfigurationErrorsController.swift @@ -12,13 +12,13 @@ class ConfigurationErrorsController: NSWindowController, NSWindowDelegate, Confi /// The data model for this view. Update this directly and the associated view will be updated, too. @Published var errors: [String] = [] { didSet { - if (errors.count == 0) { + if errors.count == 0 { self.window?.performClose(nil) } } } - //MARK: - NSWindowController + // MARK: - NSWindowController override func windowWillLoad() { shouldCascadeWindows = false diff --git a/macos/Sources/Features/Splits/SplitTree.swift b/macos/Sources/Features/Splits/SplitTree.swift index 2fb83e64c..86932b1bb 100644 --- a/macos/Sources/Features/Splits/SplitTree.swift +++ b/macos/Sources/Features/Splits/SplitTree.swift @@ -222,7 +222,7 @@ extension SplitTree { case .split: // If the best candidate is a split node, use its the leaf/rightmost // depending on our spatial direction. - return switch (spatialDirection) { + return switch spatialDirection { case .up, .left: bestNode.node.leftmostLeaf() case .down, .right: bestNode.node.rightmostLeaf() } @@ -343,7 +343,7 @@ extension SplitTree { // MARK: SplitTree Codable -fileprivate enum CodingKeys: String, CodingKey { +private enum CodingKeys: String, CodingKey { case version case root case zoomed @@ -422,7 +422,7 @@ extension SplitTree.Node { /// Returns the node in the tree that contains the given view. func node(view: ViewType) -> Node? { - switch (self) { + switch self { case .leaf(view): return self @@ -728,7 +728,6 @@ extension SplitTree.Node { } } - /// Calculate the bounds of all views in this subtree based on split ratios func calculateViewBounds(in bounds: CGRect) -> [(view: ViewType, bounds: CGRect)] { switch self { diff --git a/macos/Sources/Features/Splits/SplitView.Divider.swift b/macos/Sources/Features/Splits/SplitView.Divider.swift index a01175dce..59a10ef60 100644 --- a/macos/Sources/Features/Splits/SplitView.Divider.swift +++ b/macos/Sources/Features/Splits/SplitView.Divider.swift @@ -10,7 +10,7 @@ extension SplitView { @Binding var split: CGFloat private var visibleWidth: CGFloat? { - switch (direction) { + switch direction { case .horizontal: return visibleSize case .vertical: @@ -19,7 +19,7 @@ extension SplitView { } private var visibleHeight: CGFloat? { - switch (direction) { + switch direction { case .horizontal: return nil case .vertical: @@ -28,7 +28,7 @@ extension SplitView { } private var invisibleWidth: CGFloat? { - switch (direction) { + switch direction { case .horizontal: return visibleSize + invisibleSize case .vertical: @@ -37,7 +37,7 @@ extension SplitView { } private var invisibleHeight: CGFloat? { - switch (direction) { + switch direction { case .horizontal: return nil case .vertical: @@ -46,7 +46,7 @@ extension SplitView { } private var pointerStyle: BackportPointerStyle { - return switch (direction) { + return switch direction { case .horizontal: .resizeLeftRight case .vertical: .resizeUpDown } @@ -69,8 +69,8 @@ extension SplitView { return } - if (isHovered) { - switch (direction) { + if isHovered { + switch direction { case .horizontal: NSCursor.resizeLeftRight.push() case .vertical: diff --git a/macos/Sources/Features/Splits/SplitView.swift b/macos/Sources/Features/Splits/SplitView.swift index 42de97590..a19fdca6a 100644 --- a/macos/Sources/Features/Splits/SplitView.swift +++ b/macos/Sources/Features/Splits/SplitView.swift @@ -90,7 +90,7 @@ struct SplitView: View { private func dragGesture(_ size: CGSize, splitterPoint: CGPoint) -> some Gesture { return DragGesture() .onChanged { gesture in - switch (direction) { + switch direction { case .horizontal: let new = min(max(minSize, gesture.location.x), size.width - minSize) split = new / size.width @@ -106,14 +106,14 @@ struct SplitView: View { private func leftRect(for size: CGSize) -> CGRect { // Initially the rect is the full size var result = CGRect(x: 0, y: 0, width: size.width, height: size.height) - switch (direction) { + switch direction { case .horizontal: - result.size.width = result.size.width * split + result.size.width *= split result.size.width -= splitterVisibleSize / 2 result.size.width -= result.size.width.truncatingRemainder(dividingBy: self.resizeIncrements.width) case .vertical: - result.size.height = result.size.height * split + result.size.height *= split result.size.height -= splitterVisibleSize / 2 result.size.height -= result.size.height.truncatingRemainder(dividingBy: self.resizeIncrements.height) } @@ -125,7 +125,7 @@ struct SplitView: View { private func rightRect(for size: CGSize, leftRect: CGRect) -> CGRect { // Initially the rect is the full size var result = CGRect(x: 0, y: 0, width: size.width, height: size.height) - switch (direction) { + switch direction { case .horizontal: // For horizontal layouts we offset the starting X by the left rect // and make the width fit the remaining space. @@ -144,7 +144,7 @@ struct SplitView: View { /// Calculates the point at which the splitter should be rendered. private func splitterPoint(for size: CGSize, leftRect: CGRect) -> CGPoint { - switch (direction) { + switch direction { case .horizontal: return CGPoint(x: leftRect.size.width, y: size.height / 2) @@ -152,9 +152,9 @@ struct SplitView: View { return CGPoint(x: size.width / 2, y: leftRect.size.height) } } - + // MARK: Accessibility - + private var splitViewLabel: String { switch direction { case .horizontal: @@ -163,7 +163,7 @@ struct SplitView: View { return "Vertical split view" } } - + private var leftPaneLabel: String { switch direction { case .horizontal: @@ -172,7 +172,7 @@ struct SplitView: View { return "Top pane" } } - + private var rightPaneLabel: String { switch direction { case .horizontal: diff --git a/macos/Sources/Features/Splits/TerminalSplitTreeView.swift b/macos/Sources/Features/Splits/TerminalSplitTreeView.swift index 2a42dc599..5fa12edeb 100644 --- a/macos/Sources/Features/Splits/TerminalSplitTreeView.swift +++ b/macos/Sources/Features/Splits/TerminalSplitTreeView.swift @@ -7,19 +7,19 @@ import SwiftUI enum TerminalSplitOperation { case resize(Resize) case drop(Drop) - + struct Resize { let node: SplitTree.Node let ratio: Double } - + struct Drop { /// The surface being dragged. let payload: Ghostty.SurfaceView - + /// The surface it was dragged onto let destination: Ghostty.SurfaceView - + /// The zone it was dropped to determine how to split the destination. let zone: TerminalSplitDropZone } @@ -44,7 +44,7 @@ struct TerminalSplitTreeView: View { } } -fileprivate struct TerminalSplitSubtreeView: View { +private struct TerminalSplitSubtreeView: View { @EnvironmentObject var ghostty: Ghostty.App let node: SplitTree.Node @@ -52,12 +52,12 @@ fileprivate struct TerminalSplitSubtreeView: View { let action: (TerminalSplitOperation) -> Void var body: some View { - switch (node) { + switch node { case .leaf(let leafView): TerminalSplitLeaf(surfaceView: leafView, isSplit: !isRoot, action: action) case .split(let split): - let splitViewDirection: SplitViewDirection = switch (split.direction) { + let splitViewDirection: SplitViewDirection = switch split.direction { case .horizontal: .horizontal case .vertical: .vertical } @@ -86,14 +86,14 @@ fileprivate struct TerminalSplitSubtreeView: View { } } -fileprivate struct TerminalSplitLeaf: View { +private struct TerminalSplitLeaf: View { let surfaceView: Ghostty.SurfaceView let isSplit: Bool let action: (TerminalSplitOperation) -> Void - + @State private var dropState: DropState = .idle @State private var isSelfDragging: Bool = false - + var body: some View { GeometryReader { geometry in Ghostty.InspectableSurface( @@ -129,26 +129,26 @@ fileprivate struct TerminalSplitLeaf: View { .accessibilityLabel("Terminal pane") } } - + private enum DropState: Equatable { case idle case dropping(TerminalSplitDropZone) } - + private struct SplitDropDelegate: DropDelegate { @Binding var dropState: DropState let viewSize: CGSize let destinationSurface: Ghostty.SurfaceView let action: (TerminalSplitOperation) -> Void - + func validateDrop(info: DropInfo) -> Bool { info.hasItemsConforming(to: [.ghosttySurfaceId]) } - + func dropEntered(info: DropInfo) { dropState = .dropping(.calculate(at: info.location, in: viewSize)) } - + func dropUpdated(info: DropInfo) -> DropProposal? { // For some reason dropUpdated is sent after performDrop is called // and we don't want to reset our drop zone to show it so we have @@ -157,11 +157,11 @@ fileprivate struct TerminalSplitLeaf: View { dropState = .dropping(.calculate(at: info.location, in: viewSize)) return DropProposal(operation: .move) } - + func dropExited(info: DropInfo) { dropState = .idle } - + func performDrop(info: DropInfo) -> Bool { let zone = TerminalSplitDropZone.calculate(at: info.location, in: viewSize) dropState = .idle @@ -169,7 +169,7 @@ fileprivate struct TerminalSplitLeaf: View { // Load the dropped surface asynchronously using Transferable let providers = info.itemProviders(for: [.ghosttySurfaceId]) guard let provider = providers.first else { return false } - + // Capture action before the async closure _ = provider.loadTransferable(type: Ghostty.SurfaceView.self) { [weak destinationSurface] result in switch result { @@ -180,12 +180,12 @@ fileprivate struct TerminalSplitLeaf: View { guard sourceSurface !== destinationSurface else { return } action(.drop(.init(payload: sourceSurface, destination: destinationSurface, zone: zone))) } - + case .failure: break } } - + return true } } diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index b739e9ed1..1cff80c52 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -31,13 +31,12 @@ class BaseTerminalController: NSWindowController, TerminalViewDelegate, TerminalViewModel, ClipboardConfirmationViewDelegate, - FullscreenDelegate -{ + FullscreenDelegate { /// The app instance that this terminal view will represent. let ghostty: Ghostty.App /// The currently focused surface. - var focusedSurface: Ghostty.SurfaceView? = nil { + var focusedSurface: Ghostty.SurfaceView? { didSet { syncFocusToSurfaceTree() } } @@ -48,7 +47,7 @@ class BaseTerminalController: NSWindowController, /// This can be set to show/hide the command palette. @Published var commandPaletteIsShowing: Bool = false - + /// Set if the terminal view should show the update overlay. @Published var updateOverlayIsVisible: Bool = false @@ -58,19 +57,19 @@ class BaseTerminalController: NSWindowController, } /// Non-nil when an alert is active so we don't overlap multiple. - private var alert: NSAlert? = nil + private var alert: NSAlert? /// The clipboard confirmation window, if shown. - private var clipboardConfirmation: ClipboardConfirmationController? = nil + private var clipboardConfirmation: ClipboardConfirmationController? /// Fullscreen state management. private(set) var fullscreenStyle: FullscreenStyle? /// Event monitor (see individual events for why) - private var eventMonitor: Any? = nil + private var eventMonitor: Any? /// The previous frame information from the window - private var savedFrame: SavedFrame? = nil + private var savedFrame: SavedFrame? /// Cache previously applied appearance to avoid unnecessary updates private var appliedColorScheme: ghostty_color_scheme_e? @@ -86,7 +85,7 @@ class BaseTerminalController: NSWindowController, /// An override title for the tab/window set by the user via prompt_tab_title. /// When set, this takes precedence over the computed title from the terminal. - var titleOverride: String? = nil { + var titleOverride: String? { didSet { applyTitleToWindow() } } @@ -281,7 +280,7 @@ class BaseTerminalController: NSWindowController, /// Subclasses should call super first. func surfaceTreeDidChange(from: SplitTree, to: SplitTree) { // If our surface tree becomes empty then we have no focused surface. - if (to.isEmpty) { + if to.isEmpty { focusedSurface = nil } } @@ -424,7 +423,7 @@ class BaseTerminalController: NSWindowController, /// Goes to previous split unless we're the leftmost leaf, then goes to next. private func findNextFocusTargetAfterClosing(node: SplitTree.Node) -> Ghostty.SurfaceView? { guard let root = surfaceTree.root else { return nil } - + // If we're the leftmost, then we move to the next surface after closing. // Otherwise, we move to the previous. if root.leftmostLeaf() == node.leftmostLeaf() { @@ -433,7 +432,7 @@ class BaseTerminalController: NSWindowController, return surfaceTree.focusTarget(for: .previous, from: node) } } - + /// Remove a node from the surface tree and move focus appropriately. /// /// This also updates the undo manager to support restoring this node. @@ -471,13 +470,13 @@ class BaseTerminalController: NSWindowController, Ghostty.moveFocus(to: newView, from: oldView) } } - + // Setup our undo guard let undoManager else { return } if let undoAction { undoManager.setActionName(undoAction) } - + undoManager.registerUndo( withTarget: self, expiresAfter: undoExpiration @@ -488,7 +487,7 @@ class BaseTerminalController: NSWindowController, Ghostty.moveFocus(to: oldView, from: target.focusedSurface) } } - + undoManager.registerUndo( withTarget: target, expiresAfter: target.undoExpiration @@ -531,14 +530,14 @@ class BaseTerminalController: NSWindowController, // then we let it stay that way. x: if newFrame.origin.x < visibleFrame.origin.x { if let savedFrame, savedFrame.window.origin.x < savedFrame.screen.origin.x { - break x; + break x } newFrame.origin.x = visibleFrame.origin.x } y: if newFrame.origin.y < visibleFrame.origin.y { if let savedFrame, savedFrame.window.origin.y < savedFrame.screen.origin.y { - break y; + break y } newFrame.origin.y = visibleFrame.origin.y @@ -596,7 +595,7 @@ class BaseTerminalController: NSWindowController, guard let directionAny = notification.userInfo?["direction"] else { return } guard let direction = directionAny as? ghostty_action_split_direction_e else { return } let splitDirection: SplitTree.NewDirection - switch (direction) { + switch direction { case GHOSTTY_SPLIT_DIRECTION_RIGHT: splitDirection = .right case GHOSTTY_SPLIT_DIRECTION_LEFT: splitDirection = .left case GHOSTTY_SPLIT_DIRECTION_DOWN: splitDirection = .down @@ -609,14 +608,14 @@ class BaseTerminalController: NSWindowController, @objc private func ghosttyDidEqualizeSplits(_ notification: Notification) { guard let target = notification.object as? Ghostty.SurfaceView else { return } - + // Check if target surface is in current controller's tree guard surfaceTree.contains(target) else { return } - + // Equalize the splits surfaceTree = surfaceTree.equalized() } - + @objc private func ghosttyDidFocusSplit(_ notification: Notification) { // The target must be within our tree guard let target = notification.object as? Ghostty.SurfaceView else { return } @@ -628,7 +627,7 @@ class BaseTerminalController: NSWindowController, // Find the node for the target surface guard let targetNode = surfaceTree.root?.node(view: target) else { return } - + // Find the next surface to focus guard let nextSurface = surfaceTree.focusTarget(for: direction.toSplitTreeFocusDirection(), from: targetNode) else { return @@ -649,7 +648,7 @@ class BaseTerminalController: NSWindowController, Ghostty.moveFocus(to: nextSurface, from: target) } } - + @objc private func ghosttyDidToggleSplitZoom(_ notification: Notification) { // The target must be within our tree guard let target = notification.object as? Ghostty.SurfaceView else { return } @@ -677,19 +676,19 @@ class BaseTerminalController: NSWindowController, Ghostty.moveFocus(to: target) } } - + @objc private func ghosttyDidResizeSplit(_ notification: Notification) { // The target must be within our tree guard let target = notification.object as? Ghostty.SurfaceView else { return } guard let targetNode = surfaceTree.root?.node(view: target) else { return } - + // Extract direction and amount from notification guard let directionAny = notification.userInfo?[Ghostty.Notification.ResizeSplitDirectionKey] else { return } guard let direction = directionAny as? Ghostty.SplitResizeDirection else { return } - + guard let amountAny = notification.userInfo?[Ghostty.Notification.ResizeSplitAmountKey] else { return } guard let amount = amountAny as? UInt16 else { return } - + // Convert Ghostty.SplitResizeDirection to SplitTree.Spatial.Direction let spatialDirection: SplitTree.Spatial.Direction switch direction { @@ -698,10 +697,10 @@ class BaseTerminalController: NSWindowController, case .left: spatialDirection = .left case .right: spatialDirection = .right } - + // Use viewBounds for the spatial calculation bounds let bounds = CGRect(origin: .zero, size: surfaceTree.viewBounds()) - + // Perform the resize using the new SplitTree resize method do { surfaceTree = try surfaceTree.resizing(node: targetNode, by: amount, in: spatialDirection, with: bounds) @@ -716,7 +715,7 @@ class BaseTerminalController: NSWindowController, // Bring the window to front and focus the surface. window?.makeKeyAndOrderFront(nil) - + // We use a small delay to ensure this runs after any UI cleanup // (e.g., command palette restoring focus to its original surface). Ghostty.moveFocus(to: target) @@ -729,11 +728,11 @@ class BaseTerminalController: NSWindowController, @objc private func ghosttySurfaceDragEndedNoTarget(_ notification: Notification) { guard let target = notification.object as? Ghostty.SurfaceView else { return } guard let targetNode = surfaceTree.root?.node(view: target) else { return } - + // If our tree isn't split, then we never create a new window, because // it is already a single split. guard surfaceTree.isSplit else { return } - + // If we are removing our focused surface then we move it. We need to // keep track of our old one so undo sends focus back to the right place. let oldFocusedSurface = focusedSurface @@ -746,14 +745,14 @@ class BaseTerminalController: NSWindowController, // Create a new tree with the dragged surface and open a new window let newTree = SplitTree(view: target) - + // Treat our undo below as a full group. undoManager?.beginUndoGrouping() undoManager?.setActionName("Move Split") defer { undoManager?.endUndoGrouping() } - + replaceSurfaceTree(removedTree, moveFocusFrom: oldFocusedSurface) _ = TerminalController.newWindow( ghostty, @@ -783,7 +782,7 @@ class BaseTerminalController: NSWindowController, if NSApp.mainWindow == window { surfaces = surfaces.filter { $0 != focusedSurface } } - + for surface in surfaces { surface.flagsChanged(with: event) } @@ -817,10 +816,10 @@ class BaseTerminalController: NSWindowController, titleDidChange(to: "👻") } } - + private func computeTitle(title: String, bell: Bool) -> String { var result = title - if (bell && ghostty.config.bellFeatures.contains(.title)) { + if bell && ghostty.config.bellFeatures.contains(.title) { result = "🔔 \(result)" } @@ -834,17 +833,17 @@ class BaseTerminalController: NSWindowController, private func applyTitleToWindow() { guard let window else { return } - + if let titleOverride { window.title = computeTitle( title: titleOverride, bell: focusedSurface?.bell ?? false) return } - + window.title = lastComputedTitle } - + func pwdDidChange(to: URL?) { guard let window else { return } @@ -856,7 +855,6 @@ class BaseTerminalController: NSWindowController, } } - func cellSizeDidChange(to: NSSize) { guard derivedConfig.windowStepResize else { return } // Stage manager can sometimes present windows in such a way that the @@ -896,7 +894,7 @@ class BaseTerminalController: NSWindowController, case .left: .left case .right: .right } - + // Check if source is in our tree if let sourceNode = surfaceTree.root?.node(view: source) { // Source is in our tree - same window move @@ -908,7 +906,7 @@ class BaseTerminalController: NSWindowController, Ghostty.logger.warning("failed to insert surface during drop: \(error)") return } - + replaceSurfaceTree( newTree, moveFocusTo: source, @@ -916,7 +914,7 @@ class BaseTerminalController: NSWindowController, undoAction: "Move Split") return } - + // Source is not in our tree - search other windows var sourceController: BaseTerminalController? var sourceNode: SplitTree.Node? @@ -929,12 +927,12 @@ class BaseTerminalController: NSWindowController, break } } - + guard let sourceController, let sourceNode else { Ghostty.logger.warning("source surface not found in any window during drop") return } - + // Remove from source controller's tree and add it to our tree. // We do this first because if there is an error then we can // abort. @@ -945,17 +943,17 @@ class BaseTerminalController: NSWindowController, Ghostty.logger.warning("failed to insert surface during cross-window drop: \(error)") return } - + // Treat our undo below as a full group. undoManager?.beginUndoGrouping() undoManager?.setActionName("Move Split") defer { undoManager?.endUndoGrouping() } - + // Remove the node from the source. sourceController.removeSurfaceNode(sourceNode) - + // Add in the surface to our tree replaceSurfaceTree( newTree, @@ -966,7 +964,7 @@ class BaseTerminalController: NSWindowController, func performAction(_ action: String, on surfaceView: Ghostty.SurfaceView) { guard let surface = surfaceView.surface else { return } let len = action.utf8CString.count - if (len == 0) { return } + if len == 0 { return } _ = action.withCString { cString in ghostty_surface_binding_action(surface, cString, UInt(len - 1)) } @@ -980,17 +978,17 @@ class BaseTerminalController: NSWindowController, func toggleBackgroundOpacity() { // Do nothing if config is already fully opaque guard ghostty.config.backgroundOpacity < 1 else { return } - + // Do nothing if in fullscreen (transparency doesn't apply in fullscreen) guard let window, !window.styleMask.contains(.fullScreen) else { return } // Toggle between transparent and opaque isBackgroundOpaque.toggle() - + // Update our appearance syncAppearance() } - + /// Override this to resync any appearance related properties. This will be called automatically /// when certain window properties change that affect appearance. The list below should be updated /// as we add new things: @@ -1052,7 +1050,7 @@ class BaseTerminalController: NSWindowController, func fullscreenDidChange() { guard let fullscreenStyle else { return } - + // When we enter fullscreen, we want to show the update overlay so that it // is easily visible. For native fullscreen this is visible by showing the // menubar but we don't want to rely on that. @@ -1061,7 +1059,7 @@ class BaseTerminalController: NSWindowController, } else { updateOverlayIsVisible = defaultUpdateOverlayVisibility() } - + // Always resync our appearance syncAppearance() } @@ -1109,7 +1107,7 @@ class BaseTerminalController: NSWindowController, window?.endSheet(ccWindow) } - switch (request) { + switch request { case let .osc_52_write(pasteboard): guard case .confirm = action else { break } let pb = pasteboard ?? NSPasteboard.general @@ -1117,7 +1115,7 @@ class BaseTerminalController: NSWindowController, pb.setString(cc.contents, forType: .string) case .osc_52_read, .paste: let str: String - switch (action) { + switch action { case .cancel: str = "" @@ -1146,26 +1144,26 @@ class BaseTerminalController: NSWindowController, fullscreenStyle = NativeFullscreen(window) fullscreenStyle?.delegate = self } - + // Set our update overlay state updateOverlayIsVisible = defaultUpdateOverlayVisibility() } - + func defaultUpdateOverlayVisibility() -> Bool { guard let window else { return true } - + // No titlebar we always show the update overlay because it can't support // updates in the titlebar guard window.styleMask.contains(.titled) else { return true } - + // If it's a non terminal window we can't trust it has an update accessory, // so we always want to show the overlay. guard let window = window as? TerminalWindow else { return true } - + // Show the overlay if the window isn't. return !window.supportsUpdateAccessory } @@ -1295,7 +1293,6 @@ class BaseTerminalController: NSWindowController, ghostty.splitToggleZoom(surface: surface) } - @IBAction func splitMoveFocusPrevious(_ sender: Any) { splitMoveFocus(direction: .previous) } @@ -1368,7 +1365,7 @@ class BaseTerminalController: NSWindowController, @IBAction func toggleCommandPalette(_ sender: Any?) { commandPaletteIsShowing.toggle() } - + @IBAction func find(_ sender: Any) { focusedSurface?.find(sender) } @@ -1384,11 +1381,11 @@ class BaseTerminalController: NSWindowController, @IBAction func findNext(_ sender: Any) { focusedSurface?.findNext(sender) } - + @IBAction func findPrevious(_ sender: Any) { focusedSurface?.findNext(sender) } - + @IBAction func findHide(_ sender: Any) { focusedSurface?.findHide(sender) } @@ -1430,7 +1427,7 @@ extension BaseTerminalController: NSMenuItemValidation { return true } } - + // MARK: - Surface Color Scheme /// Update the surface tree's color scheme only when it actually changes. diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index fc23cc28f..2730a0436 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -8,16 +8,16 @@ import GhosttyKit class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Controller { override var windowNibName: NSNib.Name? { let defaultValue = "Terminal" - + guard let appDelegate = NSApp.delegate as? AppDelegate else { return defaultValue } let config = appDelegate.ghostty.config - + // If we have no window decorations, there's no reason to do anything but // the default titlebar (because there will be no titlebar). if !config.windowDecorations { return defaultValue } - + let nib = switch config.macosTitlebarStyle { case "native": "Terminal" case "hidden": "TerminalHiddenTitlebar" @@ -34,33 +34,32 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr #endif default: defaultValue } - + return nib } - + /// This is set to true when we care about frame changes. This is a small optimization since /// this controller registers a listener for ALL frame change notifications and this lets us bail /// early if we don't care. private var tabListenForFrame: Bool = false - + /// This is the hash value of the last tabGroup.windows array. We use this to detect order /// changes in the list. private var tabWindowsHash: Int = 0 - + /// This is set to false by init if the window managed by this controller should not be restorable. /// For example, terminals executing custom scripts are not restorable. private var restorable: Bool = true - + /// The configuration derived from the Ghostty config so we don't need to rely on references. private(set) var derivedConfig: DerivedConfig - - + /// The notification cancellable for focused surface property changes. private var surfaceAppearanceCancellables: Set = [] - + /// This will be set to the initial frame of the window from the xib on load. - private var initialFrame: NSRect? = nil - + private var initialFrame: NSRect? + init(_ ghostty: Ghostty.App, withBaseConfig base: Ghostty.SurfaceConfiguration? = nil, withSurfaceTree tree: SplitTree? = nil, @@ -72,12 +71,12 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr // as the script. We may want to revisit this behavior when we have scrollback // restoration. self.restorable = (base?.command ?? "") == "" - + // Setup our initial derived config based on the current app config self.derivedConfig = DerivedConfig(ghostty.config) - + super.init(ghostty, baseConfig: base, surfaceTree: tree) - + // Setup our notifications for behaviors let center = NotificationCenter.default center.addObserver( @@ -134,37 +133,37 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr object: nil ) } - + required init?(coder: NSCoder) { fatalError("init(coder:) is not supported for this view") } - + deinit { // Remove all of our notificationcenter subscriptions let center = NotificationCenter.default center.removeObserver(self) } - + // MARK: Base Controller Overrides - + override func surfaceTreeDidChange(from: SplitTree, to: SplitTree) { super.surfaceTreeDidChange(from: from, to: to) - + // Whenever our surface tree changes in any way (new split, close split, etc.) // we want to invalidate our state. invalidateRestorableState() - + // Update our zoom state if let window = window as? TerminalWindow { window.surfaceIsZoomed = to.zoomed != nil } - + // If our surface tree is now nil then we close our window. - if (to.isEmpty) { + if to.isEmpty { self.window?.close() } } - + override func replaceSurfaceTree( _ newTree: SplitTree, moveFocusTo newView: Ghostty.SurfaceView? = nil, @@ -177,7 +176,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr closeTabImmediately() return } - + super.replaceSurfaceTree( newTree, moveFocusTo: newView, @@ -210,7 +209,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr // to find the preferred window to attach new tabs, perform actions, etc. We // always prefer the main window but if there isn't any (because we're triggered // by something like an App Intent) then we prefer the most previous main. - static private(set) weak var lastMain: TerminalController? = nil + static private(set) weak var lastMain: TerminalController? /// The "new window" action. static func newWindow( @@ -253,7 +252,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr DispatchQueue.main.async { // Only cascade if we aren't fullscreen. if let window = c.window { - if (!window.styleMask.contains(.fullScreen)) { + if !window.styleMask.contains(.fullScreen) { Self.lastCascadePoint = window.cascadeTopLeft(from: Self.lastCascadePoint) } } @@ -390,7 +389,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr // If the parent is miniaturized, then macOS exhibits really strange behaviors // so we have to bring it back out. - if (parent.isMiniaturized) { parent.deminiaturize(self) } + if parent.isMiniaturized { parent.deminiaturize(self) } // If our parent tab group already has this window, macOS added it and // we need to remove it so we can set the correct order in the next line. @@ -405,7 +404,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr } // If we don't allow tabs then we create a new window instead. - if (window.tabbingMode != .disallowed) { + if window.tabbingMode != .disallowed { // Add the window to the tab group and show it. switch ghostty.config.windowNewTabPosition { case "end": @@ -481,8 +480,8 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr return controller } - - //MARK: - Methods + + // MARK: - Methods @objc private func ghosttyConfigDidChange(_ notification: Notification) { // Get our managed configuration object out @@ -491,7 +490,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr ] as? Ghostty.Config else { return } // If this is an app-level config update then we update some things. - if (notification.object == nil) { + if notification.object == nil { // Update our derived config self.derivedConfig = DerivedConfig(config) @@ -562,7 +561,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr tabWindowsHash = v self.relabelTabs() } - + override func syncAppearance() { // When our focus changes, we update our window appearance based on the // currently focused surface. @@ -907,7 +906,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr alert.addButton(withTitle: "Cancel") alert.alertStyle = .warning alert.beginSheetModal(for: confirmWindow, completionHandler: { response in - if (response == .alertFirstButtonReturn) { + if response == .alertFirstButtonReturn { // This is important so that we avoid losing focus when Stage // Manager is used (#8336) alert.window.orderOut(nil) @@ -936,9 +935,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr let tabColor: TerminalTabColor } - convenience init(_ ghostty: Ghostty.App, - with undoState: UndoState - ) { + convenience init(_ ghostty: Ghostty.App, with undoState: UndoState) { self.init(ghostty, withSurfaceTree: undoState.surfaceTree) // Show the window and restore its frame @@ -963,7 +960,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr // Make it the key window window.makeKeyAndOrderFront(nil) } - + // Restore focus to the previously focused surface if let focusedUUID = undoState.focusedSurface, let focusTarget = surfaceTree.first(where: { $0.id == focusedUUID }) { @@ -994,7 +991,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr tabColor: (window as? TerminalWindow)?.tabColor ?? .none) } - //MARK: - NSWindowController + // MARK: - NSWindowController override func windowWillLoad() { // We do NOT want to cascade because we handle this manually from the manager. @@ -1013,7 +1010,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr // Setting all three of these is required for restoration to work. window.isRestorable = restorable - if (restorable) { + if restorable { window.restorationClass = TerminalWindowRestoration.self window.identifier = .init(String(describing: TerminalWindowRestoration.self)) } @@ -1035,7 +1032,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr // If we have a default size, we want to apply it. if let defaultSize { - switch (defaultSize) { + switch defaultSize { case .frame: // Frames can be applied immediately defaultSize.apply(to: window) @@ -1071,7 +1068,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr // We don't run this logic in fullscreen because in fullscreen this will end up // removing the window and putting it into its own dedicated fullscreen, which is not // the expected or desired behavior of anyone I've found. - if (!window.styleMask.contains(.fullScreen)) { + if !window.styleMask.contains(.fullScreen) { // If we have more than 1 window in our tab group we know we're a new window. // Since Ghostty manages tabbing manually this will never be more than one // at this point in the AppKit lifecycle (we add to the group after this). @@ -1101,7 +1098,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr override func windowShouldClose(_ sender: NSWindow) -> Bool { tabGroupCloseCoordinator.windowShouldClose(sender) { [weak self] scope in guard let self else { return } - switch (scope) { + switch scope { case .tab: closeTab(nil) case .window: guard self.window?.isFirstWindowInTabGroup ?? false else { return } @@ -1131,7 +1128,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr // https://github.com/ghostty-org/ghostty/issues/2565 let oldFrame = focusedWindow.frame - Self.lastCascadePoint = focusedWindow.cascadeTopLeft(from: NSZeroPoint) + Self.lastCascadePoint = focusedWindow.cascadeTopLeft(from: .zero) if focusedWindow.frame != oldFrame { focusedWindow.setFrame(oldFrame, display: true) @@ -1315,7 +1312,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr ghostty.toggleTerminalInspector(surface: surface) } - //MARK: - TerminalViewDelegate + // MARK: - TerminalViewDelegate override func focusedSurfaceDidChange(to: Ghostty.SurfaceView?) { super.focusedSurfaceDidChange(to: to) @@ -1347,7 +1344,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr } } - //MARK: - Notifications + // MARK: - Notifications @objc private func onMoveTab(notification: SwiftUI.Notification) { guard let target = notification.object as? Ghostty.SurfaceView else { return } @@ -1430,23 +1427,23 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr let finalIndex: Int // An index that is invalid is used to signal some special values. - if (tabIndex <= 0) { + if tabIndex <= 0 { guard let selectedWindow = tabGroup.selectedWindow else { return } guard let selectedIndex = tabbedWindows.firstIndex(where: { $0 == selectedWindow }) else { return } - if (tabIndex == GHOSTTY_GOTO_TAB_PREVIOUS.rawValue) { - if (selectedIndex == 0) { + if tabIndex == GHOSTTY_GOTO_TAB_PREVIOUS.rawValue { + if selectedIndex == 0 { finalIndex = tabbedWindows.count - 1 } else { finalIndex = selectedIndex - 1 } - } else if (tabIndex == GHOSTTY_GOTO_TAB_NEXT.rawValue) { - if (selectedIndex == tabbedWindows.count - 1) { + } else if tabIndex == GHOSTTY_GOTO_TAB_NEXT.rawValue { + if selectedIndex == tabbedWindows.count - 1 { finalIndex = 0 } else { finalIndex = selectedIndex + 1 } - } else if (tabIndex == GHOSTTY_GOTO_TAB_LAST.rawValue) { + } else if tabIndex == GHOSTTY_GOTO_TAB_LAST.rawValue { finalIndex = tabbedWindows.count - 1 } else { return @@ -1548,24 +1545,24 @@ extension TerminalController { guard let window, let tabGroup = window.tabGroup else { return false } guard let currentIndex = tabGroup.windows.firstIndex(of: window) else { return false } return tabGroup.windows.enumerated().contains { $0.offset > currentIndex } - + case #selector(returnToDefaultSize): guard let window else { return false } - + // Native fullscreen windows can't revert to default size. if window.styleMask.contains(.fullScreen) { return false } - + // If we're fullscreen at all then we can't change size if fullscreenStyle?.isFullscreen ?? false { return false } - + // If our window is already the default size or we don't have a // default size, then disable. return defaultSize?.isChanged(for: window) ?? false - + default: return super.validateMenuItem(item) } diff --git a/macos/Sources/Features/Terminal/TerminalRestorable.swift b/macos/Sources/Features/Terminal/TerminalRestorable.swift index fd0f4eab5..a42d4c2f6 100644 --- a/macos/Sources/Features/Terminal/TerminalRestorable.swift +++ b/macos/Sources/Features/Terminal/TerminalRestorable.swift @@ -98,7 +98,7 @@ class TerminalWindowRestoration: NSObject, NSWindowRestoration { // no matter what. Note its safe to use "ghostty.config" directly here // because window restoration is only ever invoked on app start so we // don't have to deal with config reloads. - if (appDelegate.ghostty.config.windowSaveState == "never") { + if appDelegate.ghostty.config.windowSaveState == "never" { completionHandler(nil, nil) return } @@ -137,7 +137,7 @@ class TerminalWindowRestoration: NSObject, NSWindowRestoration { break } } - + if let view = foundView { c.focusedSurface = view restoreFocus(to: view, inWindow: window) @@ -161,9 +161,9 @@ class TerminalWindowRestoration: NSObject, NSWindowRestoration { // For the first attempt, we schedule it immediately. Subsequent events wait a bit // so we don't just spin the CPU at 100%. Give up after some period of time. let after: DispatchTime - if (attempts == 0) { + if attempts == 0 { after = .now() - } else if (attempts > 40) { + } else if attempts > 40 { // 2 seconds, give up return } else { @@ -185,11 +185,10 @@ class TerminalWindowRestoration: NSObject, NSWindowRestoration { // If the window is main, then we also make sure it comes forward. This // prevents a bug found in #1177 where sometimes on restore the windows // would be behind other applications. - if (viewWindow.isMainWindow) { + if viewWindow.isMainWindow { viewWindow.orderFront(nil) } } } } - diff --git a/macos/Sources/Features/Terminal/TerminalTabColor.swift b/macos/Sources/Features/Terminal/TerminalTabColor.swift index 08d89324c..2879822b3 100644 --- a/macos/Sources/Features/Terminal/TerminalTabColor.swift +++ b/macos/Sources/Features/Terminal/TerminalTabColor.swift @@ -122,7 +122,7 @@ struct TabColorMenuView: View { VStack(alignment: .leading, spacing: 3) { Text("Tab Color") .padding(.bottom, 2) - + ForEach(Self.paletteRows, id: \.self) { row in HStack(spacing: 2) { ForEach(row, id: \.self) { color in @@ -142,7 +142,7 @@ struct TabColorMenuView: View { .padding(.top, 4) .padding(.bottom, 4) } - + static let paletteRows: [[TerminalTabColor]] = [ [.none, .blue, .purple, .pink, .red], [.orange, .yellow, .green, .teal, .graphite], diff --git a/macos/Sources/Features/Terminal/TerminalView.swift b/macos/Sources/Features/Terminal/TerminalView.swift index e117e0647..1aab8f497 100644 --- a/macos/Sources/Features/Terminal/TerminalView.swift +++ b/macos/Sources/Features/Terminal/TerminalView.swift @@ -17,7 +17,7 @@ protocol TerminalViewDelegate: AnyObject { /// Perform an action. At the time of writing this is only triggered by the command palette. func performAction(_ action: String, on: Ghostty.SurfaceView) - + /// A split tree operation func performSplitAction(_ action: TerminalSplitOperation) } @@ -32,7 +32,7 @@ protocol TerminalViewModel: ObservableObject { /// The command palette state. var commandPaletteIsShowing: Bool { get set } - + /// The update overlay should be visible. var updateOverlayIsVisible: Bool { get } } @@ -45,8 +45,8 @@ struct TerminalView: View { @ObservedObject var viewModel: ViewModel // An optional delegate to receive information about terminal changes. - weak var delegate: (any TerminalViewDelegate)? = nil - + weak var delegate: (any TerminalViewDelegate)? + // The most recently focused surface, equal to focusedSurface when // it is non-nil. @State private var lastFocusedSurface: Weak = .init() @@ -76,7 +76,7 @@ struct TerminalView: View { VStack(spacing: 0) { // If we're running in debug mode we show a warning so that users // know that performance will be degraded. - if (Ghostty.info.mode == GHOSTTY_BUILD_MODE_DEBUG || Ghostty.info.mode == GHOSTTY_BUILD_MODE_RELEASE_SAFE) { + if Ghostty.info.mode == GHOSTTY_BUILD_MODE_DEBUG || Ghostty.info.mode == GHOSTTY_BUILD_MODE_RELEASE_SAFE { DebugBuildWarningView() } @@ -116,7 +116,7 @@ struct TerminalView: View { self.delegate?.performAction(action, on: surfaceView) } } - + // Show update information above all else. if viewModel.updateOverlayIsVisible { UpdateOverlay() @@ -127,12 +127,12 @@ struct TerminalView: View { } } -fileprivate struct UpdateOverlay: View { +private struct UpdateOverlay: View { var body: some View { if let appDelegate = NSApp.delegate as? AppDelegate { VStack { Spacer() - + HStack { Spacer() UpdatePill(model: appDelegate.updateViewModel) diff --git a/macos/Sources/Features/Terminal/Window Styles/HiddenTitlebarTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/HiddenTitlebarTerminalWindow.swift index dd8b258f3..766ec5857 100644 --- a/macos/Sources/Features/Terminal/Window Styles/HiddenTitlebarTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/HiddenTitlebarTerminalWindow.swift @@ -3,7 +3,7 @@ import AppKit class HiddenTitlebarTerminalWindow: TerminalWindow { // No titlebar, we don't support accessories. override var supportsUpdateAccessory: Bool { false } - + override func awakeFromNib() { super.awakeFromNib() @@ -34,7 +34,7 @@ class HiddenTitlebarTerminalWindow: TerminalWindow { .closable, .miniaturizable, ] - + /// Apply the hidden titlebar style. private func reapplyHiddenStyle() { // If our window is fullscreen then we don't reapply the hidden style because @@ -43,7 +43,7 @@ class HiddenTitlebarTerminalWindow: TerminalWindow { if terminalController?.fullscreenStyle?.isFullscreen ?? false { return } - + // Apply our style mask while preserving the .fullScreen option if styleMask.contains(.fullScreen) { styleMask = Self.hiddenStyleMask.union([.fullScreen]) diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift index 501ac0e67..cde8d2747 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -33,9 +33,9 @@ class TerminalWindow: NSWindow { /// The configuration derived from the Ghostty config so we don't need to rely on references. private(set) var derivedConfig: DerivedConfig = .init() - + /// Sets up our tab context menu - private var tabMenuObserver: NSObjectProtocol? = nil + private var tabMenuObserver: NSObjectProtocol? /// Whether this window supports the update accessory. If this is false, then views within this /// window should determine how to show update notifications. @@ -112,7 +112,7 @@ class TerminalWindow: NSWindow { } // If window decorations are disabled, remove our title - if (!config.windowDecorations) { styleMask.remove(.titled) } + if !config.windowDecorations { styleMask.remove(.titled) } // Set our window positioning to coordinates if config value exists, otherwise // fallback to original centering behavior @@ -295,7 +295,7 @@ class TerminalWindow: NSWindow { // MARK: Tab Key Equivalents - var keyEquivalent: String? = nil { + var keyEquivalent: String? { didSet { // When our key equivalent is set, we must update the tab label. guard let keyEquivalent else { @@ -347,7 +347,7 @@ class TerminalWindow: NSWindow { button.toolTip = "Reset Zoom" button.contentTintColor = isMainWindow ? .controlAccentColor : .secondaryLabelColor button.state = .on - button.image = NSImage(named:"ResetZoom") + button.image = NSImage(named: "ResetZoom") button.frame = NSRect(x: 0, y: 0, width: 20, height: 20) button.translatesAutoresizingMaskIntoConstraints = false button.widthAnchor.constraint(equalToConstant: 20).isActive = true @@ -449,8 +449,7 @@ class TerminalWindow: NSWindow { let forceOpaque = terminalController?.isBackgroundOpaque ?? false if !styleMask.contains(.fullScreen) && !forceOpaque && - (surfaceConfig.backgroundOpacity < 1 || surfaceConfig.backgroundBlur.isGlassStyle) - { + (surfaceConfig.backgroundOpacity < 1 || surfaceConfig.backgroundBlur.isGlassStyle) { isOpaque = false // This is weird, but we don't use ".clear" because this creates a look that @@ -459,7 +458,7 @@ class TerminalWindow: NSWindow { backgroundColor = .white.withAlphaComponent(0.001) // We don't need to set blur when using glass - if !surfaceConfig.backgroundBlur.isGlassStyle, let appDelegate = NSApp.delegate as? AppDelegate { + if !surfaceConfig.backgroundBlur.isGlassStyle, let appDelegate = NSApp.delegate as? AppDelegate { ghostty_set_window_background_blur( appDelegate.ghostty.app, Unmanaged.passUnretained(self).toOpaque()) @@ -510,7 +509,7 @@ class TerminalWindow: NSWindow { private func setInitialWindowPosition(x: Int16?, y: Int16?) { // If we don't have an X/Y then we try to use the previously saved window pos. guard x != nil, y != nil else { - if (!LastWindowPosition.shared.restore(self)) { + if !LastWindowPosition.shared.restore(self) { center() } @@ -544,7 +543,7 @@ class TerminalWindow: NSWindow { NotificationCenter.default.removeObserver(observer) } } - + // MARK: Config struct DerivedConfig { diff --git a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift index 918191522..184614831 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift @@ -8,7 +8,7 @@ import SwiftUI class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSToolbarDelegate { /// The view model for SwiftUI views private var viewModel = ViewModel() - + /// Titlebar tabs can't support the update accessory because of the way we layout /// the native tabs back into the menu bar. override var supportsUpdateAccessory: Bool { false } @@ -58,13 +58,13 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool // Check if we have a tab bar and set it up if we have to. See the comment // on this function to learn why we need to check this here. setupTabBar() - + viewModel.isMainWindow = true } override func resignMain() { super.resignMain() - + viewModel.isMainWindow = false } @@ -84,18 +84,18 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool super.sendEvent(event) return } - + guard let tabBarView else { super.sendEvent(event) return } - + let locationInTabBar = tabBarView.convert(event.locationInWindow, from: nil) guard tabBarView.bounds.contains(locationInTabBar) else { super.sendEvent(event) return } - + tabBarView.rightMouseDown(with: event) } @@ -107,7 +107,7 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool // After dragging a tab into a new window, `hasTabBar` needs to be // updated to properly review window title viewModel.hasTabBar = false - + super.addTitlebarAccessoryViewController(childViewController) return } @@ -116,7 +116,7 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool // system will also try to add tab bar to this window, so we want to reset observer, // to put tab bar where we want again tabBarObserver = nil - + // Some setup needs to happen BEFORE it is added, such as layout. If // we don't do this before the call below, we'll trigger an AppKit // assertion. @@ -189,7 +189,7 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool guard let clipView = tabBarView.firstSuperview(withClassName: "NSTitlebarAccessoryClipView") else { return } guard let accessoryView = clipView.subviews[safe: 0] else { return } guard let toolbarView = titlebarView.firstDescendant(withClassName: "NSToolbarView") else { return } - + // Make sure tabBar's height won't be stretched guard let newTabButton = titlebarView.firstDescendant(withClassName: "NSTabBarNewTabButton") else { return } tabBarView.frame.size.height = newTabButton.frame.width @@ -199,7 +199,7 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool // The padding for the tab bar. If we're showing window buttons then // we need to offset the window buttons. - let leftPadding: CGFloat = switch(self.derivedConfig.macosWindowButtons) { + let leftPadding: CGFloat = switch self.derivedConfig.macosWindowButtons { case .hidden: 0 case .visible: 70 } @@ -282,7 +282,7 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool // This is the documented way to avoid the glass view on an item. // We don't want glass on our title. item.isBordered = false - + return item default: return NSToolbarItem(itemIdentifier: itemIdentifier) @@ -327,7 +327,7 @@ extension TitlebarTabsTahoeTerminalWindow { Color.clear.frame(width: 1, height: 1) } } - + @ViewBuilder var titleText: some View { Text(title) diff --git a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift index 39db13c6d..fe83fc5fd 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift @@ -20,13 +20,11 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { // false if all three traffic lights are missing/hidden, otherwise true private var hasWindowButtons: Bool { - get { - // if standardWindowButton(.theButton) == nil, the button isn't there, so coalesce to true - let closeIsHidden = standardWindowButton(.closeButton)?.isHiddenOrHasHiddenAncestor ?? true - let miniaturizeIsHidden = standardWindowButton(.miniaturizeButton)?.isHiddenOrHasHiddenAncestor ?? true - let zoomIsHidden = standardWindowButton(.zoomButton)?.isHiddenOrHasHiddenAncestor ?? true - return !(closeIsHidden && miniaturizeIsHidden && zoomIsHidden) - } + // if standardWindowButton(.theButton) == nil, the button isn't there, so coalesce to true + let closeIsHidden = standardWindowButton(.closeButton)?.isHiddenOrHasHiddenAncestor ?? true + let miniaturizeIsHidden = standardWindowButton(.miniaturizeButton)?.isHiddenOrHasHiddenAncestor ?? true + let zoomIsHidden = standardWindowButton(.zoomButton)?.isHiddenOrHasHiddenAncestor ?? true + return !(closeIsHidden && miniaturizeIsHidden && zoomIsHidden) } // MARK: NSWindow @@ -159,7 +157,7 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { titlebarColor = derivedConfig.backgroundColor.withAlphaComponent(derivedConfig.backgroundOpacity) } - if (isOpaque || themeChanged) { + if isOpaque || themeChanged { // If there is transparency, calling this will make the titlebar opaque // so we only call this if we are opaque. updateTabBar() @@ -172,7 +170,7 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { backgroundColor.luminance < 0.05 } - private var newTabButtonImageLayer: VibrantLayer? = nil + private var newTabButtonImageLayer: VibrantLayer? func updateTabBar() { newTabButtonImageLayer = nil @@ -251,7 +249,7 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { button.toolTip = "Reset Zoom" button.contentTintColor = .controlAccentColor button.state = .on - button.image = NSImage(named:"ResetZoom") + button.image = NSImage(named: "ResetZoom") button.frame = NSRect(x: 0, y: 0, width: 20, height: 20) button.translatesAutoresizingMaskIntoConstraints = false button.widthAnchor.constraint(equalToConstant: 20).isActive = true @@ -286,9 +284,9 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { // MARK: - Titlebar Tabs - private var windowButtonsBackdrop: WindowButtonsBackdropView? = nil + private var windowButtonsBackdrop: WindowButtonsBackdropView? - private var windowDragHandle: WindowDragView? = nil + private var windowDragHandle: WindowDragView? // Used by the window controller to enable/disable titlebar tabs. var titlebarTabs = false { @@ -340,7 +338,6 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { } } - // HACK: hide the "collapsed items" marker from the toolbar if it's present. // idk why it appears in macOS 15.0+ but it does... so... make it go away. (sigh) private func hideToolbarOverflowButton() { @@ -359,7 +356,7 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { override func addTitlebarAccessoryViewController(_ childViewController: NSTitlebarAccessoryViewController) { let isTabBar = self.titlebarTabs && isTabBar(childViewController) - if (isTabBar) { + if isTabBar { // Ensure it has the right layoutAttribute to force it next to our titlebar childViewController.layoutAttribute = .right @@ -374,7 +371,7 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { super.addTitlebarAccessoryViewController(childViewController) - if (isTabBar) { + if isTabBar { pushTabsToTitlebar(childViewController) } } @@ -382,7 +379,7 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { override func removeTitlebarAccessoryViewController(at index: Int) { let isTabBar = titlebarAccessoryViewControllers[index].identifier == Self.tabBarIdentifier super.removeTitlebarAccessoryViewController(at: index) - if (isTabBar) { + if isTabBar { resetCustomTabBarViews() } } @@ -403,7 +400,7 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { private func pushTabsToTitlebar(_ tabBarController: NSTitlebarAccessoryViewController) { // We need a toolbar as a target for our titlebar tabs. - if (toolbar == nil) { + if toolbar == nil { generateToolbar() } @@ -506,10 +503,10 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { } // Passes mouseDown events from this view to window.performDrag so that you can drag the window by it. -fileprivate class WindowDragView: NSView { +private class WindowDragView: NSView { override public func mouseDown(with event: NSEvent) { // Drag the window for single left clicks, double clicks should bypass the drag handle. - if (event.type == .leftMouseDown && event.clickCount == 1) { + if event.type == .leftMouseDown && event.clickCount == 1 { window?.performDrag(with: event) NSCursor.closedHand.set() } else { @@ -535,7 +532,7 @@ fileprivate class WindowDragView: NSView { } // A view that matches the color of selected and unselected tabs in the adjacent tab bar. -fileprivate class WindowButtonsBackdropView: NSView { +private class WindowButtonsBackdropView: NSView { // This must be weak because the window has this view. Otherwise // a retain cycle occurs. private weak var terminalWindow: TitlebarTabsVenturaTerminalWindow? @@ -588,7 +585,7 @@ fileprivate class WindowButtonsBackdropView: NSView { // Custom NSToolbar subclass that displays a centered window title, // in order to accommodate the titlebar tabs feature. -fileprivate class TerminalToolbar: NSToolbar, NSToolbarDelegate { +private class TerminalToolbar: NSToolbar, NSToolbarDelegate { private let titleTextField = CenteredDynamicLabel(labelWithString: "👻 Ghostty") var titleText: String { @@ -674,7 +671,7 @@ fileprivate class TerminalToolbar: NSToolbar, NSToolbarDelegate { } /// A label that expands to fit whatever text you put in it and horizontally centers itself in the current window. -fileprivate class CenteredDynamicLabel: NSTextField { +private class CenteredDynamicLabel: NSTextField { override func viewDidMoveToSuperview() { // Configure the text field isEditable = false diff --git a/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift index a72436d7f..a547d5286 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift @@ -151,7 +151,7 @@ class TransparentTitlebarTerminalWindow: TerminalWindow { tabGroupWindowsObservation = tabGroup.observe( \.windows, options: [.new] - ) { [weak self] _, change in + ) { [weak self] _, _ in // NOTE: At one point, I guarded this on only if we went from 0 to N // or N to 0 under the assumption that the tab bar would only get // replaced on those cases. This turned out to be false (Tahoe). @@ -175,7 +175,7 @@ class TransparentTitlebarTerminalWindow: TerminalWindow { tabBarVisibleObservation = tabGroup?.observe( \.isTabBarVisible, options: [.new] - ) { [weak self] _, change in + ) { [weak self] _, _ in guard let self else { return } guard let lastSurfaceConfig else { return } self.syncAppearance(lastSurfaceConfig) diff --git a/macos/Sources/Features/Update/UpdateBadge.swift b/macos/Sources/Features/Update/UpdateBadge.swift index 054fdf971..ce98bd277 100644 --- a/macos/Sources/Features/Update/UpdateBadge.swift +++ b/macos/Sources/Features/Update/UpdateBadge.swift @@ -9,15 +9,15 @@ import SwiftUI struct UpdateBadge: View { /// The update view model that provides the current state and progress @ObservedObject var model: UpdateViewModel - + /// Current rotation angle for animated icon states @State private var rotationAngle: Double = 0 - + var body: some View { badgeContent .accessibilityLabel(model.text) } - + @ViewBuilder private var badgeContent: some View { switch model.state { @@ -28,10 +28,10 @@ struct UpdateBadge: View { } else { Image(systemName: "arrow.down.circle") } - + case .extracting(let extracting): ProgressRingView(progress: min(1, max(0, extracting.progress))) - + case .checking: if let iconName = model.iconName { Image(systemName: iconName) @@ -47,7 +47,7 @@ struct UpdateBadge: View { } else { EmptyView() } - + default: if let iconName = model.iconName { Image(systemName: iconName) @@ -61,18 +61,18 @@ struct UpdateBadge: View { /// A circular progress indicator with a stroke-based ring design. /// /// Displays a partially filled circle that represents progress from 0.0 to 1.0. -fileprivate struct ProgressRingView: View { +private struct ProgressRingView: View { /// The current progress value, ranging from 0.0 (empty) to 1.0 (complete) let progress: Double - + /// The width of the progress ring stroke let lineWidth: CGFloat = 2 - + var body: some View { ZStack { Circle() .stroke(Color.primary.opacity(0.2), lineWidth: lineWidth) - + Circle() .trim(from: 0, to: progress) .stroke(Color.primary, style: StrokeStyle(lineWidth: lineWidth, lineCap: .round)) diff --git a/macos/Sources/Features/Update/UpdateController.swift b/macos/Sources/Features/Update/UpdateController.swift index 939eed420..1ca218c8b 100644 --- a/macos/Sources/Features/Update/UpdateController.swift +++ b/macos/Sources/Features/Update/UpdateController.swift @@ -11,16 +11,16 @@ class UpdateController { private(set) var updater: SPUUpdater private let userDriver: UpdateDriver private var installCancellable: AnyCancellable? - + var viewModel: UpdateViewModel { userDriver.viewModel } - + /// True if we're installing an update. var isInstalling: Bool { installCancellable != nil } - + /// Initialize a new update controller. init() { let hostBundle = Bundle.main @@ -34,11 +34,11 @@ class UpdateController { delegate: userDriver ) } - + deinit { installCancellable?.cancel() } - + /// Start the updater. /// /// This must be called before the updater can check for updates. If starting fails, @@ -59,35 +59,35 @@ class UpdateController { )) } } - + /// Force install the current update. As long as we're in some "update available" state this will /// trigger all the steps necessary to complete the update. func installUpdate() { // Must be in an installable state guard viewModel.state.isInstallable else { return } - + // If we're already force installing then do nothing. guard installCancellable == nil else { return } - + // Setup a combine listener to listen for state changes and to always // confirm them. If we go to a non-installable state, cancel the listener. // The sink runs immediately with the current state, so we don't need to // manually confirm the first state. installCancellable = viewModel.$state.sink { [weak self] state in guard let self else { return } - + // If we move to a non-installable state (error, idle, etc.) then we // stop force installing. guard state.isInstallable else { self.installCancellable = nil return } - + // Continue the `yes` chain! state.confirm() } } - + /// Check for updates. /// /// This is typically connected to a menu item action. @@ -97,11 +97,11 @@ class UpdateController { updater.checkForUpdates() return } - + // If we're not idle then we need to cancel any prior state. installCancellable?.cancel() viewModel.state.cancel() - + // The above will take time to settle, so we delay the check for some time. // The 100ms is arbitrary and I'd rather not, but we have to wait more than // one loop tick it seems. @@ -109,7 +109,7 @@ class UpdateController { self?.updater.checkForUpdates() } } - + /// Validate the check for updates menu item. /// /// - Parameter item: The menu item to validate diff --git a/macos/Sources/Features/Update/UpdateDelegate.swift b/macos/Sources/Features/Update/UpdateDelegate.swift index 619540851..72d54bd22 100644 --- a/macos/Sources/Features/Update/UpdateDelegate.swift +++ b/macos/Sources/Features/Update/UpdateDelegate.swift @@ -6,11 +6,11 @@ extension UpdateDriver: SPUUpdaterDelegate { guard let appDelegate = NSApplication.shared.delegate as? AppDelegate else { return nil } - + // Sparkle supports a native concept of "channels" but it requires that // you share a single appcast file. We don't want to do that so we // do this instead. - switch (appDelegate.ghostty.config.autoUpdateChannel) { + switch appDelegate.ghostty.config.autoUpdateChannel { case .tip: return "https://tip.files.ghostty.org/appcast.xml" case .stable: return "https://release.files.ghostty.org/appcast.xml" } diff --git a/macos/Sources/Features/Update/UpdateDriver.swift b/macos/Sources/Features/Update/UpdateDriver.swift index 3beb4c9be..b5f580f1b 100644 --- a/macos/Sources/Features/Update/UpdateDriver.swift +++ b/macos/Sources/Features/Update/UpdateDriver.swift @@ -5,23 +5,23 @@ import Sparkle class UpdateDriver: NSObject, SPUUserDriver { let viewModel: UpdateViewModel let standard: SPUStandardUserDriver - + init(viewModel: UpdateViewModel, hostBundle: Bundle) { self.viewModel = viewModel self.standard = SPUStandardUserDriver(hostBundle: hostBundle, delegate: nil) super.init() - + NotificationCenter.default.addObserver( self, selector: #selector(handleTerminalWindowWillClose), name: TerminalWindow.terminalWillCloseNotification, object: nil) } - + deinit { NotificationCenter.default.removeObserver(self) } - + @objc private func handleTerminalWindowWillClose() { // If we lost the ability to show unobtrusive states, cancel whatever // update state we're in. This will allow the manual `check for updates` @@ -36,7 +36,7 @@ class UpdateDriver: NSObject, SPUUserDriver { viewModel.state = .idle } } - + func show(_ request: SPUUpdatePermissionRequest, reply: @escaping @Sendable (SUUpdatePermissionResponse) -> Void) { viewModel.state = .permissionRequest(.init(request: request, reply: { [weak viewModel] response in @@ -47,7 +47,7 @@ class UpdateDriver: NSObject, SPUUserDriver { standard.show(request, reply: reply) } } - + func showUserInitiatedUpdateCheck(cancellation: @escaping () -> Void) { viewModel.state = .checking(.init(cancel: cancellation)) @@ -55,7 +55,7 @@ class UpdateDriver: NSObject, SPUUserDriver { standard.showUserInitiatedUpdateCheck(cancellation: cancellation) } } - + func showUpdateFound(with appcastItem: SUAppcastItem, state: SPUUserUpdateState, reply: @escaping @Sendable (SPUUserUpdateChoice) -> Void) { @@ -64,25 +64,25 @@ class UpdateDriver: NSObject, SPUUserDriver { standard.showUpdateFound(with: appcastItem, state: state, reply: reply) } } - + func showUpdateReleaseNotes(with downloadData: SPUDownloadData) { // We don't do anything with the release notes here because Ghostty // doesn't use the release notes feature of Sparkle currently. } - + func showUpdateReleaseNotesFailedToDownloadWithError(_ error: any Error) { // We don't do anything with release notes. See `showUpdateReleaseNotes` } - + func showUpdateNotFoundWithError(_ error: any Error, acknowledgement: @escaping () -> Void) { viewModel.state = .notFound(.init(acknowledgement: acknowledgement)) - + if !hasUnobtrusiveTarget { standard.showUpdateNotFoundWithError(error, acknowledgement: acknowledgement) } } - + func showUpdaterError(_ error: any Error, acknowledgement: @escaping () -> Void) { viewModel.state = .error(.init( @@ -98,71 +98,71 @@ class UpdateDriver: NSObject, SPUUserDriver { dismiss: { [weak viewModel] in viewModel?.state = .idle })) - + if !hasUnobtrusiveTarget { standard.showUpdaterError(error, acknowledgement: acknowledgement) } else { acknowledgement() } } - + func showDownloadInitiated(cancellation: @escaping () -> Void) { viewModel.state = .downloading(.init( cancel: cancellation, expectedLength: nil, progress: 0)) - + if !hasUnobtrusiveTarget { standard.showDownloadInitiated(cancellation: cancellation) } } - + func showDownloadDidReceiveExpectedContentLength(_ expectedContentLength: UInt64) { guard case let .downloading(downloading) = viewModel.state else { return } - + viewModel.state = .downloading(.init( cancel: downloading.cancel, expectedLength: expectedContentLength, progress: 0)) - + if !hasUnobtrusiveTarget { standard.showDownloadDidReceiveExpectedContentLength(expectedContentLength) } } - + func showDownloadDidReceiveData(ofLength length: UInt64) { guard case let .downloading(downloading) = viewModel.state else { return } - + viewModel.state = .downloading(.init( cancel: downloading.cancel, expectedLength: downloading.expectedLength, progress: downloading.progress + length)) - + if !hasUnobtrusiveTarget { standard.showDownloadDidReceiveData(ofLength: length) } } - + func showDownloadDidStartExtractingUpdate() { viewModel.state = .extracting(.init(progress: 0)) - + if !hasUnobtrusiveTarget { standard.showDownloadDidStartExtractingUpdate() } } - + func showExtractionReceivedProgress(_ progress: Double) { viewModel.state = .extracting(.init(progress: progress)) - + if !hasUnobtrusiveTarget { standard.showExtractionReceivedProgress(progress) } } - + func showReady(toInstallAndRelaunch reply: @escaping @Sendable (SPUUserUpdateChoice) -> Void) { if !hasUnobtrusiveTarget { standard.showReady(toInstallAndRelaunch: reply) @@ -170,7 +170,7 @@ class UpdateDriver: NSObject, SPUUserDriver { reply(.install) } } - + func showInstallingUpdate(withApplicationTerminated applicationTerminated: Bool, retryTerminatingApplication: @escaping () -> Void) { viewModel.state = .installing(.init( retryTerminatingApplication: retryTerminatingApplication, @@ -178,30 +178,30 @@ class UpdateDriver: NSObject, SPUUserDriver { viewModel?.state = .idle } )) - + if !hasUnobtrusiveTarget { standard.showInstallingUpdate(withApplicationTerminated: applicationTerminated, retryTerminatingApplication: retryTerminatingApplication) } } - + func showUpdateInstalledAndRelaunched(_ relaunched: Bool, acknowledgement: @escaping () -> Void) { standard.showUpdateInstalledAndRelaunched(relaunched, acknowledgement: acknowledgement) viewModel.state = .idle } - + func showUpdateInFocus() { if !hasUnobtrusiveTarget { standard.showUpdateInFocus() } } - + func dismissUpdateInstallation() { viewModel.state = .idle standard.dismissUpdateInstallation() } - + // MARK: No-Window Fallback - + /// True if there is a target that can render our unobtrusive update checker. var hasUnobtrusiveTarget: Bool { NSApp.windows.contains { window in diff --git a/macos/Sources/Features/Update/UpdatePill.swift b/macos/Sources/Features/Update/UpdatePill.swift index 29d1669e1..53dfbe842 100644 --- a/macos/Sources/Features/Update/UpdatePill.swift +++ b/macos/Sources/Features/Update/UpdatePill.swift @@ -4,16 +4,16 @@ import SwiftUI struct UpdatePill: View { /// The update view model that provides the current state and information @ObservedObject var model: UpdateViewModel - + /// Whether the update popover is currently visible @State private var showPopover = false - + /// Task for auto-dismissing the "No Updates" state @State private var resetTask: Task? - + /// The font used for the pill text private let textFont = NSFont.systemFont(ofSize: 11, weight: .medium) - + var body: some View { if !model.state.isIdle { pillButton @@ -36,7 +36,7 @@ struct UpdatePill: View { } } } - + /// The pill-shaped button view that displays the update badge and text @ViewBuilder private var pillButton: some View { @@ -51,7 +51,7 @@ struct UpdatePill: View { HStack(spacing: 6) { UpdateBadge(model: model) .frame(width: 14, height: 14) - + Text(model.text) .font(Font(textFont)) .lineLimit(1) @@ -71,7 +71,7 @@ struct UpdatePill: View { .help(model.text) .accessibilityLabel(model.text) } - + /// Calculated width for the text to prevent resizing during progress updates private var textWidth: CGFloat? { let attributes: [NSAttributedString.Key: Any] = [.font: textFont] diff --git a/macos/Sources/Features/Update/UpdatePopoverView.swift b/macos/Sources/Features/Update/UpdatePopoverView.swift index 87d76f801..aa4e822f3 100644 --- a/macos/Sources/Features/Update/UpdatePopoverView.swift +++ b/macos/Sources/Features/Update/UpdatePopoverView.swift @@ -8,10 +8,10 @@ import Sparkle struct UpdatePopoverView: View { /// The update view model that provides the current state and information @ObservedObject var model: UpdateViewModel - + /// Environment value for dismissing the popover @Environment(\.dismiss) private var dismiss - + var body: some View { VStack(alignment: .leading, spacing: 0) { switch model.state { @@ -19,31 +19,31 @@ struct UpdatePopoverView: View { // Shouldn't happen in a well-formed view stack. Higher levels // should not call the popover for idles. EmptyView() - + case .permissionRequest(let request): PermissionRequestView(request: request, dismiss: dismiss) - + case .checking(let checking): CheckingView(checking: checking, dismiss: dismiss) - + case .updateAvailable(let update): UpdateAvailableView(update: update, dismiss: dismiss) - + case .downloading(let download): DownloadingView(download: download, dismiss: dismiss) - + case .extracting(let extracting): ExtractingView(extracting: extracting) - + case .installing(let installing): // This is only required when `installing.isAutoUpdate == true`, // but we keep it anyway, just in case something unexpected // happens during installing InstallingView(installing: installing, dismiss: dismiss) - + case .notFound(let notFound): NotFoundView(notFound: notFound, dismiss: dismiss) - + case .error(let error): UpdateErrorView(error: error, dismiss: dismiss) } @@ -52,22 +52,22 @@ struct UpdatePopoverView: View { } } -fileprivate struct PermissionRequestView: View { +private struct PermissionRequestView: View { let request: UpdateState.PermissionRequest let dismiss: DismissAction - + var body: some View { VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 8) { Text("Enable automatic updates?") .font(.system(size: 13, weight: .semibold)) - + Text("Ghostty can automatically check for updates in the background.") .font(.system(size: 11)) .foregroundColor(.secondary) .fixedSize(horizontal: false, vertical: true) } - + HStack(spacing: 8) { Button("Not Now") { request.reply(SUUpdatePermissionResponse( @@ -76,9 +76,9 @@ fileprivate struct PermissionRequestView: View { dismiss() } .keyboardShortcut(.cancelAction) - + Spacer() - + Button("Allow") { request.reply(SUUpdatePermissionResponse( automaticUpdateChecks: true, @@ -93,10 +93,10 @@ fileprivate struct PermissionRequestView: View { } } -fileprivate struct CheckingView: View { +private struct CheckingView: View { let checking: UpdateState.Checking let dismiss: DismissAction - + var body: some View { VStack(alignment: .leading, spacing: 16) { HStack(spacing: 10) { @@ -105,7 +105,7 @@ fileprivate struct CheckingView: View { Text("Checking for updates…") .font(.system(size: 13)) } - + HStack { Spacer() Button("Cancel") { @@ -120,19 +120,19 @@ fileprivate struct CheckingView: View { } } -fileprivate struct UpdateAvailableView: View { +private struct UpdateAvailableView: View { let update: UpdateState.UpdateAvailable let dismiss: DismissAction - + private let labelWidth: CGFloat = 60 - + var body: some View { VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: 8) { Text("Update Available") .font(.system(size: 13, weight: .semibold)) - + VStack(alignment: .leading, spacing: 4) { HStack(spacing: 6) { Text("Version:") @@ -141,7 +141,7 @@ fileprivate struct UpdateAvailableView: View { Text(update.appcastItem.displayVersionString) } .font(.system(size: 11)) - + if update.appcastItem.contentLength > 0 { HStack(spacing: 6) { Text("Size:") @@ -151,7 +151,7 @@ fileprivate struct UpdateAvailableView: View { } .font(.system(size: 11)) } - + if let date = update.appcastItem.date { HStack(spacing: 6) { Text("Released:") @@ -164,23 +164,23 @@ fileprivate struct UpdateAvailableView: View { } .textSelection(.enabled) } - + HStack(spacing: 8) { Button("Skip") { update.reply(.skip) dismiss() } .controlSize(.small) - + Button("Later") { update.reply(.dismiss) dismiss() } .controlSize(.small) .keyboardShortcut(.cancelAction) - + Spacer() - + Button("Install and Relaunch") { update.reply(.install) dismiss() @@ -191,10 +191,10 @@ fileprivate struct UpdateAvailableView: View { } } .padding(16) - + if let notes = update.releaseNotes { Divider() - + Link(destination: notes.url) { HStack { Image(systemName: "doc.text") @@ -217,16 +217,16 @@ fileprivate struct UpdateAvailableView: View { } } -fileprivate struct DownloadingView: View { +private struct DownloadingView: View { let download: UpdateState.Downloading let dismiss: DismissAction - + var body: some View { VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 8) { Text("Downloading Update") .font(.system(size: 13, weight: .semibold)) - + if let expectedLength = download.expectedLength, expectedLength > 0 { let progress = min(1, max(0, Double(download.progress) / Double(expectedLength))) VStack(alignment: .leading, spacing: 6) { @@ -240,7 +240,7 @@ fileprivate struct DownloadingView: View { .controlSize(.small) } } - + HStack { Spacer() Button("Cancel") { @@ -255,14 +255,14 @@ fileprivate struct DownloadingView: View { } } -fileprivate struct ExtractingView: View { +private struct ExtractingView: View { let extracting: UpdateState.Extracting - + var body: some View { VStack(alignment: .leading, spacing: 8) { Text("Preparing Update") .font(.system(size: 13, weight: .semibold)) - + VStack(alignment: .leading, spacing: 6) { ProgressView(value: min(1, max(0, extracting.progress)), total: 1.0) Text(String(format: "%.0f%%", min(1, max(0, extracting.progress)) * 100)) @@ -274,22 +274,22 @@ fileprivate struct ExtractingView: View { } } -fileprivate struct InstallingView: View { +private struct InstallingView: View { let installing: UpdateState.Installing let dismiss: DismissAction - + var body: some View { VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 8) { Text("Restart Required") .font(.system(size: 13, weight: .semibold)) - + Text("The update is ready. Please restart the application to complete the installation.") .font(.system(size: 11)) .foregroundColor(.secondary) .fixedSize(horizontal: false, vertical: true) } - + HStack { Button("Restart Later") { installing.dismiss() @@ -297,9 +297,9 @@ fileprivate struct InstallingView: View { } .keyboardShortcut(.cancelAction) .controlSize(.small) - + Spacer() - + Button("Restart Now") { installing.retryTerminatingApplication() dismiss() @@ -313,22 +313,22 @@ fileprivate struct InstallingView: View { } } -fileprivate struct NotFoundView: View { +private struct NotFoundView: View { let notFound: UpdateState.NotFound let dismiss: DismissAction - + var body: some View { VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 8) { Text("No Updates Found") .font(.system(size: 13, weight: .semibold)) - + Text("You're already running the latest version.") .font(.system(size: 11)) .foregroundColor(.secondary) .fixedSize(horizontal: false, vertical: true) } - + HStack { Spacer() Button("OK") { @@ -343,10 +343,10 @@ fileprivate struct NotFoundView: View { } } -fileprivate struct UpdateErrorView: View { +private struct UpdateErrorView: View { let error: UpdateState.Error let dismiss: DismissAction - + var body: some View { VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 8) { @@ -357,13 +357,13 @@ fileprivate struct UpdateErrorView: View { Text("Update Failed") .font(.system(size: 13, weight: .semibold)) } - + Text(error.error.localizedDescription) .font(.system(size: 11)) .foregroundColor(.secondary) .fixedSize(horizontal: false, vertical: true) } - + HStack(spacing: 8) { Button("OK") { error.dismiss() @@ -371,9 +371,9 @@ fileprivate struct UpdateErrorView: View { } .keyboardShortcut(.cancelAction) .controlSize(.small) - + Spacer() - + Button("Retry") { error.retry() dismiss() diff --git a/macos/Sources/Features/Update/UpdateSimulator.swift b/macos/Sources/Features/Update/UpdateSimulator.swift index bf168d9fc..c893993e0 100644 --- a/macos/Sources/Features/Update/UpdateSimulator.swift +++ b/macos/Sources/Features/Update/UpdateSimulator.swift @@ -9,31 +9,31 @@ import Sparkle enum UpdateSimulator { /// Complete successful update flow: checking → available → download → extract → ready → install → idle case happyPath - + /// No updates available: checking (2s) → "No Updates Available" (3s) → idle case notFound - + /// Error during check: checking (2s) → error with retry callback case error - + /// Slower download for testing progress UI: checking → available → download (20 steps, ~10s) → extract → install case slowDownload - + /// Initial permission request flow: shows permission dialog → proceeds with happy path if accepted case permissionRequest - + /// User cancels during download: checking → available → download (5 steps) → cancels → idle case cancelDuringDownload - + /// User cancels while checking: checking (1s) → cancels → idle case cancelDuringChecking - + /// Shows the installing state with restart button: installing (stays until dismissed) case installing - + /// Simulates auto-update flow: goes directly to installing state without showing intermediate UI case autoUpdate - + func simulate(with viewModel: UpdateViewModel) { switch self { case .happyPath: @@ -56,12 +56,12 @@ enum UpdateSimulator { simulateAutoUpdate(viewModel) } } - + private func simulateHappyPath(_ viewModel: UpdateViewModel) { viewModel.state = .checking(.init(cancel: { viewModel.state = .idle })) - + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { viewModel.state = .updateAvailable(.init( appcastItem: SUAppcastItem.empty(), @@ -75,28 +75,28 @@ enum UpdateSimulator { )) } } - + private func simulateNotFound(_ viewModel: UpdateViewModel) { viewModel.state = .checking(.init(cancel: { viewModel.state = .idle })) - + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { viewModel.state = .notFound(.init(acknowledgement: { // Acknowledgement called when dismissed })) - + DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) { viewModel.state = .idle } } } - + private func simulateError(_ viewModel: UpdateViewModel) { viewModel.state = .checking(.init(cancel: { viewModel.state = .idle })) - + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { viewModel.state = .error(.init( error: NSError(domain: "UpdateError", code: 1, userInfo: [ @@ -111,12 +111,12 @@ enum UpdateSimulator { )) } } - + private func simulateSlowDownload(_ viewModel: UpdateViewModel) { viewModel.state = .checking(.init(cancel: { viewModel.state = .idle })) - + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { viewModel.state = .updateAvailable(.init( appcastItem: SUAppcastItem.empty(), @@ -130,7 +130,7 @@ enum UpdateSimulator { )) } } - + private func simulateSlowDownloadProgress(_ viewModel: UpdateViewModel) { let download = UpdateState.Downloading( cancel: { @@ -140,7 +140,7 @@ enum UpdateSimulator { progress: 0 ) viewModel.state = .downloading(download) - + for i in 1...20 { DispatchQueue.main.asyncAfter(deadline: .now() + Double(i) * 0.5) { let updatedDownload = UpdateState.Downloading( @@ -149,7 +149,7 @@ enum UpdateSimulator { progress: UInt64(i * 100) ) viewModel.state = .downloading(updatedDownload) - + if i == 20 { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { simulateExtract(viewModel) @@ -158,7 +158,7 @@ enum UpdateSimulator { } } } - + private func simulatePermissionRequest(_ viewModel: UpdateViewModel) { let request = SPUUpdatePermissionRequest(systemProfile: []) viewModel.state = .permissionRequest(.init( @@ -172,12 +172,12 @@ enum UpdateSimulator { } )) } - + private func simulateCancelDuringDownload(_ viewModel: UpdateViewModel) { viewModel.state = .checking(.init(cancel: { viewModel.state = .idle })) - + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { viewModel.state = .updateAvailable(.init( appcastItem: SUAppcastItem.empty(), @@ -191,7 +191,7 @@ enum UpdateSimulator { )) } } - + private func simulateDownloadThenCancel(_ viewModel: UpdateViewModel) { let download = UpdateState.Downloading( cancel: { @@ -201,7 +201,7 @@ enum UpdateSimulator { progress: 0 ) viewModel.state = .downloading(download) - + for i in 1...5 { DispatchQueue.main.asyncAfter(deadline: .now() + Double(i) * 0.3) { let updatedDownload = UpdateState.Downloading( @@ -210,7 +210,7 @@ enum UpdateSimulator { progress: UInt64(i * 100) ) viewModel.state = .downloading(updatedDownload) - + if i == 5 { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { viewModel.state = .idle @@ -219,17 +219,17 @@ enum UpdateSimulator { } } } - + private func simulateCancelDuringChecking(_ viewModel: UpdateViewModel) { viewModel.state = .checking(.init(cancel: { viewModel.state = .idle })) - + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { viewModel.state = .idle } } - + private func simulateDownload(_ viewModel: UpdateViewModel) { let download = UpdateState.Downloading( cancel: { @@ -239,7 +239,7 @@ enum UpdateSimulator { progress: 0 ) viewModel.state = .downloading(download) - + for i in 1...10 { DispatchQueue.main.asyncAfter(deadline: .now() + Double(i) * 0.3) { let updatedDownload = UpdateState.Downloading( @@ -248,7 +248,7 @@ enum UpdateSimulator { progress: UInt64(i * 100) ) viewModel.state = .downloading(updatedDownload) - + if i == 10 { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { simulateExtract(viewModel) @@ -257,14 +257,14 @@ enum UpdateSimulator { } } } - + private func simulateExtract(_ viewModel: UpdateViewModel) { viewModel.state = .extracting(.init(progress: 0.0)) - + for j in 1...5 { DispatchQueue.main.asyncAfter(deadline: .now() + Double(j) * 0.3) { viewModel.state = .extracting(.init(progress: Double(j) / 5.0)) - + if j == 5 { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { simulateInstalling(viewModel) @@ -273,7 +273,7 @@ enum UpdateSimulator { } } } - + private func simulateInstalling(_ viewModel: UpdateViewModel) { viewModel.state = .installing(.init( retryTerminatingApplication: { @@ -285,7 +285,7 @@ enum UpdateSimulator { } )) } - + private func simulateAutoUpdate(_ viewModel: UpdateViewModel) { viewModel.state = .installing(.init( isAutoUpdate: true, diff --git a/macos/Sources/Features/Update/UpdateViewModel.swift b/macos/Sources/Features/Update/UpdateViewModel.swift index 1f9304616..8e66f4a16 100644 --- a/macos/Sources/Features/Update/UpdateViewModel.swift +++ b/macos/Sources/Features/Update/UpdateViewModel.swift @@ -4,7 +4,7 @@ import Sparkle class UpdateViewModel: ObservableObject { @Published var state: UpdateState = .idle - + /// The text to display for the current update state. /// Returns an empty string for idle state, progress percentages for downloading/extracting, /// or descriptive text for other states. @@ -38,7 +38,7 @@ class UpdateViewModel: ObservableObject { return err.error.localizedDescription } } - + /// The maximum width text for states that show progress. /// Used to prevent the pill from resizing as percentages change. var maxWidthText: String { @@ -51,7 +51,7 @@ class UpdateViewModel: ObservableObject { return text } } - + /// The SF Symbol icon name for the current update state. var iconName: String? { switch state { @@ -75,7 +75,7 @@ class UpdateViewModel: ObservableObject { return "exclamationmark.triangle.fill" } } - + /// A longer description for the current update state. /// Used in contexts like the command palette where more detail is helpful. var description: String { @@ -100,7 +100,7 @@ class UpdateViewModel: ObservableObject { return "An error occurred during the update process" } } - + /// A badge to display for the current update state. /// Returns version numbers, progress percentages, or nil. var badge: String? { @@ -120,7 +120,7 @@ class UpdateViewModel: ObservableObject { return nil } } - + /// The color to apply to the icon for the current update state. var iconColor: Color { switch state { @@ -140,7 +140,7 @@ class UpdateViewModel: ObservableObject { return .orange } } - + /// The background color for the update pill. var backgroundColor: Color { switch state { @@ -156,7 +156,7 @@ class UpdateViewModel: ObservableObject { return Color(nsColor: .controlBackgroundColor) } } - + /// The foreground (text) color for the update pill. var foregroundColor: Color { switch state { @@ -184,27 +184,27 @@ enum UpdateState: Equatable { case downloading(Downloading) case extracting(Extracting) case installing(Installing) - + var isIdle: Bool { if case .idle = self { return true } return false } - + /// This is true if we're in a state that can be force installed. var isInstallable: Bool { - switch (self) { + switch self { case .checking, .updateAvailable, .downloading, .extracting, .installing: return true - + default: return false } } - + func cancel() { switch self { case .checking(let checking): @@ -221,7 +221,7 @@ enum UpdateState: Equatable { break } } - + /// Confirms or accepts the current update state. /// - For available updates: begins installation /// - For ready-to-install: proceeds with installation @@ -233,7 +233,7 @@ enum UpdateState: Equatable { break } } - + static func == (lhs: UpdateState, rhs: UpdateState) -> Bool { switch (lhs, rhs) { case (.idle, .idle): @@ -258,38 +258,38 @@ enum UpdateState: Equatable { return false } } - + struct NotFound { let acknowledgement: () -> Void } - + struct PermissionRequest { let request: SPUUpdatePermissionRequest let reply: @Sendable (SUUpdatePermissionResponse) -> Void } - + struct Checking { let cancel: () -> Void } - + struct UpdateAvailable { let appcastItem: SUAppcastItem let reply: @Sendable (SPUUserUpdateChoice) -> Void - + var releaseNotes: ReleaseNotes? { let currentCommit = Bundle.main.infoDictionary?["GhosttyCommit"] as? String return ReleaseNotes(displayVersionString: appcastItem.displayVersionString, currentCommit: currentCommit) } } - + enum ReleaseNotes { case commit(URL) case compareTip(URL) case tagged(URL) - + init?(displayVersionString: String, currentCommit: String?) { let version = displayVersionString - + // Check for semantic version (x.y.z) if let semver = Self.extractSemanticVersion(from: version) { let slug = semver.replacingOccurrences(of: ".", with: "-") @@ -298,12 +298,12 @@ enum UpdateState: Equatable { return } } - + // Fall back to git hash detection guard let newHash = Self.extractGitHash(from: version) else { return nil } - + if let currentHash = currentCommit, !currentHash.isEmpty, let url = URL(string: "https://github.com/ghostty-org/ghostty/compare/\(currentHash)...\(newHash)") { self = .compareTip(url) @@ -313,7 +313,7 @@ enum UpdateState: Equatable { return nil } } - + private static func extractSemanticVersion(from version: String) -> String? { let pattern = #"^\d+\.\d+\.\d+$"# if version.range(of: pattern, options: .regularExpression) != nil { @@ -321,7 +321,7 @@ enum UpdateState: Equatable { } return nil } - + private static func extractGitHash(from version: String) -> String? { let pattern = #"[0-9a-f]{7,40}"# if let range = version.range(of: pattern, options: .regularExpression) { @@ -329,7 +329,7 @@ enum UpdateState: Equatable { } return nil } - + var url: URL { switch self { case .commit(let url): return url @@ -337,32 +337,32 @@ enum UpdateState: Equatable { case .tagged(let url): return url } } - + var label: String { - switch (self) { + switch self { case .commit: return "View GitHub Commit" case .compareTip: return "Changes Since This Tip Release" case .tagged: return "View Release Notes" } } } - + struct Error { let error: any Swift.Error let retry: () -> Void let dismiss: () -> Void } - + struct Downloading { let cancel: () -> Void let expectedLength: UInt64? let progress: UInt64 } - + struct Extracting { let progress: Double } - + struct Installing { /// True if this state is triggered by ``Ghostty/UpdateDriver/updater(_:willInstallUpdateOnQuit:immediateInstallationBlock:)`` var isAutoUpdate = false diff --git a/macos/Sources/Ghostty/Ghostty.Action.swift b/macos/Sources/Ghostty/Ghostty.Action.swift index 91f1491dd..f3842fc56 100644 --- a/macos/Sources/Ghostty/Ghostty.Action.swift +++ b/macos/Sources/Ghostty/Ghostty.Action.swift @@ -18,7 +18,7 @@ extension Ghostty.Action { } init(c: ghostty_action_color_change_s) { - switch (c.kind) { + switch c.kind { case GHOSTTY_ACTION_COLOR_KIND_FOREGROUND: self.kind = .foreground case GHOSTTY_ACTION_COLOR_KIND_BACKGROUND: @@ -40,13 +40,13 @@ extension Ghostty.Action { self.amount = c.amount } } - + struct OpenURL { enum Kind { case unknown case text case html - + init(_ c: ghostty_action_open_url_kind_e) { switch c { case GHOSTTY_ACTION_OPEN_URL_KIND_TEXT: @@ -58,13 +58,13 @@ extension Ghostty.Action { } } } - + let kind: Kind let url: String - + init(c: ghostty_action_open_url_s) { self.kind = Kind(c.kind) - + if let urlCString = c.url { let data = Data(bytes: urlCString, count: Int(c.len)) self.url = String(data: data, encoding: .utf8) ?? "" @@ -81,7 +81,7 @@ extension Ghostty.Action { case error case indeterminate case pause - + init(_ c: ghostty_action_progress_report_state_e) { switch c { case GHOSTTY_PROGRESS_STATE_REMOVE: @@ -99,26 +99,26 @@ extension Ghostty.Action { } } } - + let state: State let progress: UInt8? } - + struct Scrollbar { let total: UInt64 let offset: UInt64 let len: UInt64 - + init(c: ghostty_action_scrollbar_s) { total = c.total - offset = c.offset + offset = c.offset len = c.len } } struct StartSearch { let needle: String? - + init(c: ghostty_action_start_search_s) { if let needleCString = c.needle { self.needle = String(cString: needleCString) diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index e3441257f..42d4368e7 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -33,7 +33,7 @@ extension Ghostty { private var configPath: String? /// The ghostty app instance. We only have one of these for the entire app, although I guess /// in theory you can have multiple... I don't know why you would... - @Published var app: ghostty_app_t? = nil { + @Published var app: ghostty_app_t? { didSet { guard let old = oldValue else { return } ghostty_app_free(old) @@ -140,7 +140,7 @@ extension Ghostty { guard let app = self.app else { return } // Soft updates just call with our existing config - if (soft) { + if soft { ghostty_app_update_config(app, config.config!) return } @@ -158,7 +158,7 @@ extension Ghostty { func reloadConfig(surface: ghostty_surface_t, soft: Bool = false) { // Soft updates just call with our existing config - if (soft) { + if soft { ghostty_surface_update_config(surface, config.config!) return } @@ -183,14 +183,14 @@ extension Ghostty { func newTab(surface: ghostty_surface_t) { let action = "new_tab" - if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) { logger.warning("action failed action=\(action)") } } func newWindow(surface: ghostty_surface_t) { let action = "new_window" - if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) { logger.warning("action failed action=\(action)") } } @@ -213,14 +213,14 @@ extension Ghostty { func splitToggleZoom(surface: ghostty_surface_t) { let action = "toggle_split_zoom" - if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) { logger.warning("action failed action=\(action)") } } func toggleFullscreen(surface: ghostty_surface_t) { let action = "toggle_fullscreen" - if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) { logger.warning("action failed action=\(action)") } } @@ -241,21 +241,21 @@ extension Ghostty { case .reset: action = "reset_font_size" } - if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) { logger.warning("action failed action=\(action)") } } func toggleTerminalInspector(surface: ghostty_surface_t) { let action = "inspector:toggle" - if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) { logger.warning("action failed action=\(action)") } } func resetTerminal(surface: ghostty_surface_t) { let action = "reset" - if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) { logger.warning("action failed action=\(action)") } } @@ -312,7 +312,6 @@ extension Ghostty { ghostty_app_set_focus(app, false) } - // MARK: Ghostty Callbacks (macOS) static func closeSurface(_ userdata: UnsafeMutableRawPointer?, processAlive: Bool) { @@ -379,25 +378,25 @@ extension Ghostty { let surface = self.surfaceUserdata(from: userdata) guard let pasteboard = NSPasteboard.ghostty(location) else { return } guard let content = content, len > 0 else { return } - + // Convert the C array to Swift array let contentArray = (0.. Bool { // Make sure it a target we understand so all our action handlers can assert - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP, GHOSTTY_TARGET_SURFACE: break @@ -473,7 +472,7 @@ extension Ghostty { } // Action dispatch - switch (action.tag) { + switch action.tag { case GHOSTTY_ACTION_QUIT: quit(app) @@ -605,7 +604,7 @@ extension Ghostty { case GHOSTTY_ACTION_CHECK_FOR_UPDATES: checkForUpdates(app) - + case GHOSTTY_ACTION_OPEN_URL: return openURL(action.action.open_url) @@ -681,12 +680,12 @@ extension Ghostty { appDelegate.checkForUpdates(nil) } } - + private static func openURL( _ v: ghostty_action_open_url_s ) -> Bool { let action = Ghostty.Action.OpenURL(c: v) - + // If the URL doesn't have a valid scheme we assume its a file path. The URL // initializer will gladly take invalid URLs (e.g. plain file paths) and turn // them into schema-less URLs, but these won't open properly in text editors. @@ -697,7 +696,7 @@ extension Ghostty { } else { url = URL(filePath: action.url) } - + switch action.kind { case .text: // Open with the default editor for `*.ghostty` file or just system text editor @@ -706,15 +705,15 @@ extension Ghostty { NSWorkspace.shared.open([url], withApplicationAt: textEditor, configuration: NSWorkspace.OpenConfiguration()) return true } - + case .html: // The extension will be HTML and we do the right thing automatically. break - + case .unknown: break } - + // Open with the default application for the URL NSWorkspace.shared.open(url) return true @@ -722,7 +721,7 @@ extension Ghostty { private static func undo(_ app: ghostty_app_t, target: ghostty_target_s) -> Bool { let undoManager: UndoManager? - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: undoManager = (NSApp.delegate as? AppDelegate)?.undoManager @@ -743,7 +742,7 @@ extension Ghostty { private static func redo(_ app: ghostty_app_t, target: ghostty_target_s) -> Bool { let undoManager: UndoManager? - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: undoManager = (NSApp.delegate as? AppDelegate)?.undoManager @@ -763,7 +762,7 @@ extension Ghostty { } private static func newWindow(_ app: ghostty_app_t, target: ghostty_target_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: NotificationCenter.default.post( name: Notification.ghosttyNewWindow, @@ -782,14 +781,13 @@ extension Ghostty { ] ) - default: assertionFailure() } } private static func newTab(_ app: ghostty_app_t, target: ghostty_target_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: NotificationCenter.default.post( name: Notification.ghosttyNewTab, @@ -819,7 +817,6 @@ extension Ghostty { ] ) - default: assertionFailure() } @@ -829,7 +826,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, direction: ghostty_action_split_direction_e) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: // New split does nothing with an app target Ghostty.logger.warning("new split does nothing with an app target") @@ -848,7 +845,6 @@ extension Ghostty { ] ) - default: assertionFailure() } @@ -858,7 +854,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s ) -> Bool { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: return false @@ -879,7 +875,7 @@ extension Ghostty { } private static func closeTab(_ app: ghostty_app_t, target: ghostty_target_s, mode: ghostty_action_close_tab_mode_e) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("close tabs does nothing with an app target") return @@ -888,7 +884,7 @@ extension Ghostty { guard let surface = target.target.surface else { return } guard let surfaceView = self.surfaceView(from: surface) else { return } - switch (mode) { + switch mode { case GHOSTTY_ACTION_CLOSE_TAB_MODE_THIS: NotificationCenter.default.post( name: .ghosttyCloseTab, @@ -914,14 +910,13 @@ extension Ghostty { assertionFailure() } - default: assertionFailure() } } private static func closeWindow(_ app: ghostty_app_t, target: ghostty_target_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("close window does nothing with an app target") return @@ -949,7 +944,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, mode raw: ghostty_action_fullscreen_e) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("toggle fullscreen does nothing with an app target") return @@ -969,7 +964,6 @@ extension Ghostty { ] ) - default: assertionFailure() } @@ -978,7 +972,7 @@ extension Ghostty { private static func toggleCommandPalette( _ app: ghostty_app_t, target: ghostty_target_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("toggle command palette does nothing with an app target") return @@ -991,7 +985,6 @@ extension Ghostty { object: surfaceView ) - default: assertionFailure() } @@ -1001,7 +994,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s ) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("toggle maximize does nothing with an app target") return @@ -1014,7 +1007,6 @@ extension Ghostty { object: surfaceView ) - default: assertionFailure() } @@ -1031,7 +1023,7 @@ extension Ghostty { private static func ringBell( _ app: ghostty_app_t, target: ghostty_target_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: // Technically we could still request app attention here but there // are no known cases where the bell is rang with an app target so @@ -1056,7 +1048,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, v: ghostty_action_readonly_e) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("set readonly does nothing with an app target") return @@ -1081,7 +1073,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, move: ghostty_action_move_tab_s) -> Bool { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("move tab does nothing with an app target") return false @@ -1112,7 +1104,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, tab: ghostty_action_goto_tab_e) -> Bool { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("goto tab does nothing with an app target") return false @@ -1144,7 +1136,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, direction: ghostty_action_goto_split_e) -> Bool { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("goto split does nothing with an app target") return false @@ -1250,7 +1242,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, resize: ghostty_action_resize_split_s) -> Bool { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("resize split does nothing with an app target") return false @@ -1283,7 +1275,7 @@ extension Ghostty { private static func equalizeSplits( _ app: ghostty_app_t, target: ghostty_target_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("equalize splits does nothing with an app target") return @@ -1296,7 +1288,6 @@ extension Ghostty { object: surfaceView ) - default: assertionFailure() } @@ -1305,7 +1296,7 @@ extension Ghostty { private static func toggleSplitZoom( _ app: ghostty_app_t, target: ghostty_target_s) -> Bool { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("toggle split zoom does nothing with an app target") return false @@ -1324,7 +1315,6 @@ extension Ghostty { ) return true - default: assertionFailure() return false @@ -1335,7 +1325,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, mode: ghostty_action_inspector_e) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("toggle inspector does nothing with an app target") return @@ -1349,7 +1339,6 @@ extension Ghostty { userInfo: ["mode": mode] ) - default: assertionFailure() } @@ -1359,7 +1348,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, n: ghostty_action_desktop_notification_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("toggle split zoom does nothing with an app target") return @@ -1377,12 +1366,11 @@ extension Ghostty { } } - center.getNotificationSettings() { settings in + center.getNotificationSettings { settings in guard settings.authorizationStatus == .authorized else { return } surfaceView.showUserNotification(title: title, body: body) } - default: assertionFailure() } @@ -1395,7 +1383,7 @@ extension Ghostty { ) { guard let mode = SetFloatWIndow.from(mode_raw) else { return } - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("toggle float window does nothing with an app target") return @@ -1405,7 +1393,7 @@ extension Ghostty { guard let surfaceView = self.surfaceView(from: surface) else { return } guard let window = surfaceView.window as? TerminalWindow else { return } - switch (mode) { + switch mode { case .on: window.level = .floating @@ -1429,7 +1417,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s ) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("toggle background opacity does nothing with an app target") return @@ -1453,7 +1441,7 @@ extension Ghostty { ) { guard let mode = SetSecureInput.from(mode_raw) else { return } - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: guard let appDelegate = NSApplication.shared.delegate as? AppDelegate else { return } appDelegate.setSecureInput(mode) @@ -1464,7 +1452,7 @@ extension Ghostty { guard let appState = self.appState(fromView: surfaceView) else { return } guard appState.config.autoSecureInput else { return } - switch (mode) { + switch mode { case .on: surfaceView.passwordInput = true @@ -1492,7 +1480,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, v: ghostty_action_set_title_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("set title does nothing with an app target") return @@ -1511,7 +1499,7 @@ extension Ghostty { private static func copyTitleToClipboard( _ app: ghostty_app_t, target: ghostty_target_s) -> Bool { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_SURFACE: guard let surface = target.target.surface else { return false } guard let surfaceView = self.surfaceView(from: surface) else { return false } @@ -1534,7 +1522,7 @@ extension Ghostty { let promptTitle = Action.PromptTitle(v) switch promptTitle { case .surface: - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("set title prompt does nothing with an app target") return false @@ -1551,7 +1539,7 @@ extension Ghostty { } case .tab: - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: guard let window = NSApp.mainWindow ?? NSApp.keyWindow, let controller = window.windowController as? BaseTerminalController @@ -1579,7 +1567,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, v: ghostty_action_pwd_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("pwd change does nothing with an app target") return @@ -1599,7 +1587,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, shape: ghostty_action_mouse_shape_e) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("set mouse shapes nothing with an app target") return @@ -1609,7 +1597,6 @@ extension Ghostty { guard let surfaceView = self.surfaceView(from: surface) else { return } surfaceView.setCursorShape(shape) - default: assertionFailure() } @@ -1619,7 +1606,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, v: ghostty_action_mouse_visibility_e) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("set mouse shapes nothing with an app target") return @@ -1627,7 +1614,7 @@ extension Ghostty { case GHOSTTY_TARGET_SURFACE: guard let surface = target.target.surface else { return } guard let surfaceView = self.surfaceView(from: surface) else { return } - switch (v) { + switch v { case GHOSTTY_MOUSE_VISIBLE: surfaceView.setCursorVisibility(true) @@ -1638,7 +1625,6 @@ extension Ghostty { return } - default: assertionFailure() } @@ -1648,7 +1634,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, v: ghostty_action_mouse_over_link_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("mouse over link does nothing with an app target") return @@ -1664,7 +1650,6 @@ extension Ghostty { let buffer = Data(bytes: v.url!, count: v.len) surfaceView.hoverUrl = String(data: buffer, encoding: .utf8) - default: assertionFailure() } @@ -1674,7 +1659,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, v: ghostty_action_initial_size_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("initial size does nothing with an app target") return @@ -1682,8 +1667,7 @@ extension Ghostty { case GHOSTTY_TARGET_SURFACE: guard let surface = target.target.surface else { return } guard let surfaceView = self.surfaceView(from: surface) else { return } - surfaceView.initialSize = NSMakeSize(Double(v.width), Double(v.height)) - + surfaceView.initialSize = NSSize(width: Double(v.width), height: Double(v.height)) default: assertionFailure() @@ -1693,7 +1677,7 @@ extension Ghostty { private static func resetWindowSize( _ app: ghostty_app_t, target: ghostty_target_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("reset window size does nothing with an app target") return @@ -1706,7 +1690,6 @@ extension Ghostty { object: surfaceView ) - default: assertionFailure() } @@ -1716,7 +1699,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, v: ghostty_action_cell_size_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("mouse over link does nothing with an app target") return @@ -1738,7 +1721,7 @@ extension Ghostty { private static func renderInspector( _ app: ghostty_app_t, target: ghostty_target_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("mouse over link does nothing with an app target") return @@ -1760,7 +1743,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, v: ghostty_action_renderer_health_e) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("mouse over link does nothing with an app target") return @@ -1785,7 +1768,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, v: ghostty_action_key_sequence_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("key sequence does nothing with an app target") return @@ -1817,7 +1800,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, v: ghostty_action_key_table_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("key table does nothing with an app target") return @@ -1842,7 +1825,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, v: ghostty_action_progress_report_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("progress report does nothing with an app target") return @@ -1850,7 +1833,7 @@ extension Ghostty { case GHOSTTY_TARGET_SURFACE: guard let surface = target.target.surface else { return } guard let surfaceView = self.surfaceView(from: surface) else { return } - + let progressReport = Ghostty.Action.ProgressReport(c: v) DispatchQueue.main.async { if progressReport.state == .remove { @@ -1869,7 +1852,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, v: ghostty_action_scrollbar_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("scrollbar does nothing with an app target") return @@ -1877,7 +1860,7 @@ extension Ghostty { case GHOSTTY_TARGET_SURFACE: guard let surface = target.target.surface else { return } guard let surfaceView = self.surfaceView(from: surface) else { return } - + let scrollbar = Ghostty.Action.Scrollbar(c: v) NotificationCenter.default.post( name: .ghosttyDidUpdateScrollbar, @@ -1896,7 +1879,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, v: ghostty_action_start_search_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("start_search does nothing with an app target") return @@ -1914,7 +1897,7 @@ extension Ghostty { } else { surfaceView.searchState = Ghostty.SurfaceView.SearchState(from: startSearch) } - + NotificationCenter.default.post(name: .ghosttySearchFocus, object: surfaceView) } @@ -1926,7 +1909,7 @@ extension Ghostty { private static func endSearch( _ app: ghostty_app_t, target: ghostty_target_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("end_search does nothing with an app target") return @@ -1948,7 +1931,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, v: ghostty_action_search_total_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("search_total does nothing with an app target") return @@ -1971,7 +1954,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, v: ghostty_action_search_selected_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("search_selected does nothing with an app target") return @@ -1993,14 +1976,13 @@ extension Ghostty { private static func configReload( _ app: ghostty_app_t, target: ghostty_target_s, - v: ghostty_action_reload_config_s) - { + v: ghostty_action_reload_config_s) { logger.info("config reload notification") guard let app_ud = ghostty_app_userdata(app) else { return } let ghostty = Unmanaged.fromOpaque(app_ud).takeUnretainedValue() - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: ghostty.reloadConfig(soft: v.soft) return @@ -2026,7 +2008,7 @@ extension Ghostty { // something so apprt's do not have to do this. let config = Config(clone: v.config) - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: // Notify the world that the app config changed NotificationCenter.default.post( @@ -2066,7 +2048,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, change: ghostty_action_color_change_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("color change does nothing with an app target") return @@ -2087,7 +2069,6 @@ extension Ghostty { } } - // MARK: User Notifications /// Handle a received user notification. This is called when a user notification is clicked or dismissed by the user @@ -2097,7 +2078,7 @@ extension Ghostty { let uuid = UUID(uuidString: uuidString), let surface = delegate?.findSurface(forUUID: uuid) else { return } - switch (response.actionIdentifier) { + switch response.actionIdentifier { case UNNotificationDefaultActionIdentifier, Ghostty.userNotificationActionShow: // The user clicked on a notification surface.handleUserNotification(notification: response.notification, focus: true) diff --git a/macos/Sources/Ghostty/Ghostty.Config.swift b/macos/Sources/Ghostty/Ghostty.Config.swift index 55e289a3c..d65bac27f 100644 --- a/macos/Sources/Ghostty/Ghostty.Config.swift +++ b/macos/Sources/Ghostty/Ghostty.Config.swift @@ -7,7 +7,7 @@ extension Ghostty { // The underlying C pointer to the Ghostty config structure. This // should never be accessed directly. Any operations on this should // be called from the functions on this or another class. - private(set) var config: ghostty_config_t? = nil { + private(set) var config: ghostty_config_t? { didSet { // Free the old value whenever we change guard let old = oldValue else { return } @@ -22,7 +22,7 @@ extension Ghostty { var errors: [String] { guard let cfg = self.config else { return [] } - var diags: [String] = []; + var diags: [String] = [] let diagsCount = ghostty_config_diagnostics_count(cfg) for i in 0.. 0 { logger.warning("config error: \(diagsCount) configuration errors on reload") - var diags: [String] = []; + var diags: [String] = [] for i in 0..? = nil + var v: UnsafePointer? let key = "title" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return nil } guard let ptr = v else { return nil } @@ -169,7 +169,7 @@ extension Ghostty { var windowSaveState: String { guard let config = self.config else { return "" } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "window-save-state" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return "" } guard let ptr = v else { return "" } @@ -192,7 +192,7 @@ extension Ghostty { var windowNewTabPosition: String { guard let config = self.config else { return "" } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "window-new-tab-position" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return "" } guard let ptr = v else { return "" } @@ -202,7 +202,7 @@ extension Ghostty { var windowDecorations: Bool { let defaultValue = true guard let config = self.config else { return defaultValue } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "window-decoration" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue } guard let ptr = v else { return defaultValue } @@ -212,7 +212,7 @@ extension Ghostty { var windowTheme: String? { guard let config = self.config else { return nil } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "window-theme" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return nil } guard let ptr = v else { return nil } @@ -233,7 +233,7 @@ extension Ghostty { #if canImport(AppKit) var windowFullscreen: FullscreenMode? { guard let config = self.config else { return nil } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "fullscreen" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return nil } guard let ptr = v else { return nil } @@ -256,7 +256,7 @@ extension Ghostty { #else var windowFullscreen: Bool { guard let config = self.config else { return false } - var v: UnsafePointer? = nil + var v: UnsafePointer? 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 } @@ -271,7 +271,7 @@ extension Ghostty { var windowFullscreenMode: FullscreenMode { let defaultValue: FullscreenMode = .native guard let config = self.config else { return defaultValue } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "macos-non-native-fullscreen" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue } guard let ptr = v else { return defaultValue } @@ -293,7 +293,7 @@ extension Ghostty { var windowTitleFontFamily: String? { guard let config = self.config else { return nil } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "window-title-font-family" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return nil } guard let ptr = v else { return nil } @@ -303,7 +303,7 @@ extension Ghostty { var macosWindowButtons: MacOSWindowButtons { let defaultValue = MacOSWindowButtons.visible guard let config = self.config else { return defaultValue } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "macos-window-buttons" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue } guard let ptr = v else { return defaultValue } @@ -314,7 +314,7 @@ extension Ghostty { var macosTitlebarStyle: String { let defaultValue = "transparent" guard let config = self.config else { return defaultValue } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "macos-titlebar-style" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue } guard let ptr = v else { return defaultValue } @@ -324,7 +324,7 @@ extension Ghostty { var macosTitlebarProxyIcon: MacOSTitlebarProxyIcon { let defaultValue = MacOSTitlebarProxyIcon.visible guard let config = self.config else { return defaultValue } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "macos-titlebar-proxy-icon" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue } guard let ptr = v else { return defaultValue } @@ -335,7 +335,7 @@ extension Ghostty { var macosDockDropBehavior: MacDockDropBehavior { let defaultValue = MacDockDropBehavior.new_tab guard let config = self.config else { return defaultValue } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "macos-dock-drop-behavior" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue } guard let ptr = v else { return defaultValue } @@ -345,7 +345,7 @@ extension Ghostty { var macosWindowShadow: Bool { guard let config = self.config else { return false } - var v = false; + var v = false let key = "macos-window-shadow" _ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) return v @@ -354,7 +354,7 @@ extension Ghostty { var macosIcon: MacOSIcon { let defaultValue = MacOSIcon.official guard let config = self.config else { return defaultValue } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "macos-icon" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue } guard let ptr = v else { return defaultValue } @@ -366,7 +366,7 @@ extension Ghostty { #if os(macOS) let defaultValue = NSString("~/.config/ghostty/Ghostty.icns").expandingTildeInPath guard let config = self.config else { return defaultValue } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "macos-custom-icon" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue } guard let ptr = v else { return defaultValue } @@ -380,7 +380,7 @@ extension Ghostty { var macosIconFrame: MacOSIconFrame { let defaultValue = MacOSIconFrame.aluminum guard let config = self.config else { return defaultValue } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "macos-icon-frame" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue } guard let ptr = v else { return defaultValue } @@ -408,7 +408,7 @@ extension Ghostty { var macosHidden: MacHidden { guard let config = self.config else { return .never } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "macos-hidden" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return .never } guard let ptr = v else { return .never } @@ -416,18 +416,18 @@ extension Ghostty { return MacHidden(rawValue: str) ?? .never } - var focusFollowsMouse : Bool { + var focusFollowsMouse: Bool { guard let config = self.config else { return false } - var v = false; + var v = false let key = "focus-follows-mouse" _ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) return v } var backgroundColor: Color { - var color: ghostty_config_color_s = .init(); + 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 !ghostty_config_get(config, &color, bg_key, UInt(bg_key.lengthOfBytes(using: .utf8))) { #if os(macOS) return Color(NSColor.windowBackgroundColor) #elseif os(iOS) @@ -449,7 +449,7 @@ extension Ghostty { var v: Double = 1 let key = "background-opacity" _ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) - return v; + return v } var backgroundBlur: BackgroundBlur { @@ -471,11 +471,11 @@ extension Ghostty { var unfocusedSplitFill: Color { guard let config = self.config else { return .white } - var color: ghostty_config_color_s = .init(); + var color: ghostty_config_color_s = .init() let key = "unfocused-split-fill" - if (!ghostty_config_get(config, &color, key, UInt(key.lengthOfBytes(using: .utf8)))) { + if !ghostty_config_get(config, &color, key, UInt(key.lengthOfBytes(using: .utf8))) { let bg_key = "background" - _ = ghostty_config_get(config, &color, bg_key, UInt(bg_key.lengthOfBytes(using: .utf8))); + _ = ghostty_config_get(config, &color, bg_key, UInt(bg_key.lengthOfBytes(using: .utf8))) } return .init( @@ -492,9 +492,9 @@ extension Ghostty { guard let config = self.config else { return Color(newColor) } - var color: ghostty_config_color_s = .init(); + var color: ghostty_config_color_s = .init() let key = "split-divider-color" - if (!ghostty_config_get(config, &color, key, UInt(key.lengthOfBytes(using: .utf8)))) { + if !ghostty_config_get(config, &color, key, UInt(key.lengthOfBytes(using: .utf8))) { return Color(newColor) } @@ -508,7 +508,7 @@ extension Ghostty { #if canImport(AppKit) var quickTerminalPosition: QuickTerminalPosition { guard let config = self.config else { return .top } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "quick-terminal-position" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return .top } guard let ptr = v else { return .top } @@ -518,7 +518,7 @@ extension Ghostty { var quickTerminalScreen: QuickTerminalScreen { guard let config = self.config else { return .main } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "quick-terminal-screen" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return .main } guard let ptr = v else { return .main } @@ -544,7 +544,7 @@ extension Ghostty { var quickTerminalSpaceBehavior: QuickTerminalSpaceBehavior { guard let config = self.config else { return .move } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "quick-terminal-space-behavior" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return .move } guard let ptr = v else { return .move } @@ -563,7 +563,7 @@ extension Ghostty { var resizeOverlay: ResizeOverlay { guard let config = self.config else { return .after_first } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "resize-overlay" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return .after_first } guard let ptr = v else { return .after_first } @@ -574,7 +574,7 @@ extension Ghostty { var resizeOverlayPosition: ResizeOverlayPosition { let defaultValue = ResizeOverlayPosition.center guard let config = self.config else { return defaultValue } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "resize-overlay-position" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue } guard let ptr = v else { return defaultValue } @@ -587,7 +587,7 @@ extension Ghostty { var v: UInt = 0 let key = "resize-overlay-duration" _ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) - return v; + return v } var undoTimeout: Duration { @@ -600,7 +600,7 @@ extension Ghostty { var autoUpdate: AutoUpdate? { guard let config = self.config else { return nil } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "auto-update" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return nil } guard let ptr = v else { return nil } @@ -611,7 +611,7 @@ extension Ghostty { var autoUpdateChannel: AutoUpdateChannel { let defaultValue = AutoUpdateChannel.stable guard let config = self.config else { return defaultValue } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "auto-update-channel" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue } guard let ptr = v else { return defaultValue } @@ -621,7 +621,7 @@ extension Ghostty { var autoSecureInput: Bool { guard let config = self.config else { return true } - var v = false; + var v = false let key = "macos-auto-secure-input" _ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) return v @@ -629,7 +629,7 @@ extension Ghostty { var secureInputIndication: Bool { guard let config = self.config else { return true } - var v = false; + var v = false let key = "macos-secure-input-indication" _ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) return v @@ -637,7 +637,7 @@ extension Ghostty { var maximize: Bool { guard let config = self.config else { return true } - var v = false; + var v = false let key = "maximize" _ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) return v @@ -646,7 +646,7 @@ extension Ghostty { var macosShortcuts: MacShortcuts { let defaultValue = MacShortcuts.ask guard let config = self.config else { return defaultValue } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "macos-shortcuts" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue } guard let ptr = v else { return defaultValue } @@ -657,7 +657,7 @@ extension Ghostty { var scrollbar: Scrollbar { let defaultValue = Scrollbar.system guard let config = self.config else { return defaultValue } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "scrollbar" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue } guard let ptr = v else { return defaultValue } @@ -680,7 +680,7 @@ extension Ghostty { // MARK: Configuration Enums extension Ghostty.Config { - enum AutoUpdate : String { + enum AutoUpdate: String { case off case check case download @@ -763,13 +763,13 @@ extension Ghostty.Config { static let navigation = SplitPreserveZoom(rawValue: 1 << 0) } - + enum MacDockDropBehavior: String { case new_tab = "new-tab" case new_window = "new-window" } - enum MacHidden : String { + enum MacHidden: String { case never case always } @@ -785,13 +785,13 @@ extension Ghostty.Config { case never } - enum ResizeOverlay : String { + enum ResizeOverlay: String { case always case never case after_first = "after-first" } - enum ResizeOverlayPosition : String { + enum ResizeOverlayPosition: String { case center case top_left = "top-left" case top_center = "top-center" @@ -801,30 +801,30 @@ extension Ghostty.Config { case bottom_right = "bottom-right" func top() -> Bool { - switch (self) { - case .top_left, .top_center, .top_right: return true; - default: return false; + switch self { + case .top_left, .top_center, .top_right: return true + default: return false } } func bottom() -> Bool { - switch (self) { - case .bottom_left, .bottom_center, .bottom_right: return true; - default: return false; + switch self { + case .bottom_left, .bottom_center, .bottom_right: return true + default: return false } } func left() -> Bool { - switch (self) { - case .top_left, .bottom_left: return true; - default: return false; + switch self { + case .top_left, .bottom_left: return true + default: return false } } func right() -> Bool { - switch (self) { - case .top_right, .bottom_right: return true; - default: return false; + switch self { + case .top_right, .bottom_right: return true + default: return false } } } diff --git a/macos/Sources/Ghostty/Ghostty.Input.swift b/macos/Sources/Ghostty/Ghostty.Input.swift index 7b2905abb..27f4d05dd 100644 --- a/macos/Sources/Ghostty/Ghostty.Input.swift +++ b/macos/Sources/Ghostty/Ghostty.Input.swift @@ -18,7 +18,7 @@ extension Ghostty { /// be used for things like NSMenu that only support keyboard shortcuts anyways. static func keyboardShortcut(for trigger: ghostty_input_trigger_s) -> KeyboardShortcut? { let key: KeyEquivalent - switch (trigger.tag) { + switch trigger.tag { case GHOSTTY_TRIGGER_PHYSICAL: // Only functional keys can be converted to a KeyboardShortcut. Other physical // mappings cannot because KeyboardShortcut in Swift is inherently layout-dependent. @@ -49,11 +49,11 @@ extension Ghostty { /// Returns the event modifier flags set for the Ghostty mods enum. static func eventModifierFlags(mods: ghostty_input_mods_e) -> NSEvent.ModifierFlags { - var flags = NSEvent.ModifierFlags(rawValue: 0); - if (mods.rawValue & GHOSTTY_MODS_SHIFT.rawValue != 0) { flags.insert(.shift) } - if (mods.rawValue & GHOSTTY_MODS_CTRL.rawValue != 0) { flags.insert(.control) } - if (mods.rawValue & GHOSTTY_MODS_ALT.rawValue != 0) { flags.insert(.option) } - if (mods.rawValue & GHOSTTY_MODS_SUPER.rawValue != 0) { flags.insert(.command) } + var flags = NSEvent.ModifierFlags(rawValue: 0) + if mods.rawValue & GHOSTTY_MODS_SHIFT.rawValue != 0 { flags.insert(.shift) } + if mods.rawValue & GHOSTTY_MODS_CTRL.rawValue != 0 { flags.insert(.control) } + if mods.rawValue & GHOSTTY_MODS_ALT.rawValue != 0 { flags.insert(.option) } + if mods.rawValue & GHOSTTY_MODS_SUPER.rawValue != 0 { flags.insert(.command) } return flags } @@ -61,19 +61,19 @@ extension Ghostty { 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 } + 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 that's 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 } + 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) } @@ -81,7 +81,7 @@ extension Ghostty { /// A map from the Ghostty key enum to the keyEquivalent string for shortcuts. Note that /// not all ghostty key enum values are represented here because not all of them can be /// mapped to a KeyEquivalent. - static let keyToEquivalent: [ghostty_input_key_e : KeyEquivalent] = [ + static let keyToEquivalent: [ghostty_input_key_e: KeyEquivalent] = [ // Function keys GHOSTTY_KEY_ARROW_UP: .upArrow, GHOSTTY_KEY_ARROW_DOWN: .downArrow, @@ -243,7 +243,7 @@ extension Ghostty.Input { extension Ghostty.Input.Action: AppEnum { static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Key Action") - static var caseDisplayRepresentations: [Ghostty.Input.Action : DisplayRepresentation] = [ + static var caseDisplayRepresentations: [Ghostty.Input.Action: DisplayRepresentation] = [ .release: "Release", .press: "Press", .repeat: "Repeat" @@ -355,7 +355,7 @@ extension Ghostty.Input { extension Ghostty.Input.MouseState: AppEnum { static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Mouse State") - static var caseDisplayRepresentations: [Ghostty.Input.MouseState : DisplayRepresentation] = [ + static var caseDisplayRepresentations: [Ghostty.Input.MouseState: DisplayRepresentation] = [ .release: "Release", .press: "Press" ] @@ -420,7 +420,7 @@ extension Ghostty.Input { extension Ghostty.Input.MouseButton: AppEnum { static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Mouse Button") - static var caseDisplayRepresentations: [Ghostty.Input.MouseButton : DisplayRepresentation] = [ + static var caseDisplayRepresentations: [Ghostty.Input.MouseButton: DisplayRepresentation] = [ .unknown: "Unknown", .left: "Left", .right: "Right", @@ -504,7 +504,7 @@ extension Ghostty.Input { extension Ghostty.Input.Momentum: AppEnum { static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Scroll Momentum") - static var caseDisplayRepresentations: [Ghostty.Input.Momentum : DisplayRepresentation] = [ + static var caseDisplayRepresentations: [Ghostty.Input.Momentum: DisplayRepresentation] = [ .none: "None", .began: "Began", .stationary: "Stationary", @@ -1223,7 +1223,7 @@ extension Ghostty.Input.Key: AppEnum { ] } - static var caseDisplayRepresentations: [Ghostty.Input.Key : DisplayRepresentation] = [ + static var caseDisplayRepresentations: [Ghostty.Input.Key: DisplayRepresentation] = [ // Letters (A-Z) .a: "A", .b: "B", .c: "C", .d: "D", .e: "E", .f: "F", .g: "G", .h: "H", .i: "I", .j: "J", .k: "K", .l: "L", .m: "M", .n: "N", .o: "O", .p: "P", .q: "Q", .r: "R", .s: "S", .t: "T", diff --git a/macos/Sources/Ghostty/Ghostty.Surface.swift b/macos/Sources/Ghostty/Ghostty.Surface.swift index 7cb32ed71..b072db15e 100644 --- a/macos/Sources/Ghostty/Ghostty.Surface.swift +++ b/macos/Sources/Ghostty/Ghostty.Surface.swift @@ -40,7 +40,7 @@ extension Ghostty { @MainActor func sendText(_ text: String) { let len = text.utf8CString.count - if (len == 0) { return } + if len == 0 { return } text.withCString { ptr in // len includes the null terminator so we do len - 1 @@ -149,7 +149,7 @@ extension Ghostty { @MainActor func perform(action: String) -> Bool { let len = action.utf8CString.count - if (len == 0) { return false } + if len == 0 { return false } return action.withCString { cString in ghostty_surface_binding_action(surface, cString, UInt(len - 1)) } diff --git a/macos/Sources/Ghostty/NSEvent+Extension.swift b/macos/Sources/Ghostty/NSEvent+Extension.swift index b67c1932e..55888944e 100644 --- a/macos/Sources/Ghostty/NSEvent+Extension.swift +++ b/macos/Sources/Ghostty/NSEvent+Extension.swift @@ -39,8 +39,7 @@ extension NSEvent { key_ev.unshifted_codepoint = 0 if type == .keyDown || type == .keyUp { if let chars = characters(byApplyingModifiers: []), - let codepoint = chars.unicodeScalars.first - { + let codepoint = chars.unicodeScalars.first { key_ev.unshifted_codepoint = codepoint.value } } diff --git a/macos/Sources/Ghostty/Package.swift b/macos/Sources/Ghostty/Package.swift index 15cb3a51e..1e92eb8a1 100644 --- a/macos/Sources/Ghostty/Package.swift +++ b/macos/Sources/Ghostty/Package.swift @@ -100,7 +100,7 @@ extension Ghostty { case toggle static func from(_ c: ghostty_action_float_window_e) -> Self? { - switch (c) { + switch c { case GHOSTTY_FLOAT_WINDOW_ON: return .on @@ -122,7 +122,7 @@ extension Ghostty { case toggle static func from(_ c: ghostty_action_secure_input_e) -> Self? { - switch (c) { + switch c { case GHOSTTY_SECURE_INPUT_ON: return .on @@ -144,7 +144,7 @@ extension Ghostty { /// Initialize from a Ghostty API enum. static func from(direction: ghostty_action_goto_split_e) -> Self? { - switch (direction) { + switch direction { case GHOSTTY_GOTO_SPLIT_PREVIOUS: return .previous @@ -169,7 +169,7 @@ extension Ghostty { } func toNative() -> ghostty_action_goto_split_e { - switch (self) { + switch self { case .previous: return GHOSTTY_GOTO_SPLIT_PREVIOUS @@ -196,30 +196,30 @@ extension Ghostty { case up, down, left, right static func from(direction: ghostty_action_resize_split_direction_e) -> Self? { - switch (direction) { + switch direction { case GHOSTTY_RESIZE_SPLIT_UP: - return .up; + return .up case GHOSTTY_RESIZE_SPLIT_DOWN: - return .down; + return .down case GHOSTTY_RESIZE_SPLIT_LEFT: - return .left; + return .left case GHOSTTY_RESIZE_SPLIT_RIGHT: - return .right; + return .right default: return nil } } func toNative() -> ghostty_action_resize_split_direction_e { - switch (self) { + switch self { case .up: - return GHOSTTY_RESIZE_SPLIT_UP; + return GHOSTTY_RESIZE_SPLIT_UP case .down: - return GHOSTTY_RESIZE_SPLIT_DOWN; + return GHOSTTY_RESIZE_SPLIT_DOWN case .left: - return GHOSTTY_RESIZE_SPLIT_LEFT; + return GHOSTTY_RESIZE_SPLIT_LEFT case .right: - return GHOSTTY_RESIZE_SPLIT_RIGHT; + return GHOSTTY_RESIZE_SPLIT_RIGHT } } } @@ -268,7 +268,7 @@ extension Ghostty { /// The text to show in the clipboard confirmation prompt for a given request type func text() -> String { - switch (self) { + switch self { case .paste: return """ Pasting this text to the terminal may be dangerous as it looks like some commands may be executed. @@ -287,7 +287,7 @@ extension Ghostty { } static func from(request: ghostty_clipboard_request_e) -> ClipboardRequest? { - switch (request) { + switch request { case GHOSTTY_CLIPBOARD_REQUEST_PASTE: return .paste case GHOSTTY_CLIPBOARD_REQUEST_OSC_52_READ: @@ -299,17 +299,17 @@ extension Ghostty { } } } - + struct ClipboardContent { let mime: String let data: String - + static func from(content: ghostty_clipboard_content_s) -> ClipboardContent? { guard let mimePtr = content.mime, let dataPtr = content.data else { return nil } - + return ClipboardContent( mime: String(cString: mimePtr), data: String(cString: dataPtr) @@ -498,4 +498,4 @@ extension Ghostty.Notification { } // Make the input enum hashable. -extension ghostty_input_key_e : @retroactive Hashable {} +extension ghostty_input_key_e: @retroactive Hashable {} diff --git a/macos/Sources/Ghostty/Surface View/InspectorView.swift b/macos/Sources/Ghostty/Surface View/InspectorView.swift index 03be794e9..e7320c782 100644 --- a/macos/Sources/Ghostty/Surface View/InspectorView.swift +++ b/macos/Sources/Ghostty/Surface View/InspectorView.swift @@ -23,7 +23,7 @@ extension Ghostty { let pubInspector = center.publisher(for: Notification.didControlInspector, object: surfaceView) ZStack { - if (!surfaceView.inspectorVisible) { + if !surfaceView.inspectorVisible { SurfaceWrapper(surfaceView: surfaceView, isSplit: isSplit) } else { SplitView(.vertical, $split, dividerColor: ghostty.config.splitDividerColor, left: { @@ -42,7 +42,7 @@ extension Ghostty { .onChange(of: surfaceView.inspectorVisible) { inspectorVisible in // 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 (inspectorVisible) { + if inspectorVisible { // We need to delay this until SwiftUI shows the inspector. DispatchQueue.main.async { _ = surfaceView.resignFirstResponder() @@ -59,7 +59,7 @@ extension Ghostty { guard let modeAny = notification.userInfo?["mode"] else { return } guard let mode = modeAny as? ghostty_action_inspector_e else { return } - switch (mode) { + switch mode { case GHOSTTY_INSPECTOR_TOGGLE: surfaceView.inspectorVisible = !surfaceView.inspectorVisible @@ -94,7 +94,7 @@ extension Ghostty { class InspectorView: MTKView, NSTextInputClient { let commandQueue: MTLCommandQueue - var surfaceView: SurfaceView? = nil { + var surfaceView: SurfaceView? { didSet { surfaceViewDidChange() } } @@ -180,7 +180,7 @@ extension Ghostty { override func becomeFirstResponder() -> Bool { let result = super.becomeFirstResponder() - if (result) { + if result { if let inspector = self.inspector { inspector.setFocus(true) } @@ -190,7 +190,7 @@ extension Ghostty { override func resignFirstResponder() -> Bool { let result = super.resignFirstResponder() - if (result) { + if result { if let inspector = self.inspector { inspector.setFocus(false) } @@ -275,7 +275,7 @@ extension Ghostty { // Determine our momentum value var momentum: ghostty_input_mouse_momentum_e = GHOSTTY_MOUSE_MOMENTUM_NONE - switch (event.momentumPhase) { + switch event.momentumPhase { case .began: momentum = GHOSTTY_MOUSE_MOMENTUM_BEGAN case .stationary: @@ -309,8 +309,8 @@ extension Ghostty { } override func flagsChanged(with event: NSEvent) { - let mod: UInt32; - switch (event.keyCode) { + 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 @@ -325,7 +325,7 @@ extension Ghostty { // 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 } + if mods.rawValue & mod != 0 { action = GHOSTTY_ACTION_PRESS } keyAction(action, event: event) } @@ -382,7 +382,7 @@ extension Ghostty { } func firstRect(forCharacterRange range: NSRange, actualRange: NSRangePointer?) -> NSRect { - return NSMakeRect(frame.origin.x, frame.origin.y, 0, 0) + return NSRect(x: frame.origin.x, y: frame.origin.y, width: 0, height: 0) } func insertText(_ string: Any, replacementRange: NSRange) { @@ -392,7 +392,7 @@ extension Ghostty { // We want the string view of the any value var chars = "" - switch (string) { + switch string { case let v as NSAttributedString: chars = v.string case let v as String: @@ -402,7 +402,7 @@ extension Ghostty { } let len = chars.utf8CString.count - if (len == 0) { return } + if len == 0 { return } inspector.text(chars) } diff --git a/macos/Sources/Ghostty/Surface View/SurfaceDragSource.swift b/macos/Sources/Ghostty/Surface View/SurfaceDragSource.swift index 37a69852e..dd2f3ef5e 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceDragSource.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceDragSource.swift @@ -5,13 +5,13 @@ extension Ghostty { /// A preference key that propagates the ID of the SurfaceView currently being dragged, /// or nil if no surface is being dragged. struct DraggingSurfaceKey: PreferenceKey { - static var defaultValue: SurfaceView.ID? = nil - + static var defaultValue: SurfaceView.ID? + static func reduce(value: inout SurfaceView.ID?, nextValue: () -> SurfaceView.ID?) { value = nextValue() ?? value } } - + /// A SwiftUI view that provides drag source functionality for terminal surfaces. /// /// This view wraps an AppKit-based drag source to enable drag-and-drop reordering @@ -24,13 +24,13 @@ extension Ghostty { struct SurfaceDragSource: View { /// The surface view that will be dragged. let surfaceView: SurfaceView - + /// Binding that reflects whether a drag session is currently active. @Binding var isDragging: Bool - + /// Binding that reflects whether the mouse is hovering over this view. @Binding var isHovering: Bool - + var body: some View { SurfaceDragSourceViewRepresentable( surfaceView: surfaceView, @@ -46,7 +46,7 @@ extension Ghostty { let surfaceView: SurfaceView @Binding var isDragging: Bool @Binding var isHovering: Bool - + func makeNSView(context: Context) -> SurfaceDragSourceView { let view = SurfaceDragSourceView() view.surfaceView = surfaceView @@ -60,7 +60,7 @@ extension Ghostty { } return view } - + func updateNSView(_ nsView: SurfaceDragSourceView, context: Context) { nsView.surfaceView = surfaceView nsView.onDragStateChanged = { dragging in @@ -73,7 +73,7 @@ extension Ghostty { } } } - + /// The underlying NSView that handles drag operations. /// /// This view manages mouse tracking and drag initiation for surface reordering. @@ -82,26 +82,26 @@ extension Ghostty { fileprivate class SurfaceDragSourceView: NSView, NSDraggingSource { /// Scale factor applied to the surface snapshot for the drag preview image. private static let previewScale: CGFloat = 0.2 - + /// The surface view that will be dragged. Its UUID is encoded into the /// pasteboard for drop targets to identify which surface is being moved. var surfaceView: SurfaceView? - + /// Callback invoked when the drag state changes. Called with `true` when /// a drag session begins, and `false` when it ends (completed or cancelled). var onDragStateChanged: ((Bool) -> Void)? - + /// Callback invoked when the mouse enters or exits this view's bounds. /// Used to update the hover state for visual feedback in the parent view. var onHoverChanged: ((Bool) -> Void)? - + /// Whether we are currently in a mouse tracking loop (between mouseDown /// and either mouseUp or drag initiation). Used to determine cursor state. private var isTracking: Bool = false - + /// Local event monitor to detect escape key presses during drag. private var escapeMonitor: Any? - + /// Whether the current drag was cancelled by pressing escape. private var dragCancelledByEscape: Bool = false @@ -137,26 +137,26 @@ extension Ghostty { userInfo: nil )) } - + override func resetCursorRects() { addCursorRect(bounds, cursor: isTracking ? .closedHand : .openHand) } - + override func mouseEntered(with event: NSEvent) { onHoverChanged?(true) } - + override func mouseExited(with event: NSEvent) { onHoverChanged?(false) } - + override func mouseDragged(with event: NSEvent) { guard !isTracking, let surfaceView = surfaceView else { return } - + // Create our dragging item from our transferable guard let pasteboardItem = surfaceView.pasteboardItem() else { return } let item = NSDraggingItem(pasteboardWriter: pasteboardItem) - + // Create a scaled preview image from the surface snapshot if let snapshot = surfaceView.asImage { let imageSize = NSSize( @@ -172,7 +172,7 @@ extension Ghostty { fraction: 1.0 ) scaledImage.unlockFocus() - + // Position the drag image so the mouse is at the center of the image. // I personally like the top middle or top left corner best but // this matches macOS native tab dragging behavior (at least, as of @@ -187,30 +187,30 @@ extension Ghostty { contents: scaledImage ) } - + onDragStateChanged?(true) let session = beginDraggingSession(with: [item], event: event, source: self) - + // We need to disable this so that endedAt happens immediately for our // drags outside of any targets. session.animatesToStartingPositionsOnCancelOrFail = false } - + // MARK: NSDraggingSource - + func draggingSession( _ session: NSDraggingSession, sourceOperationMaskFor context: NSDraggingContext ) -> NSDragOperation { return context == .withinApplication ? .move : [] } - + func draggingSession( _ session: NSDraggingSession, willBeginAt screenPoint: NSPoint ) { isTracking = true - + // Reset our escape tracking dragCancelledByEscape = false escapeMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event in @@ -220,14 +220,14 @@ extension Ghostty { return event } } - + func draggingSession( _ session: NSDraggingSession, movedTo screenPoint: NSPoint ) { NSCursor.closedHand.set() } - + func draggingSession( _ session: NSDraggingSession, endedAt screenPoint: NSPoint, @@ -262,7 +262,7 @@ extension Notification.Name { /// released outside a valid drop target) and was not cancelled by the user /// pressing escape. The notification's object is the SurfaceView that was dragged. static let ghosttySurfaceDragEndedNoTarget = Notification.Name("ghosttySurfaceDragEndedNoTarget") - + /// Key for the screen point where the drag ended in the userInfo dictionary. static let ghosttySurfaceDragEndedNoTargetPointKey = "endedAtPoint" } diff --git a/macos/Sources/Ghostty/Surface View/SurfaceGrabHandle.swift b/macos/Sources/Ghostty/Surface View/SurfaceGrabHandle.swift index f3ee80874..ff751df10 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceGrabHandle.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceGrabHandle.swift @@ -1,17 +1,17 @@ import AppKit import SwiftUI -extension Ghostty { +extension Ghostty { /// A grab handle overlay at the top of the surface for dragging the window. /// Only appears when hovering in the top region of the surface. struct SurfaceGrabHandle: View { private let handleHeight: CGFloat = 10 - + let surfaceView: SurfaceView - + @State private var isHovering: Bool = false @State private var isDragging: Bool = false - + var body: some View { VStack(spacing: 0) { Rectangle() @@ -32,7 +32,7 @@ extension Ghostty { isHovering: $isHovering ) } - + Spacer() } .frame(maxWidth: .infinity, maxHeight: .infinity) diff --git a/macos/Sources/Ghostty/Surface View/SurfaceProgressBar.swift b/macos/Sources/Ghostty/Surface View/SurfaceProgressBar.swift index 82d26e681..0478bf2bf 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceProgressBar.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceProgressBar.swift @@ -5,7 +5,7 @@ import SwiftUI /// control. struct SurfaceProgressBar: View { let report: Ghostty.Action.ProgressReport - + private var color: Color { switch report.state { case .error: return .red @@ -13,17 +13,17 @@ struct SurfaceProgressBar: View { default: return .accentColor } } - + private var progress: UInt8? { // If we have an explicit progress use that. if let v = report.progress { return v } - + // Otherwise, if we're in the pause state, we act as if we're at 100%. if report.state == .pause { return 100 } - + return nil } - + private var accessibilityLabel: String { switch report.state { case .error: return "Terminal progress - Error" @@ -32,7 +32,7 @@ struct SurfaceProgressBar: View { default: return "Terminal progress" } } - + private var accessibilityValue: String { if let progress { return "\(progress) percent complete" @@ -45,7 +45,7 @@ struct SurfaceProgressBar: View { } } } - + var body: some View { GeometryReader { geometry in ZStack(alignment: .leading) { @@ -78,15 +78,15 @@ struct SurfaceProgressBar: View { private struct BouncingProgressBar: View { let color: Color @State private var position: CGFloat = 0 - + private let barWidthRatio: CGFloat = 0.25 - + var body: some View { GeometryReader { geometry in ZStack(alignment: .leading) { Rectangle() .fill(color.opacity(0.3)) - + Rectangle() .fill(color) .frame( @@ -110,4 +110,3 @@ private struct BouncingProgressBar: View { } } - diff --git a/macos/Sources/Ghostty/Surface View/SurfaceScrollView.swift b/macos/Sources/Ghostty/Surface View/SurfaceScrollView.swift index b55f2e231..aab99c088 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceScrollView.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceScrollView.swift @@ -19,12 +19,12 @@ class SurfaceScrollView: NSView { private var observers: [NSObjectProtocol] = [] private var cancellables: Set = [] private var isLiveScrolling = false - + /// The last row position sent via scroll_to_row action. Used to avoid /// sending redundant actions when the user drags the scrollbar but stays /// on the same row. private var lastSentRow: Int? - + init(contentSize: CGSize, surfaceView: Ghostty.SurfaceView) { self.surfaceView = surfaceView // The scroll view is our outermost view that controls all our scrollbar @@ -44,26 +44,26 @@ class SurfaceScrollView: NSView { // (we currently only use overlay scrollers, but might as well // configure the views correctly in case we change our mind) scrollView.contentView.clipsToBounds = false - + // The document view is what the scrollview is actually going // to be directly scrolling. We set it up to a "blank" NSView // with the desired content size. documentView = NSView(frame: NSRect(origin: .zero, size: contentSize)) scrollView.documentView = documentView - + // The document view contains our actual surface as a child. // We synchronize the scrolling of the document with this surface // so that our primary Ghostty renderer only needs to render the viewport. documentView.addSubview(surfaceView) - + super.init(frame: .zero) - + // Our scroll view is our only view addSubview(scrollView) - + // Apply initial scrollbar settings synchronizeAppearance() - + // We listen for scroll events through bounds notifications on our NSClipView. // This is based on: https://christiantietze.de/posts/2018/07/synchronize-nsscrollview/ scrollView.contentView.postsBoundsChangedNotifications = true @@ -74,7 +74,7 @@ class SurfaceScrollView: NSView { ) { [weak self] notification in self?.handleScrollChange(notification) }) - + // Listen for scrollbar updates from Ghostty observers.append(NotificationCenter.default.addObserver( forName: .ghosttyDidUpdateScrollbar, @@ -83,7 +83,7 @@ class SurfaceScrollView: NSView { ) { [weak self] notification in self?.handleScrollbarUpdate(notification) }) - + // Listen for live scroll events observers.append(NotificationCenter.default.addObserver( forName: NSScrollView.willStartLiveScrollNotification, @@ -92,7 +92,7 @@ class SurfaceScrollView: NSView { ) { [weak self] _ in self?.isLiveScrolling = true }) - + observers.append(NotificationCenter.default.addObserver( forName: NSScrollView.didEndLiveScrollNotification, object: scrollView, @@ -100,7 +100,7 @@ class SurfaceScrollView: NSView { ) { [weak self] _ in self?.isLiveScrolling = false }) - + observers.append(NotificationCenter.default.addObserver( forName: NSScrollView.didLiveScrollNotification, object: scrollView, @@ -108,7 +108,7 @@ class SurfaceScrollView: NSView { ) { [weak self] _ in self?.handleLiveScroll() }) - + observers.append(NotificationCenter.default.addObserver( forName: NSScroller.preferredScrollerStyleDidChangeNotification, object: nil, @@ -150,11 +150,11 @@ class SurfaceScrollView: NSView { } .store(in: &cancellables) } - + required init?(coder: NSCoder) { fatalError("init(coder:) not implemented") } - + deinit { observers.forEach { NotificationCenter.default.removeObserver($0) } } @@ -163,10 +163,10 @@ class SurfaceScrollView: NSView { // insets. This is necessary for the content view to match the // surface view if we have the "hidden" titlebar style. override var safeAreaInsets: NSEdgeInsets { return NSEdgeInsetsZero } - + override func layout() { super.layout() - + // Fill entire bounds with scroll view scrollView.frame = bounds surfaceView.frame.size = scrollView.bounds.size @@ -174,13 +174,13 @@ class SurfaceScrollView: NSView { // We only set the width of the documentView here, as the height depends // on the scrollbar state and is updated in synchronizeScrollView documentView.frame.size.width = scrollView.bounds.width - + // When our scrollview changes make sure our scroller and surface views are synchronized synchronizeScrollView() synchronizeSurfaceView() synchronizeCoreSurface() } - + // MARK: Scrolling private func synchronizeAppearance() { @@ -220,7 +220,7 @@ class SurfaceScrollView: NSView { private func synchronizeScrollView() { // Update the document height to give our scroller the correct proportions documentView.frame.size.height = documentHeight() - + // Only update our actual scroll position if we're not actively scrolling. if !isLiveScrolling { // Convert row units to pixels using cell height, ignore zero height. @@ -236,13 +236,13 @@ class SurfaceScrollView: NSView { lastSentRow = Int(scrollbar.offset) } } - + // Always update our scrolled view with the latest dimensions scrollView.reflectScrolledClipView(scrollView.contentView) } - + // MARK: Notifications - + /// Handles bounds changes in the scroll view's clip view, keeping the surface view synchronized. private func handleScrollChange(_ notification: Notification) { synchronizeSurfaceView() @@ -259,7 +259,7 @@ class SurfaceScrollView: NSView { synchronizeAppearance() synchronizeCoreSurface() } - + /// Handles live scroll events (user actively dragging the scrollbar). /// /// Converts the current scroll position to a row number and sends a `scroll_to_row` action @@ -270,21 +270,21 @@ class SurfaceScrollView: NSView { // happen with a tiny terminal. let cellHeight = surfaceView.cellSize.height guard cellHeight > 0 else { return } - + // AppKit views are +Y going up, so we calculate from the bottom let visibleRect = scrollView.contentView.documentVisibleRect let documentHeight = documentView.frame.height let scrollOffset = documentHeight - visibleRect.origin.y - visibleRect.height let row = Int(scrollOffset / cellHeight) - + // Only send action if the row changed to avoid action spam guard row != lastSentRow else { return } lastSentRow = row - + // Use the keybinding action to scroll. _ = surfaceView.surfaceModel?.perform(action: "scroll_to_row:\(row)") } - + /// Handles scrollbar state updates from the terminal core. /// /// Updates the document view size to reflect total scrollback and adjusts scroll position diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView+Transferable.swift b/macos/Sources/Ghostty/Surface View/SurfaceView+Transferable.swift index 509713309..106875813 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceView+Transferable.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView+Transferable.swift @@ -17,11 +17,11 @@ extension Ghostty.SurfaceView: Transferable { let uuid = data.withUnsafeBytes { $0.load(as: UUID.self) } - + guard let imported = await Self.find(uuid: uuid) else { throw TransferError.invalidData } - + return imported } } @@ -29,7 +29,7 @@ extension Ghostty.SurfaceView: Transferable { enum TransferError: Error { case invalidData } - + @MainActor static func find(uuid: UUID) -> Self? { #if canImport(AppKit) diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView.swift b/macos/Sources/Ghostty/Surface View/SurfaceView.swift index c5c2ee97c..221bc4c37 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceView.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView.swift @@ -49,7 +49,7 @@ extension Ghostty { // True if we're hovering over the left URL view, so we can show it on the right. @State private var isHoveringURLLeft: Bool = false - + #if canImport(AppKit) // Observe SecureInput to detect when its enabled @ObservedObject private var secureInput = SecureInput.shared @@ -84,7 +84,7 @@ extension Ghostty { .onReceive(pubResign) { notification in guard let window = notification.object as? NSWindow else { return } guard let surfaceWindow = surfaceView.window else { return } - if (surfaceWindow == window) { + if surfaceWindow == window { windowFocus = false } } @@ -103,7 +103,7 @@ extension Ghostty { } } .ghosttySurfaceView(surfaceView) - + // Progress report if let progressReport = surfaceView.progressReport, progressReport.state != .remove { VStack(spacing: 0) { @@ -114,7 +114,7 @@ extension Ghostty { .allowsHitTesting(false) .transition(.opacity) } - + #if canImport(AppKit) // Readonly indicator badge if surfaceView.readonly { @@ -122,7 +122,7 @@ extension Ghostty { surfaceView.toggleReadonly(nil) } } - + // Show key state indicator for active key tables and/or pending key sequences KeyStateIndicator( keyTables: surfaceView.keyTables, @@ -177,10 +177,10 @@ extension Ghostty { #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 && + if ghostty.config.secureInputIndication && secureInput.enabled && surfaceFocus && - windowFocus) { + windowFocus { SecureInputOverlay() } #endif @@ -200,7 +200,7 @@ extension Ghostty { } // Show bell border if enabled - if (ghostty.config.bellFeatures.contains(.border)) { + if ghostty.config.bellFeatures.contains(.border) { BellBorderOverlay(bell: surfaceView.bell) } @@ -208,10 +208,10 @@ extension Ghostty { HighlightOverlay(highlighted: surfaceView.highlighted) // If our surface is not healthy, then we render an error view over it. - if (!surfaceView.healthy) { + if !surfaceView.healthy { Rectangle().fill(ghostty.config.backgroundColor) SurfaceRendererUnhealthyView() - } else if (surfaceView.error != nil) { + } else if surfaceView.error != nil { Rectangle().fill(ghostty.config.backgroundColor) SurfaceErrorView() } @@ -220,9 +220,9 @@ extension Ghostty { // rectangle above our view to make it look unfocused. We use "surfaceFocus" // because we want to keep our focused surface dark even if we don't have window // focus. - if (isSplit && !surfaceFocus) { - let overlayOpacity = ghostty.config.unfocusedSplitOpacity; - if (overlayOpacity > 0) { + if isSplit && !surfaceFocus { + let overlayOpacity = ghostty.config.unfocusedSplitOpacity + if overlayOpacity > 0 { Rectangle() .fill(ghostty.config.unfocusedSplitFill) .allowsHitTesting(false) @@ -286,8 +286,6 @@ extension Ghostty { } } - - // This is the resize overlay that shows on top of a surface to show the current // size during a resize operation. struct SurfaceResizeOverlay: View { @@ -300,7 +298,7 @@ extension Ghostty { // This is the last size that we processed. This is how we handle our // timer state. - @State var lastSize: CGSize? = nil + @State var lastSize: CGSize? // Ready is set to true after a short delay. This avoids some of the // challenges of initial view sizing from SwiftUI. @@ -312,42 +310,42 @@ extension Ghostty { // This computed boolean is set to true when the overlay should be hidden. private var hidden: Bool { // If we aren't ready yet then we wait... - if (!ready) { return true; } + if !ready { return true; } // Hidden if we already processed this size. - if (lastSize == geoSize) { return true; } + if lastSize == geoSize { return true; } // If we were focused recently we hide it as well. This avoids showing // the resize overlay when SwiftUI is lazily resizing. if let instant = focusInstant { let d = instant.duration(to: ContinuousClock.now) - if (d < .milliseconds(500)) { + if d < .milliseconds(500) { // Avoid this size completely. We can't set values during // view updates so we have to defer this to another tick. DispatchQueue.main.async { lastSize = geoSize } - return true; + return true } } // Hidden depending on overlay config - switch (overlay) { - case .never: return true; - case .always: return false; - case .after_first: return lastSize == nil; + switch overlay { + case .never: return true + case .always: return false + case .after_first: return lastSize == nil } } var body: some View { VStack { - if (!position.top()) { + if !position.top() { Spacer() } HStack { - if (!position.left()) { + if !position.left() { Spacer() } @@ -361,12 +359,12 @@ extension Ghostty { .lineLimit(1) .truncationMode(.tail) - if (!position.right()) { + if !position.right() { Spacer() } } - if (!position.bottom()) { + if !position.bottom() { Spacer() } } @@ -386,7 +384,7 @@ extension Ghostty { // We only sleep if we're ready. If we're not ready then we want to set // our last size right away to avoid a flash. - if (ready) { + if ready { try? await Task.sleep(nanoseconds: UInt64(duration) * 1_000_000) } @@ -404,9 +402,9 @@ extension Ghostty { @State private var dragOffset: CGSize = .zero @State private var barSize: CGSize = .zero @FocusState private var isSearchFieldFocused: Bool - + private let padding: CGFloat = 8 - + var body: some View { GeometryReader { geo in HStack(spacing: 4) { @@ -460,7 +458,7 @@ extension Ghostty { Image(systemName: "chevron.up") } .buttonStyle(SearchButtonStyle()) - + Button(action: { guard let surface = surfaceView.surface else { return } let action = "navigate_search:previous" @@ -469,7 +467,7 @@ extension Ghostty { Image(systemName: "chevron.down") } .buttonStyle(SearchButtonStyle()) - + Button(action: onClose) { Image(systemName: "xmark") } @@ -529,7 +527,7 @@ extension Ghostty { enum Corner { case topLeft, topRight, bottomLeft, bottomRight - + var alignment: Alignment { switch self { case .topLeft: return .topLeading @@ -539,11 +537,11 @@ extension Ghostty { } } } - + private func centerPosition(for corner: Corner, in containerSize: CGSize, barSize: CGSize) -> CGPoint { let halfWidth = barSize.width / 2 + padding let halfHeight = barSize.height / 2 + padding - + switch corner { case .topLeft: return CGPoint(x: halfWidth, y: halfHeight) @@ -555,21 +553,21 @@ extension Ghostty { return CGPoint(x: containerSize.width - halfWidth, y: containerSize.height - halfHeight) } } - + private func closestCorner(to point: CGPoint, in containerSize: CGSize) -> Corner { let midX = containerSize.width / 2 let midY = containerSize.height / 2 - + if point.x < midX { return point.y < midY ? .topLeft : .bottomLeft } else { return point.y < midY ? .topRight : .bottomRight } } - + struct SearchButtonStyle: ButtonStyle { @State private var isHovered = false - + func makeBody(configuration: Configuration) -> some View { configuration.label .foregroundStyle(isHovered || configuration.isPressed ? .primary : .secondary) @@ -584,7 +582,7 @@ extension Ghostty { } .backport.pointerStyle(.link) } - + private func backgroundColor(isPressed: Bool) -> Color { if isPressed { return Color.primary.opacity(0.2) @@ -640,20 +638,20 @@ extension Ghostty { /// libghostty, usually from the Ghostty configuration. struct SurfaceConfiguration { /// Explicit font size to use in points - var fontSize: Float32? = nil + var fontSize: Float32? /// Explicit working directory to set - var workingDirectory: String? = nil + var workingDirectory: String? /// Explicit command to set - var command: String? = nil - + var command: String? + /// Environment variables to set for the terminal var environmentVariables: [String: String] = [:] /// Extra input to send as stdin - var initialInput: String? = nil - + var initialInput: String? + /// Wait after the command var waitAfterCommand: Bool = false @@ -711,7 +709,7 @@ extension Ghostty { // Zero is our default value that means to inherit the font size. config.font_size = fontSize ?? 0 - + // Set wait after command config.wait_after_command = waitAfterCommand @@ -736,7 +734,7 @@ extension Ghostty { return try keys.withCStrings { keyCStrings in return try values.withCStrings { valueCStrings in // Create array of ghostty_env_var_s - var envVars = Array() + var envVars = [ghostty_env_var_s]() envVars.reserveCapacity(environmentVariables.count) for i in 0.. Double { let phase = animationPhase let offset = Double(index) / 3.0 @@ -981,7 +979,7 @@ extension Ghostty { /// Visual overlay that shows a border around the edges when the bell rings with border feature enabled. struct BellBorderOverlay: View { let bell: Bool - + var body: some View { Rectangle() .strokeBorder( @@ -998,7 +996,7 @@ extension Ghostty { /// Uses a soft, soothing highlight with a pulsing border effect. struct HighlightOverlay: View { let highlighted: Bool - + @State private var borderPulse: Bool = false var body: some View { @@ -1051,21 +1049,21 @@ extension Ghostty { } // MARK: Readonly Badge - + /// A badge overlay that indicates a surface is in readonly mode. /// Positioned in the top-right corner and styled to be noticeable but unobtrusive. struct ReadonlyBadge: View { let onDisable: () -> Void - + @State private var showingPopover = false - + private let badgeColor = Color(hue: 0.08, saturation: 0.5, brightness: 0.8) - + var body: some View { VStack { HStack { Spacer() - + HStack(spacing: 5) { Image(systemName: "eye.fill") .font(.system(size: 12)) @@ -1085,13 +1083,13 @@ extension Ghostty { } } .padding(8) - + Spacer() } .accessibilityElement(children: .ignore) .accessibilityLabel("Read-only terminal") } - + private var badgeBackground: some View { RoundedRectangle(cornerRadius: 6) .fill(.regularMaterial) @@ -1101,11 +1099,11 @@ extension Ghostty { ) } } - + struct ReadonlyPopoverView: View { let onDisable: () -> Void @Binding var isPresented: Bool - + var body: some View { VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 8) { @@ -1116,16 +1114,16 @@ extension Ghostty { Text("Read-Only Mode") .font(.system(size: 13, weight: .semibold)) } - + Text("This terminal is in read-only mode. You can still view, select, and scroll through the content, but no input events will be sent to the running application.") .font(.system(size: 11)) .foregroundColor(.secondary) .fixedSize(horizontal: false, vertical: true) } - + HStack { Spacer() - + Button("Disable") { onDisable() isPresented = false @@ -1252,8 +1250,8 @@ extension FocusedValues { extension Ghostty.SurfaceView { class SearchState: ObservableObject { @Published var needle: String = "" - @Published var selected: UInt? = nil - @Published var total: UInt? = nil + @Published var selected: UInt? + @Published var total: UInt? init(from startSearch: Ghostty.Action.StartSearch) { self.needle = startSearch.needle ?? "" diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift index c856b0163..6b3bfbfb4 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift @@ -27,7 +27,7 @@ extension Ghostty { // The current pwd of the surface as defined by the pty. This can be // changed with escape codes. - @Published var pwd: String? = nil + @Published var pwd: String? // The cell size of this surface. This is set by the core when the // surface is first created and any time the cell size changes (i.e. @@ -40,13 +40,13 @@ extension Ghostty { @Published var healthy: Bool = true // Any error while initializing the surface. - @Published var error: Error? = nil + @Published var error: Error? // The hovered URL string - @Published var hoverUrl: String? = nil + @Published var hoverUrl: String? // The progress report (if any) - @Published var progressReport: Action.ProgressReport? = nil { + @Published var progressReport: Action.ProgressReport? { didSet { // Cancel any existing timer progressReportTimer?.invalidate() @@ -69,7 +69,7 @@ extension Ghostty { @Published var keyTables: [String] = [] // The current search state. When non-nil, the search overlay should be shown. - @Published var searchState: SearchState? = nil { + @Published var searchState: SearchState? { didSet { if let searchState { // I'm not a Combine expert so if there is a better way to do this I'm @@ -107,11 +107,11 @@ extension Ghostty { // The time this surface last became focused. This is a ContinuousClock.Instant // on supported platforms. - @Published var focusInstant: ContinuousClock.Instant? = nil + @Published var focusInstant: ContinuousClock.Instant? // Returns sizing information for the surface. This is the raw C // structure because I'm lazy. - @Published var surfaceSize: ghostty_surface_size_s? = nil + @Published var surfaceSize: ghostty_surface_size_s? // Whether the pointer should be visible or not @Published private(set) var pointerStyle: CursorStyle = .horizontalText @@ -121,7 +121,7 @@ extension Ghostty { /// The background color within the color palette of the surface. This is only set if it is /// dynamically updated. Otherwise, the background color is the default background color. - @Published private(set) var backgroundColor: Color? = nil + @Published private(set) var backgroundColor: Color? /// True when the bell is active. This is set inactive on focus or event. @Published private(set) var bell: Bool = false @@ -134,7 +134,7 @@ extension Ghostty { // An initial size to request for a window. This will only affect // then the view is moved to a new window. - var initialSize: NSSize? = nil + var initialSize: NSSize? // A content size received through sizeDidChange that may in some cases // be different from the frame size. @@ -151,7 +151,7 @@ extension Ghostty { // We need to update our state within the SecureInput manager. let input = SecureInput.shared let id = ObjectIdentifier(self) - if (passwordInput) { + if passwordInput { input.setScoped(id, focused: focused) } else { input.removeScoped(id) @@ -183,7 +183,7 @@ extension Ghostty { // True if the inspector should be visible @Published var inspectorVisible: Bool = false { didSet { - if (oldValue && !inspectorVisible) { + if oldValue && !inspectorVisible { guard let surface = self.surface else { return } ghostty_inspector_free(surface) } @@ -210,10 +210,10 @@ extension Ghostty { private var markedText: NSMutableAttributedString private(set) var focused: Bool = true private var prevPressureStage: Int = 0 - private var appearanceObserver: NSKeyValueObservation? = nil + private var appearanceObserver: NSKeyValueObservation? // This is set to non-null during keyDown to accumulate insertText contents - private var keyTextAccumulator: [String]? = nil + private var keyTextAccumulator: [String]? // A small delay that is introduced before a title change to avoid flickers private var titleChangeTimer: Timer? @@ -234,7 +234,7 @@ extension Ghostty { private(set) var cachedVisibleContents: CachedValue /// Event monitor (see individual events for why) - private var eventMonitor: Any? = nil + private var eventMonitor: Any? // We need to support being a first responder so that we can get input events override var acceptsFirstResponder: Bool { return true } @@ -259,7 +259,7 @@ extension Ghostty { // 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(frame: NSMakeRect(0, 0, 800, 600)) + super.init(frame: NSRect(x: 0, y: 0, width: 800, height: 600)) // Our cache of screen data cachedScreenContents = .init(duration: .milliseconds(500)) { [weak self] in @@ -431,11 +431,11 @@ extension Ghostty { ghostty_surface_set_focus(surface, focused) // Update our secure input state if we are a password input - if (passwordInput) { + if passwordInput { SecureInput.shared.setScoped(ObjectIdentifier(self), focused: focused) } - if (focused) { + if focused { // On macOS 13+ we can store our continuous clock... focusInstant = ContinuousClock.now @@ -480,7 +480,7 @@ extension Ghostty { } func setCursorShape(_ shape: ghostty_action_mouse_shape_e) { - switch (shape) { + switch shape { case GHOSTTY_MOUSE_SHAPE_DEFAULT: pointerStyle = .default @@ -656,7 +656,7 @@ extension Ghostty { private func localEventKeyUp(_ event: NSEvent) -> NSEvent? { // We only care about events with "command" because all others will // trigger the normal responder chain. - if (!event.modifierFlags.contains(.command)) { return event } + if !event.modifierFlags.contains(.command) { return event } // Command keyUp events are never sent to the normal responder chain // so we send them here. @@ -722,7 +722,7 @@ extension Ghostty { SwiftUI.Notification.Name.GhosttyColorChangeKey ] as? Ghostty.Action.ColorChange else { return } - switch (change.kind) { + switch change.kind { case .background: DispatchQueue.main.async { [weak self] in self?.backgroundColor = change.color @@ -767,7 +767,7 @@ extension Ghostty { override func becomeFirstResponder() -> Bool { let result = super.becomeFirstResponder() - if (result) { focusDidChange(true) } + if result { focusDidChange(true) } return result } @@ -776,7 +776,7 @@ extension Ghostty { // We sometimes call this manually (see SplitView) as a way to force us to // yield our focus state. - if (result) { focusDidChange(false) } + if result { focusDidChange(false) } return result } @@ -873,17 +873,16 @@ extension Ghostty { ghostty_surface_mouse_button(surface, GHOSTTY_MOUSE_RELEASE, button.cMouseButton, mods) } - override func rightMouseDown(with event: NSEvent) { guard let surface = self.surface else { return super.rightMouseDown(with: event) } let mods = Ghostty.ghosttyMods(event.modifierFlags) - if (ghostty_surface_mouse_button( + if ghostty_surface_mouse_button( surface, GHOSTTY_MOUSE_PRESS, GHOSTTY_MOUSE_RIGHT, mods - )) { + ) { // Consumed return } @@ -896,12 +895,12 @@ extension Ghostty { guard let surface = self.surface else { return super.rightMouseUp(with: event) } let mods = Ghostty.ghosttyMods(event.modifierFlags) - if (ghostty_surface_mouse_button( + if ghostty_surface_mouse_button( surface, GHOSTTY_MOUSE_RELEASE, GHOSTTY_MOUSE_RIGHT, mods - )) { + ) { // Handled return } @@ -963,10 +962,9 @@ extension Ghostty { if let window, let controller = window.windowController as? BaseTerminalController, !controller.commandPaletteIsShowing, - (window.isKeyWindow && + window.isKeyWindow && !self.focused && - controller.focusFollowsMouse) - { + controller.focusFollowsMouse { Ghostty.moveFocus(to: self) } } @@ -992,8 +990,8 @@ extension Ghostty { if precision { // We do a 2x speed multiplier. This is subjective, it "feels" better to me. - x *= 2; - y *= 2; + x *= 2 + y *= 2 // TODO(mitchellh): do we have to scale the x/y here by window scale factor? } @@ -1048,7 +1046,7 @@ extension Ghostty { // for exact states and set them. var translationMods = event.modifierFlags for flag in [NSEvent.ModifierFlags.shift, .control, .option, .command] { - if (translationModsGhostty.contains(flag)) { + if translationModsGhostty.contains(flag) { translationMods.insert(flag) } else { translationMods.remove(flag) @@ -1061,7 +1059,7 @@ extension Ghostty { // this keeps things like Korean input working. There must be some object // equality happening in AppKit somewhere because this is required. let translationEvent: NSEvent - if (translationMods == event.modifierFlags) { + if translationMods == event.modifierFlags { translationEvent = event } else { translationEvent = NSEvent.keyEvent( @@ -1093,7 +1091,7 @@ extension Ghostty { // We need to know the keyboard layout before below because some keyboard // input events will change our keyboard layout and we don't want those // going to the terminal. - let keyboardIdBefore: String? = if (!markedTextBefore) { + let keyboardIdBefore: String? = if !markedTextBefore { KeyboardLayout.id } else { nil @@ -1108,7 +1106,7 @@ extension Ghostty { // If our keyboard changed from this we just assume an input method // grabbed it and do nothing. - if (!markedTextBefore && keyboardIdBefore != KeyboardLayout.id) { + if !markedTextBefore && keyboardIdBefore != KeyboardLayout.id { return } @@ -1185,17 +1183,17 @@ extension Ghostty { // We only care about key down events. It might not even be possible // to receive any other event type here. guard event.type == .keyDown else { return false } - + // Only process events if we're focused. Some key events like C-/ macOS // appears to send to the first view in the hierarchy rather than the // the first responder (I don't know why). This prevents us from handling it. // Besides C-/, its important we don't process key equivalents if unfocused // because there are other event listeners for that (i.e. AppDelegate's // local event handler). - if (!focused) { + if !focused { return false } - + // Get information about if this is a binding. let bindingFlags = surfaceModel.flatMap { surface in var ghosttyEvent = event.ghosttyKeyEvent(GHOSTTY_ACTION_PRESS) @@ -1204,7 +1202,7 @@ extension Ghostty { return surface.keyIsBinding(ghosttyEvent) } } - + // If this is a binding then we want to perform it. if let bindingFlags { // Attempt to trigger a menu item for this key binding. We only do this if: @@ -1221,17 +1219,17 @@ extension Ghostty { return true } } - + self.keyDown(with: event) return true } let equivalent: String - switch (event.charactersIgnoringModifiers) { + switch event.charactersIgnoringModifiers { case "\r": // Pass C- through verbatim // (prevent the default context menu equivalent) - if (!event.modifierFlags.contains(.control)) { + if !event.modifierFlags.contains(.control) { return false } @@ -1240,8 +1238,8 @@ extension Ghostty { case "/": // Treat C-/ as C-_. We do this because C-/ makes macOS make a beep // sound and we don't like the beep sound. - if (!event.modifierFlags.contains(.control) || - !event.modifierFlags.isDisjoint(with: [.shift, .command, .option])) { + if !event.modifierFlags.contains(.control) || + !event.modifierFlags.isDisjoint(with: [.shift, .command, .option]) { return false } @@ -1265,8 +1263,8 @@ extension Ghostty { // Ignore all other non-command events. This lets the event continue // through the AppKit event systems. - if (!event.modifierFlags.contains(.command) && - !event.modifierFlags.contains(.control)) { + if !event.modifierFlags.contains(.command) && + !event.modifierFlags.contains(.control) { // Reset since we got a non-command event. lastPerformKeyEvent = nil return false @@ -1304,8 +1302,8 @@ extension Ghostty { } override func flagsChanged(with event: NSEvent) { - let mod: UInt32; - switch (event.keyCode) { + 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 @@ -1323,26 +1321,26 @@ extension Ghostty { // If the key that pressed this is active, its a press, else release. var action = GHOSTTY_ACTION_RELEASE - if (mods.rawValue & mod != 0) { + if mods.rawValue & mod != 0 { // If the key is pressed, its slightly more complicated, because we // want to check if the pressed modifier is the correct side. If the // correct side is pressed then its a press event otherwise its a release // event with the opposite modifier still held. let sidePressed: Bool - switch (event.keyCode) { + switch event.keyCode { case 0x3C: - sidePressed = event.modifierFlags.rawValue & UInt(NX_DEVICERSHIFTKEYMASK) != 0; + sidePressed = event.modifierFlags.rawValue & UInt(NX_DEVICERSHIFTKEYMASK) != 0 case 0x3E: - sidePressed = event.modifierFlags.rawValue & UInt(NX_DEVICERCTLKEYMASK) != 0; + sidePressed = event.modifierFlags.rawValue & UInt(NX_DEVICERCTLKEYMASK) != 0 case 0x3D: - sidePressed = event.modifierFlags.rawValue & UInt(NX_DEVICERALTKEYMASK) != 0; + sidePressed = event.modifierFlags.rawValue & UInt(NX_DEVICERALTKEYMASK) != 0 case 0x36: - sidePressed = event.modifierFlags.rawValue & UInt(NX_DEVICERCMDKEYMASK) != 0; + sidePressed = event.modifierFlags.rawValue & UInt(NX_DEVICERCMDKEYMASK) != 0 default: sidePressed = true } - if (sidePressed) { + if sidePressed { action = GHOSTTY_ACTION_PRESS } } @@ -1389,7 +1387,7 @@ extension Ghostty { // since we always have a primary font. The only scenario this doesn't // work is if someone is using a non-CoreText build which would be // unofficial. - var attributes: [ NSAttributedString.Key : Any ] = [:]; + var attributes: [ NSAttributedString.Key: Any ] = [:] if let fontRaw = ghostty_surface_quicklook_font(surface) { // Memory management here is wonky: ghostty_surface_quicklook_font // will create a copy of a CTFont, Swift will auto-retain the @@ -1400,9 +1398,9 @@ extension Ghostty { } // Ghostty coordinate system is top-left, convert to bottom-left for AppKit - let pt = NSMakePoint(text.tl_px_x, frame.size.height - text.tl_px_y) + let pt = NSPoint(x: text.tl_px_x, y: frame.size.height - text.tl_px_y) let str = NSAttributedString.init(string: String(cString: text.text), attributes: attributes) - self.showDefinition(for: str, at: pt); + self.showDefinition(for: str, at: pt) } override func menu(for event: NSEvent) -> NSMenu? { @@ -1483,7 +1481,7 @@ extension Ghostty { @IBAction func copy(_ sender: Any?) { guard let surface = self.surface else { return } let action = "copy_to_clipboard" - if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) { AppDelegate.logger.warning("action failed action=\(action)") } } @@ -1491,16 +1489,15 @@ extension Ghostty { @IBAction func paste(_ sender: Any?) { guard let surface = self.surface else { return } let action = "paste_from_clipboard" - if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) { AppDelegate.logger.warning("action failed action=\(action)") } } - @IBAction func pasteAsPlainText(_ sender: Any?) { guard let surface = self.surface else { return } let action = "paste_from_clipboard" - if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) { AppDelegate.logger.warning("action failed action=\(action)") } } @@ -1508,7 +1505,7 @@ extension Ghostty { @IBAction func pasteSelection(_ sender: Any?) { guard let surface = self.surface else { return } let action = "paste_from_selection" - if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) { AppDelegate.logger.warning("action failed action=\(action)") } } @@ -1516,7 +1513,7 @@ extension Ghostty { @IBAction override func selectAll(_ sender: Any?) { guard let surface = self.surface else { return } let action = "select_all" - if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) { AppDelegate.logger.warning("action failed action=\(action)") } } @@ -1524,7 +1521,7 @@ extension Ghostty { @IBAction func find(_ sender: Any?) { guard let surface = self.surface else { return } let action = "start_search" - if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) { AppDelegate.logger.warning("action failed action=\(action)") } } @@ -1532,7 +1529,7 @@ extension Ghostty { @IBAction func selectionForFind(_ sender: Any?) { guard let surface = self.surface else { return } let action = "search_selection" - if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) { AppDelegate.logger.warning("action failed action=\(action)") } } @@ -1540,7 +1537,7 @@ extension Ghostty { @IBAction func scrollToSelection(_ sender: Any?) { guard let surface = self.surface else { return } let action = "scroll_to_selection" - if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) { AppDelegate.logger.warning("action failed action=\(action)") } } @@ -1548,7 +1545,7 @@ extension Ghostty { @IBAction func findNext(_ sender: Any?) { guard let surface = self.surface else { return } let action = "search:next" - if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) { AppDelegate.logger.warning("action failed action=\(action)") } } @@ -1556,7 +1553,7 @@ extension Ghostty { @IBAction func findPrevious(_ sender: Any?) { guard let surface = self.surface else { return } let action = "search:previous" - if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) { AppDelegate.logger.warning("action failed action=\(action)") } } @@ -1564,7 +1561,7 @@ extension Ghostty { @IBAction func findHide(_ sender: Any?) { guard let surface = self.surface else { return } let action = "end_search" - if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) { AppDelegate.logger.warning("action failed action=\(action)") } } @@ -1572,7 +1569,7 @@ extension Ghostty { @IBAction func toggleReadonly(_ sender: Any?) { guard let surface = self.surface else { return } let action = "toggle_readonly" - if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) { AppDelegate.logger.warning("action failed action=\(action)") } } @@ -1608,7 +1605,7 @@ extension Ghostty { @objc func resetTerminal(_ sender: Any) { guard let surface = self.surface else { return } let action = "reset" - if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) { AppDelegate.logger.warning("action failed action=\(action)") } } @@ -1616,7 +1613,7 @@ extension Ghostty { @objc func toggleTerminalInspector(_ sender: Any) { guard let surface = self.surface else { return } let action = "inspector:toggle" - if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) { AppDelegate.logger.warning("action failed action=\(action)") } } @@ -1657,7 +1654,7 @@ extension Ghostty { // If we're focused then we schedule to remove the notification // after a few seconds. If we gain focus we automatically remove it // in focusDidChange. - if (self.focused) { + if self.focused { Task { @MainActor [weak self] in try await Task.sleep(for: .seconds(3)) self?.notificationIdentifiers.remove(uuid) @@ -1831,7 +1828,7 @@ extension Ghostty.SurfaceView: NSTextInputClient { // since we always have a primary font. The only scenario this doesn't // work is if someone is using a non-CoreText build which would be // unofficial. - var attributes: [ NSAttributedString.Key : Any ] = [:]; + var attributes: [ NSAttributedString.Key: Any ] = [:] if let fontRaw = ghostty_surface_quicklook_font(surface) { // Memory management here is wonky: ghostty_surface_quicklook_font // will create a copy of a CTFont, Swift will auto-retain the @@ -1850,7 +1847,7 @@ extension Ghostty.SurfaceView: NSTextInputClient { func firstRect(forCharacterRange range: NSRange, actualRange: NSRangePointer?) -> NSRect { guard let surface = self.surface else { - return NSMakeRect(frame.origin.x, frame.origin.y, 0, 0) + return NSRect(x: frame.origin.x, y: frame.origin.y, width: 0, height: 0) } // Ghostty will tell us where it thinks an IME keyboard should render. @@ -1869,8 +1866,8 @@ extension Ghostty.SurfaceView: NSTextInputClient { if ghostty_surface_read_selection(surface, &text) { // The -2/+2 here is subjective. QuickLook seems to offset the rectangle // a bit and I think these small adjustments make it look more natural. - x = text.tl_px_x - 2; - y = text.tl_px_y + 2; + x = text.tl_px_x - 2 + y = text.tl_px_y + 2 // Free our text ghostty_surface_free_text(surface, &text) @@ -1892,11 +1889,11 @@ extension Ghostty.SurfaceView: NSTextInputClient { // when there's is no characters selected, // width should be 0 so that dictation indicator // can start in the right place - let viewRect = NSMakeRect( - x, - frame.size.height - y, - width, - max(height, cellSize.height)) + let viewRect = NSRect( + x: x, + y: frame.size.height - y, + width: width, + height: max(height, cellSize.height)) // Convert the point to the window coordinates let winRect = self.convert(viewRect, to: nil) @@ -1913,7 +1910,7 @@ extension Ghostty.SurfaceView: NSTextInputClient { // We want the string view of the any value var chars = "" - switch (string) { + switch string { case let v as NSAttributedString: chars = v.string case let v as String: @@ -1944,8 +1941,7 @@ extension Ghostty.SurfaceView: NSTextInputClient { // we send it back through the event system so it can be encoded. if let lastPerformKeyEvent, let current = NSApp.currentEvent, - lastPerformKeyEvent == current.timestamp - { + lastPerformKeyEvent == current.timestamp { NSApp.sendEvent(current) return } @@ -2052,7 +2048,7 @@ extension Ghostty.SurfaceView: NSServicesMenuRequestor { guard let str = pboard.getOpinionatedStringContents() else { return false } let len = str.utf8CString.count - if (len == 0) { return true } + if len == 0 { return true } str.withCString { ptr in // len includes the null terminator so we do len - 1 ghostty_surface_text(surface, ptr, UInt(len - 1)) @@ -2134,7 +2130,7 @@ extension Ghostty.SurfaceView { DispatchQueue.main.async { self.insertText( content, - replacementRange: NSMakeRange(0, 0) + replacementRange: NSRange(location: 0, length: 0) ) } return true diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView_UIKit.swift b/macos/Sources/Ghostty/Surface View/SurfaceView_UIKit.swift index f9baf56c9..9a4cf4d9b 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceView_UIKit.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView_UIKit.swift @@ -15,7 +15,7 @@ extension Ghostty { @Published var title: String = "👻" // The current pwd of the surface. - @Published var pwd: String? = nil + @Published var pwd: String? // The cell size of this surface. This is set by the core when the // surface is first created and any time the cell size changes (i.e. @@ -28,30 +28,30 @@ extension Ghostty { @Published var healthy: Bool = true // Any error while initializing the surface. - @Published var error: Error? = nil + @Published var error: Error? // The hovered URL - @Published var hoverUrl: String? = nil - + @Published var hoverUrl: String? + // The progress report (if any) - @Published var progressReport: Action.ProgressReport? = nil + @Published var progressReport: Action.ProgressReport? // The time this surface last became focused. This is a ContinuousClock.Instant // on supported platforms. - @Published var focusInstant: ContinuousClock.Instant? = nil + @Published var focusInstant: ContinuousClock.Instant? /// True when the bell is active. This is set inactive on focus or event. @Published var bell: Bool = false - + // The current search state. When non-nil, the search overlay should be shown. - @Published var searchState: SearchState? = nil + @Published var searchState: SearchState? // The currently active key tables. Empty if no tables are active. @Published var keyTables: [String] = [] /// True when the surface is in readonly mode. @Published private(set) var readonly: Bool = false - + /// True when the surface should show a highlight effect (e.g., when presented via goto_split). @Published private(set) var highlighted: Bool = false @@ -81,7 +81,7 @@ extension Ghostty { // TODO return } - self.surface = surface; + self.surface = surface } required init?(coder: NSCoder) { @@ -98,7 +98,7 @@ extension Ghostty { ghostty_surface_set_focus(surface, focused) // On macOS 13+ we can store our continuous clock... - if (focused) { + if focused { focusInstant = ContinuousClock.now } } @@ -122,9 +122,7 @@ extension Ghostty { // MARK: UIView override class var layerClass: AnyClass { - get { - return CAMetalLayer.self - } + return CAMetalLayer.self } override func didMoveToWindow() { diff --git a/macos/Sources/Helpers/AnySortKey.swift b/macos/Sources/Helpers/AnySortKey.swift index 6813ccf45..ffafb6b90 100644 --- a/macos/Sources/Helpers/AnySortKey.swift +++ b/macos/Sources/Helpers/AnySortKey.swift @@ -4,7 +4,7 @@ import Foundation struct AnySortKey: Comparable { private let value: Any private let comparator: (Any, Any) -> ComparisonResult - + init(_ value: T) { self.value = value self.comparator = { lhs, rhs in @@ -14,11 +14,11 @@ struct AnySortKey: Comparable { return .orderedSame } } - + static func < (lhs: AnySortKey, rhs: AnySortKey) -> Bool { lhs.comparator(lhs.value, rhs.value) == .orderedAscending } - + static func == (lhs: AnySortKey, rhs: AnySortKey) -> Bool { lhs.comparator(lhs.value, rhs.value) == .orderedSame } diff --git a/macos/Sources/Helpers/AppInfo.swift b/macos/Sources/Helpers/AppInfo.swift index 281bad18b..940d247d5 100644 --- a/macos/Sources/Helpers/AppInfo.swift +++ b/macos/Sources/Helpers/AppInfo.swift @@ -2,9 +2,5 @@ import Foundation /// True if we appear to be running in Xcode. func isRunningInXcode() -> Bool { - if let _ = ProcessInfo.processInfo.environment["__XCODE_BUILT_PRODUCTS_DIR_PATHS"] { - return true - } - - return false + ProcessInfo.processInfo.environment["__XCODE_BUILT_PRODUCTS_DIR_PATHS"] != nil } diff --git a/macos/Sources/Helpers/Backport.swift b/macos/Sources/Helpers/Backport.swift index 8c43652e4..28da6cce6 100644 --- a/macos/Sources/Helpers/Backport.swift +++ b/macos/Sources/Helpers/Backport.swift @@ -48,7 +48,7 @@ extension Backport where Content: View { 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) diff --git a/macos/Sources/Helpers/ExpiringUndoManager.swift b/macos/Sources/Helpers/ExpiringUndoManager.swift index 5fde0e870..3b1abd44a 100644 --- a/macos/Sources/Helpers/ExpiringUndoManager.swift +++ b/macos/Sources/Helpers/ExpiringUndoManager.swift @@ -98,10 +98,10 @@ class ExpiringUndoManager: UndoManager { private class ExpiringTarget { /// The actual target object for the undo operation, held weakly to avoid retain cycles. private(set) weak var target: AnyObject? - + /// Timer that triggers expiration after the specified duration. private var timer: Timer? - + /// The undo manager from which to remove actions when this target expires. private weak var undoManager: UndoManager? @@ -141,7 +141,7 @@ extension ExpiringTarget: Hashable, Equatable { static func == (lhs: ExpiringTarget, rhs: ExpiringTarget) -> Bool { return lhs === rhs } - + func hash(into hasher: inout Hasher) { hasher.combine(ObjectIdentifier(self)) } diff --git a/macos/Sources/Helpers/Extensions/Array+Extension.swift b/macos/Sources/Helpers/Extensions/Array+Extension.swift index 4e8e39918..92beb0505 100644 --- a/macos/Sources/Helpers/Extensions/Array+Extension.swift +++ b/macos/Sources/Helpers/Extensions/Array+Extension.swift @@ -2,7 +2,7 @@ extension Array { subscript(safe index: Int) -> Element? { return indices.contains(index) ? self[index] : nil } - + /// Returns the index before i, with wraparound. Assumes i is a valid index. func indexWrapping(before i: Int) -> Int { if i == 0 { @@ -35,7 +35,7 @@ extension Array where Element == String { if index == count { return try body(accumulated) } - + return try self[index].withCString { cStr in var newAccumulated = accumulated newAccumulated.append(cStr) diff --git a/macos/Sources/Helpers/Extensions/EventModifiers+Extension.swift b/macos/Sources/Helpers/Extensions/EventModifiers+Extension.swift index 8d379bd99..cc8d49cf8 100644 --- a/macos/Sources/Helpers/Extensions/EventModifiers+Extension.swift +++ b/macos/Sources/Helpers/Extensions/EventModifiers+Extension.swift @@ -5,11 +5,13 @@ import SwiftUI extension EventModifiers { init(nsFlags: NSEvent.ModifierFlags) { var result: SwiftUI.EventModifiers = [] + // swiftlint:disable opening_brace if nsFlags.contains(.shift) { result.insert(.shift) } if nsFlags.contains(.control) { result.insert(.control) } if nsFlags.contains(.option) { result.insert(.option) } if nsFlags.contains(.command) { result.insert(.command) } if nsFlags.contains(.capsLock) { result.insert(.capsLock) } + // swiftlint:enable opening_brace self = result } } @@ -17,11 +19,13 @@ extension EventModifiers { extension NSEvent.ModifierFlags { init(swiftUIFlags: SwiftUI.EventModifiers) { var result: NSEvent.ModifierFlags = [] + // swiftlint:disable opening_brace if swiftUIFlags.contains(.shift) { result.insert(.shift) } if swiftUIFlags.contains(.control) { result.insert(.control) } if swiftUIFlags.contains(.option) { result.insert(.option) } if swiftUIFlags.contains(.command) { result.insert(.command) } if swiftUIFlags.contains(.capsLock) { result.insert(.capsLock) } + // swiftlint:enable opening_brace self = result } } diff --git a/macos/Sources/Helpers/Extensions/NSAppearance+Extension.swift b/macos/Sources/Helpers/Extensions/NSAppearance+Extension.swift index 28edb1a35..c45f37a62 100644 --- a/macos/Sources/Helpers/Extensions/NSAppearance+Extension.swift +++ b/macos/Sources/Helpers/Extensions/NSAppearance+Extension.swift @@ -9,7 +9,7 @@ extension NSAppearance { /// Initialize a desired NSAppearance for the Ghostty configuration. convenience init?(ghosttyConfig config: Ghostty.Config) { guard let theme = config.windowTheme else { return nil } - switch (theme) { + switch theme { case "dark": self.init(named: .darkAqua) diff --git a/macos/Sources/Helpers/Extensions/NSApplication+Extension.swift b/macos/Sources/Helpers/Extensions/NSApplication+Extension.swift index 0bc79fb6a..2d3bc2cba 100644 --- a/macos/Sources/Helpers/Extensions/NSApplication+Extension.swift +++ b/macos/Sources/Helpers/Extensions/NSApplication+Extension.swift @@ -18,7 +18,7 @@ extension NSApplication { func releasePresentationOption(_ option: NSApplication.PresentationOptions.Element) { guard let value = Self.presentationOptionCounts[option] else { return } guard value > 0 else { return } - if (value == 1) { + if value == 1 { presentationOptions.remove(option) Self.presentationOptionCounts.removeValue(forKey: option) } else { diff --git a/macos/Sources/Helpers/Extensions/NSPasteboard+Extension.swift b/macos/Sources/Helpers/Extensions/NSPasteboard+Extension.swift index a036f02b4..a54735fde 100644 --- a/macos/Sources/Helpers/Extensions/NSPasteboard+Extension.swift +++ b/macos/Sources/Helpers/Extensions/NSPasteboard+Extension.swift @@ -13,14 +13,14 @@ extension NSPasteboard.PasteboardType { default: break } - + // Try to get UTType from MIME type guard let utType = UTType(mimeType: mimeType) else { // Fallback: use the MIME type directly as identifier self.init(mimeType) return } - + // Use the UTType's identifier self.init(utType.identifier) } @@ -50,7 +50,7 @@ extension NSPasteboard { /// The pasteboard for the Ghostty enum type. static func ghostty(_ clipboard: ghostty_clipboard_e) -> NSPasteboard? { - switch (clipboard) { + switch clipboard { case GHOSTTY_CLIPBOARD_STANDARD: return Self.general diff --git a/macos/Sources/Helpers/Extensions/NSScreen+Extension.swift b/macos/Sources/Helpers/Extensions/NSScreen+Extension.swift index a8eb7b876..ca338f102 100644 --- a/macos/Sources/Helpers/Extensions/NSScreen+Extension.swift +++ b/macos/Sources/Helpers/Extensions/NSScreen+Extension.swift @@ -5,7 +5,7 @@ extension NSScreen { var displayID: UInt32? { deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? UInt32 } - + /// The stable UUID for this display, suitable for tracking across reconnects and NSScreen garbage collection. var displayUUID: UUID? { guard let displayID = displayID else { return nil } @@ -19,7 +19,7 @@ extension NSScreen { var hasDock: Bool { // If the dock autohides then we don't have a dock ever. if let dockAutohide = UserDefaults.standard.persistentDomain(forName: "com.apple.dock")?["autohide"] as? Bool { - if (dockAutohide) { return false } + if dockAutohide { return false } } // There is no public API to directly ask about dock visibility, so we have to figure it out @@ -29,7 +29,7 @@ extension NSScreen { // which triggers showing the dock. // If our visible width is less than the frame we assume its the dock. - if (visibleFrame.width < frame.width) { + if visibleFrame.width < frame.width { return true } @@ -48,7 +48,7 @@ extension NSScreen { // know any other situation this is true. return safeAreaInsets.top > 0 } - + /// Converts top-left offset coordinates to bottom-left origin coordinates for window positioning. /// - Parameters: /// - x: X offset from top-left corner @@ -57,11 +57,11 @@ extension NSScreen { /// - Returns: CGPoint suitable for setFrameOrigin that positions the window as requested func origin(fromTopLeftOffsetX x: CGFloat, offsetY y: CGFloat, windowSize: CGSize) -> CGPoint { let vf = visibleFrame - + // Convert top-left coordinates to bottom-left origin let originX = vf.minX + x let originY = vf.maxY - y - windowSize.height - + return CGPoint(x: originX, y: originY) } } diff --git a/macos/Sources/Helpers/Extensions/NSView+Extension.swift b/macos/Sources/Helpers/Extensions/NSView+Extension.swift index fb209e4ac..030de0d1d 100644 --- a/macos/Sources/Helpers/Extensions/NSView+Extension.swift +++ b/macos/Sources/Helpers/Extensions/NSView+Extension.swift @@ -131,12 +131,12 @@ extension NSView { /// This includes private views like title bar views. func firstViewFromRoot(withClassName name: String) -> NSView? { let root = rootView - + // Check if the root view itself matches if String(describing: type(of: root)) == name { return root } - + // Otherwise search descendants return root.firstDescendant(withClassName: name) } @@ -155,67 +155,67 @@ extension NSView { print("View Hierarchy from Root:") print(root.viewHierarchyDescription()) } - + /// Returns a string representation of the view hierarchy in a tree-like format. func viewHierarchyDescription(indent: String = "", isLast: Bool = true) -> String { var result = "" - + // Add the tree branch characters result += indent if !indent.isEmpty { result += isLast ? "└── " : "├── " } - + // Add the class name and optional identifier let className = String(describing: type(of: self)) result += className - + // Add identifier if present if let identifier = self.identifier { result += " (id: \(identifier.rawValue))" } - + // Add frame info result += " [frame: \(frame)]" - + // Add visual properties var properties: [String] = [] - + // Hidden status if isHidden { properties.append("hidden") } - + // Opaque status properties.append(isOpaque ? "opaque" : "transparent") - + // Layer backing if wantsLayer { properties.append("layer-backed") if let bgColor = layer?.backgroundColor { let color = NSColor(cgColor: bgColor) if let rgb = color?.usingColorSpace(.deviceRGB) { - properties.append(String(format: "bg:rgba(%.0f,%.0f,%.0f,%.2f)", - rgb.redComponent * 255, - rgb.greenComponent * 255, - rgb.blueComponent * 255, + properties.append(String(format: "bg:rgba(%.0f,%.0f,%.0f,%.2f)", + rgb.redComponent * 255, + rgb.greenComponent * 255, + rgb.blueComponent * 255, rgb.alphaComponent)) } else { properties.append("bg:\(bgColor)") } } } - + result += " [\(properties.joined(separator: ", "))]" result += "\n" - + // Process subviews for (index, subview) in subviews.enumerated() { let isLastSubview = index == subviews.count - 1 let newIndent = indent + (isLast ? " " : "│ ") result += subview.viewHierarchyDescription(indent: newIndent, isLast: isLastSubview) } - + return result } } diff --git a/macos/Sources/Helpers/Extensions/NSWindow+Extension.swift b/macos/Sources/Helpers/Extensions/NSWindow+Extension.swift index 5d1831f26..0fa330f1b 100644 --- a/macos/Sources/Helpers/Extensions/NSWindow+Extension.swift +++ b/macos/Sources/Helpers/Extensions/NSWindow+Extension.swift @@ -52,31 +52,31 @@ extension NSWindow { guard themeFrameView.responds(to: Selector(("titlebarView"))) else { return nil } return themeFrameView.value(forKey: "titlebarView") as? NSView } - + /// Returns the [private] NSTabBar view, if it exists. var tabBarView: NSView? { titlebarView?.firstDescendant(withClassName: "NSTabBar") } - + /// Returns the index of the tab button at the given screen point, if any. func tabIndex(atScreenPoint screenPoint: NSPoint) -> Int? { guard let tabBarView else { return nil } let locationInWindow = convertPoint(fromScreen: screenPoint) let locationInTabBar = tabBarView.convert(locationInWindow, from: nil) guard tabBarView.bounds.contains(locationInTabBar) else { return nil } - + // Find all tab buttons and sort by x position to get visual order. // The view hierarchy order doesn't match the visual tab order. let tabItemViews = tabBarView.descendants(withClassName: "NSTabButton") .sorted { $0.frame.origin.x < $1.frame.origin.x } - + for (index, tabItemView) in tabItemViews.enumerated() { let locationInTab = tabItemView.convert(locationInWindow, from: nil) if tabItemView.bounds.contains(locationInTab) { return index } } - + return nil } } diff --git a/macos/Sources/Helpers/Extensions/NSWorkspace+Extension.swift b/macos/Sources/Helpers/Extensions/NSWorkspace+Extension.swift index e87e0676c..c4f7ca5c1 100644 --- a/macos/Sources/Helpers/Extensions/NSWorkspace+Extension.swift +++ b/macos/Sources/Helpers/Extensions/NSWorkspace+Extension.swift @@ -24,7 +24,7 @@ extension NSWorkspace { nil )?.takeRetainedValue() as? URL } - + /// Returns the URL of the default application for opening files with the specified file extension. /// - Parameter ext: The file extension to find the default application for. /// - Returns: The URL of the default application, or nil if no default application is found. diff --git a/macos/Sources/Helpers/Extensions/String+Extension.swift b/macos/Sources/Helpers/Extensions/String+Extension.swift index 03f715fd8..e28877ca8 100644 --- a/macos/Sources/Helpers/Extensions/String+Extension.swift +++ b/macos/Sources/Helpers/Extensions/String+Extension.swift @@ -27,5 +27,4 @@ extension String { } #endif - } diff --git a/macos/Sources/Helpers/Extensions/Transferable+Extension.swift b/macos/Sources/Helpers/Extensions/Transferable+Extension.swift index 3bcc9057f..a45cdc7a4 100644 --- a/macos/Sources/Helpers/Extensions/Transferable+Extension.swift +++ b/macos/Sources/Helpers/Extensions/Transferable+Extension.swift @@ -40,16 +40,16 @@ private final class TransferableDataProvider: NSObject, NSPasteboardItemDataProv // to block until the async load completes. This is safe because AppKit // calls this method on a background thread during drag operations. let semaphore = DispatchSemaphore(value: 0) - + var result: Data? itemProvider.loadDataRepresentation(forTypeIdentifier: type.rawValue) { data, _ in result = data semaphore.signal() } - + // Wait for the data to load semaphore.wait() - + // Set it. I honestly don't know what happens here if this fails. if let data = result { item.setData(data, forType: type) diff --git a/macos/Sources/Helpers/Fullscreen.swift b/macos/Sources/Helpers/Fullscreen.swift index 8ab476267..6773b6f0c 100644 --- a/macos/Sources/Helpers/Fullscreen.swift +++ b/macos/Sources/Helpers/Fullscreen.swift @@ -204,12 +204,12 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle { // We must hide the dock FIRST then hide the menu: // If you specify autoHideMenuBar, it must be accompanied by either hideDock or autoHideDock. // https://developer.apple.com/documentation/appkit/nsapplication/presentationoptions-swift.struct - if (savedState.dock) { + if savedState.dock { hideDock() } // Hide the menu if requested - if (properties.hideMenu && savedState.menu) { + if properties.hideMenu && savedState.menu { hideMenu() } @@ -261,7 +261,7 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle { if savedState.dock { unhideDock() } - if (properties.hideMenu && savedState.menu) { + if properties.hideMenu && savedState.menu { unhideMenu() } @@ -328,8 +328,8 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle { // calculate this ourselves. var frame = screen.frame - if (!NSApp.presentationOptions.contains(.autoHideMenuBar) && - !NSApp.presentationOptions.contains(.hideMenuBar)) { + if !NSApp.presentationOptions.contains(.autoHideMenuBar) && + !NSApp.presentationOptions.contains(.hideMenuBar) { // We need to subtract the menu height since we're still showing it. frame.size.height -= NSApp.mainMenu?.menuBarHeight ?? 0 @@ -339,7 +339,7 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle { // put an #available check, but it was in a bug fix release so I think // if a bug is reported to Ghostty we can just advise the user to // update. - } else if (properties.paddedNotch) { + } else if properties.paddedNotch { // We are hiding the menu, we may need to avoid the notch. frame.size.height -= screen.safeAreaInsets.top } @@ -413,7 +413,7 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle { self.toolbarStyle = window.toolbarStyle self.dock = window.screen?.hasDock ?? false - self.titlebarAccessoryViewControllers = if (window.hasTitleBar) { + self.titlebarAccessoryViewControllers = if window.hasTitleBar { // Accessing titlebarAccessoryViewControllers without a titlebar triggers a crash. window.titlebarAccessoryViewControllers } else { diff --git a/macos/Sources/Helpers/MetalView.swift b/macos/Sources/Helpers/MetalView.swift index 6579f8863..e8c27b52b 100644 --- a/macos/Sources/Helpers/MetalView.swift +++ b/macos/Sources/Helpers/MetalView.swift @@ -10,7 +10,7 @@ struct MetalView: View { } } -fileprivate struct MetalViewRepresentable: NSViewRepresentable { +private struct MetalViewRepresentable: NSViewRepresentable { @Binding var metalView: V func makeNSView(context: Context) -> some NSView { diff --git a/macos/Sources/Helpers/PermissionRequest.swift b/macos/Sources/Helpers/PermissionRequest.swift index 9c16c7163..29d1ab6d3 100644 --- a/macos/Sources/Helpers/PermissionRequest.swift +++ b/macos/Sources/Helpers/PermissionRequest.swift @@ -40,7 +40,7 @@ class PermissionRequest { completion(storedResult) return } - + let alert = NSAlert() alert.messageText = message alert.informativeText = informative @@ -59,7 +59,7 @@ class PermissionRequest { target: nil, action: nil) checkbox!.state = .off - + // Set checkbox as accessory view alert.accessoryView = checkbox } @@ -74,7 +74,7 @@ class PermissionRequest { handleResponse(response, rememberDecision: checkbox?.state == .on, key: key, allowDuration: allowDuration, rememberDuration: rememberDuration, completion: completion) } } - + /// Handles the alert response and processes caching logic /// - Parameters: /// - response: The alert response from the user @@ -90,7 +90,7 @@ class PermissionRequest { allowDuration: AllowDuration, rememberDuration: Duration?, completion: @escaping (Bool) -> Void) { - + let result: Bool switch response { case .alertFirstButtonReturn: // Allow @@ -100,7 +100,7 @@ class PermissionRequest { default: result = false } - + // Store the result if checkbox is checked or if "Allow" was selected and allowDuration is set if rememberDecision, let rememberDuration = rememberDuration { storeResult(result, for: key, duration: rememberDuration) @@ -118,10 +118,10 @@ class PermissionRequest { storeResult(result, for: key, duration: duration) } } - + completion(result) } - + /// Retrieves a cached permission decision if it hasn't expired /// - Parameter key: The UserDefaults key to check /// - Returns: The cached decision, or nil if no valid cached decision exists @@ -132,16 +132,16 @@ class PermissionRequest { ofClass: StoredPermission.self, from: data) else { return nil } - + if Date() > storedPermission.expiry { // Decision has expired, remove stored value userDefaults.removeObject(forKey: key) return nil } - + return storedPermission.result } - + /// Stores a permission decision in UserDefaults with an expiration date /// - Parameters: /// - result: The permission decision to store @@ -180,7 +180,7 @@ class PermissionRequest { return "Remember my decision for \(days) day\(days == 1 ? "" : "s")" } } - + /// Internal class for storing permission decisions with expiration dates in UserDefaults /// Conforms to NSSecureCoding for safe archiving/unarchiving @objc(StoredPermission) diff --git a/macos/Tests/NSPasteboardTests.swift b/macos/Tests/NSPasteboardTests.swift index d956ce733..9db17ca33 100644 --- a/macos/Tests/NSPasteboardTests.swift +++ b/macos/Tests/NSPasteboardTests.swift @@ -16,14 +16,14 @@ struct NSPasteboardTypeExtensionTests { #expect(pasteboardType != nil) #expect(pasteboardType == .string) } - + /// Test text/html MIME type converts to .html @Test func testTextHtmlMimeType() async throws { let pasteboardType = NSPasteboard.PasteboardType(mimeType: "text/html") #expect(pasteboardType != nil) #expect(pasteboardType == .html) } - + /// Test image/png MIME type @Test func testImagePngMimeType() async throws { let pasteboardType = NSPasteboard.PasteboardType(mimeType: "image/png") diff --git a/macos/Tests/NSScreenTests.swift b/macos/Tests/NSScreenTests.swift index f7431bf05..6e67bb7e4 100644 --- a/macos/Tests/NSScreenTests.swift +++ b/macos/Tests/NSScreenTests.swift @@ -15,65 +15,65 @@ struct NSScreenExtensionTests { // Mock screen with 1000x800 visible frame starting at (0, 100) let mockScreenFrame = NSRect(x: 0, y: 100, width: 1000, height: 800) let mockScreen = MockNSScreen(visibleFrame: mockScreenFrame) - + // Mock window size let windowSize = CGSize(width: 400, height: 300) - + // Test top-left positioning: x=15, y=15 let origin = mockScreen.origin( fromTopLeftOffsetX: 15, offsetY: 15, windowSize: windowSize) - + // Expected: x = 0 + 15 = 15, y = (100 + 800) - 15 - 300 = 585 #expect(origin.x == 15) #expect(origin.y == 585) } - + /// Test zero coordinates (exact top-left corner) @Test func testZeroCoordinates() async throws { let mockScreenFrame = NSRect(x: 0, y: 100, width: 1000, height: 800) let mockScreen = MockNSScreen(visibleFrame: mockScreenFrame) let windowSize = CGSize(width: 400, height: 300) - + let origin = mockScreen.origin( fromTopLeftOffsetX: 0, offsetY: 0, windowSize: windowSize) - + // Expected: x = 0, y = (100 + 800) - 0 - 300 = 600 #expect(origin.x == 0) #expect(origin.y == 600) } - + /// Test with offset screen (not starting at origin) @Test func testOffsetScreen() async throws { // Secondary monitor at position (1440, 0) with 1920x1080 resolution let mockScreenFrame = NSRect(x: 1440, y: 0, width: 1920, height: 1080) let mockScreen = MockNSScreen(visibleFrame: mockScreenFrame) let windowSize = CGSize(width: 600, height: 400) - + let origin = mockScreen.origin( fromTopLeftOffsetX: 100, offsetY: 50, windowSize: windowSize) - + // Expected: x = 1440 + 100 = 1540, y = (0 + 1080) - 50 - 400 = 630 #expect(origin.x == 1540) #expect(origin.y == 630) } - + /// Test large coordinates @Test func testLargeCoordinates() async throws { let mockScreenFrame = NSRect(x: 0, y: 0, width: 1920, height: 1080) let mockScreen = MockNSScreen(visibleFrame: mockScreenFrame) let windowSize = CGSize(width: 400, height: 300) - + let origin = mockScreen.origin( fromTopLeftOffsetX: 500, offsetY: 200, windowSize: windowSize) - + // Expected: x = 0 + 500 = 500, y = (0 + 1080) - 200 - 300 = 580 #expect(origin.x == 500) #expect(origin.y == 580) @@ -83,16 +83,16 @@ struct NSScreenExtensionTests { /// Mock NSScreen class for testing coordinate conversion private class MockNSScreen: NSScreen { private let mockVisibleFrame: NSRect - + init(visibleFrame: NSRect) { self.mockVisibleFrame = visibleFrame super.init() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + override var visibleFrame: NSRect { return mockVisibleFrame } diff --git a/macos/Tests/Update/ReleaseNotesTests.swift b/macos/Tests/Update/ReleaseNotesTests.swift index b029fa6bc..6c7d43ed5 100644 --- a/macos/Tests/Update/ReleaseNotesTests.swift +++ b/macos/Tests/Update/ReleaseNotesTests.swift @@ -9,7 +9,7 @@ struct ReleaseNotesTests { displayVersionString: "1.2.3", currentCommit: nil ) - + #expect(notes != nil) if case .tagged(let url) = notes { #expect(url.absoluteString == "https://ghostty.org/docs/install/release-notes/1-2-3") @@ -18,14 +18,14 @@ struct ReleaseNotesTests { Issue.record("Expected tagged case") } } - + /// Test tip release comparison with current commit @Test func testTipReleaseComparison() async throws { let notes = UpdateState.ReleaseNotes( displayVersionString: "tip-abc1234", currentCommit: "def5678" ) - + #expect(notes != nil) if case .compareTip(let url) = notes { #expect(url.absoluteString == "https://github.com/ghostty-org/ghostty/compare/def5678...abc1234") @@ -34,14 +34,14 @@ struct ReleaseNotesTests { Issue.record("Expected compareTip case") } } - + /// Test tip release without current commit @Test func testTipReleaseWithoutCurrentCommit() async throws { let notes = UpdateState.ReleaseNotes( displayVersionString: "tip-abc1234", currentCommit: nil ) - + #expect(notes != nil) if case .commit(let url) = notes { #expect(url.absoluteString == "https://github.com/ghostty-org/ghostty/commit/abc1234") @@ -50,14 +50,14 @@ struct ReleaseNotesTests { Issue.record("Expected commit case") } } - + /// Test tip release with empty current commit @Test func testTipReleaseWithEmptyCurrentCommit() async throws { let notes = UpdateState.ReleaseNotes( displayVersionString: "tip-abc1234", currentCommit: "" ) - + #expect(notes != nil) if case .commit(let url) = notes { #expect(url.absoluteString == "https://github.com/ghostty-org/ghostty/commit/abc1234") @@ -65,14 +65,14 @@ struct ReleaseNotesTests { Issue.record("Expected commit case") } } - + /// Test version with full 40-character hash @Test func testFullGitHash() async throws { let notes = UpdateState.ReleaseNotes( displayVersionString: "tip-1234567890abcdef1234567890abcdef12345678", currentCommit: nil ) - + #expect(notes != nil) if case .commit(let url) = notes { #expect(url.absoluteString == "https://github.com/ghostty-org/ghostty/commit/1234567890abcdef1234567890abcdef12345678") @@ -80,46 +80,46 @@ struct ReleaseNotesTests { Issue.record("Expected commit case") } } - + /// Test version with no recognizable pattern @Test func testInvalidVersion() async throws { let notes = UpdateState.ReleaseNotes( displayVersionString: "unknown-version", currentCommit: nil ) - + #expect(notes == nil) } - + /// Test semantic version with prerelease suffix should not match @Test func testSemanticVersionWithSuffix() async throws { let notes = UpdateState.ReleaseNotes( displayVersionString: "1.2.3-beta", currentCommit: nil ) - + // Should not match semantic version pattern, falls back to hash detection #expect(notes == nil) } - + /// Test semantic version with 4 components should not match @Test func testSemanticVersionFourComponents() async throws { let notes = UpdateState.ReleaseNotes( displayVersionString: "1.2.3.4", currentCommit: nil ) - + // Should not match pattern #expect(notes == nil) } - + /// Test version string with git hash embedded @Test func testVersionWithEmbeddedHash() async throws { let notes = UpdateState.ReleaseNotes( displayVersionString: "v2024.01.15-abc1234", currentCommit: "def5678" ) - + #expect(notes != nil) if case .compareTip(let url) = notes { #expect(url.absoluteString == "https://github.com/ghostty-org/ghostty/compare/def5678...abc1234") diff --git a/macos/Tests/Update/UpdateStateTests.swift b/macos/Tests/Update/UpdateStateTests.swift index 354d371c5..6aefa22a2 100644 --- a/macos/Tests/Update/UpdateStateTests.swift +++ b/macos/Tests/Update/UpdateStateTests.swift @@ -5,25 +5,25 @@ import Sparkle struct UpdateStateTests { // MARK: - Equatable Tests - + @Test func testIdleEquality() { let state1: UpdateState = .idle let state2: UpdateState = .idle #expect(state1 == state2) } - + @Test func testCheckingEquality() { let state1: UpdateState = .checking(.init(cancel: {})) let state2: UpdateState = .checking(.init(cancel: {})) #expect(state1 == state2) } - + @Test func testNotFoundEquality() { let state1: UpdateState = .notFound(.init(acknowledgement: {})) let state2: UpdateState = .notFound(.init(acknowledgement: {})) #expect(state1 == state2) } - + @Test func testInstallingEquality() { let state1: UpdateState = .installing(.init(isAutoUpdate: false, retryTerminatingApplication: {}, dismiss: {})) let state2: UpdateState = .installing(.init(isAutoUpdate: false, retryTerminatingApplication: {}, dismiss: {})) @@ -31,7 +31,7 @@ struct UpdateStateTests { let state3: UpdateState = .installing(.init(isAutoUpdate: true, retryTerminatingApplication: {}, dismiss: {})) #expect(state3 != state2) } - + @Test func testPermissionRequestEquality() { let request1 = SPUUpdatePermissionRequest(systemProfile: []) let request2 = SPUUpdatePermissionRequest(systemProfile: []) @@ -39,43 +39,43 @@ struct UpdateStateTests { let state2: UpdateState = .permissionRequest(.init(request: request2, reply: { _ in })) #expect(state1 == state2) } - + @Test func testDownloadingEqualityWithSameProgress() { let state1: UpdateState = .downloading(.init(cancel: {}, expectedLength: 1000, progress: 500)) let state2: UpdateState = .downloading(.init(cancel: {}, expectedLength: 1000, progress: 500)) #expect(state1 == state2) } - + @Test func testDownloadingInequalityWithDifferentProgress() { let state1: UpdateState = .downloading(.init(cancel: {}, expectedLength: 1000, progress: 500)) let state2: UpdateState = .downloading(.init(cancel: {}, expectedLength: 1000, progress: 600)) #expect(state1 != state2) } - + @Test func testDownloadingInequalityWithDifferentExpectedLength() { let state1: UpdateState = .downloading(.init(cancel: {}, expectedLength: 1000, progress: 500)) let state2: UpdateState = .downloading(.init(cancel: {}, expectedLength: 2000, progress: 500)) #expect(state1 != state2) } - + @Test func testDownloadingEqualityWithNilExpectedLength() { let state1: UpdateState = .downloading(.init(cancel: {}, expectedLength: nil, progress: 500)) let state2: UpdateState = .downloading(.init(cancel: {}, expectedLength: nil, progress: 500)) #expect(state1 == state2) } - + @Test func testExtractingEqualityWithSameProgress() { let state1: UpdateState = .extracting(.init(progress: 0.5)) let state2: UpdateState = .extracting(.init(progress: 0.5)) #expect(state1 == state2) } - + @Test func testExtractingInequalityWithDifferentProgress() { let state1: UpdateState = .extracting(.init(progress: 0.5)) let state2: UpdateState = .extracting(.init(progress: 0.6)) #expect(state1 != state2) } - + @Test func testErrorEqualityWithSameDescription() { let error1 = NSError(domain: "Test", code: 1, userInfo: [NSLocalizedDescriptionKey: "Error message"]) let error2 = NSError(domain: "Test", code: 2, userInfo: [NSLocalizedDescriptionKey: "Error message"]) @@ -83,7 +83,7 @@ struct UpdateStateTests { let state2: UpdateState = .error(.init(error: error2, retry: {}, dismiss: {})) #expect(state1 == state2) } - + @Test func testErrorInequalityWithDifferentDescription() { let error1 = NSError(domain: "Test", code: 1, userInfo: [NSLocalizedDescriptionKey: "Error 1"]) let error2 = NSError(domain: "Test", code: 1, userInfo: [NSLocalizedDescriptionKey: "Error 2"]) @@ -91,20 +91,20 @@ struct UpdateStateTests { let state2: UpdateState = .error(.init(error: error2, retry: {}, dismiss: {})) #expect(state1 != state2) } - + @Test func testDifferentStatesAreNotEqual() { let state1: UpdateState = .idle let state2: UpdateState = .checking(.init(cancel: {})) #expect(state1 != state2) } - + // MARK: - isIdle Tests - + @Test func testIsIdleTrue() { let state: UpdateState = .idle #expect(state.isIdle == true) } - + @Test func testIsIdleFalse() { let state: UpdateState = .checking(.init(cancel: {})) #expect(state.isIdle == false) diff --git a/macos/Tests/Update/UpdateViewModelTests.swift b/macos/Tests/Update/UpdateViewModelTests.swift index 529c2bc52..9b747f9ec 100644 --- a/macos/Tests/Update/UpdateViewModelTests.swift +++ b/macos/Tests/Update/UpdateViewModelTests.swift @@ -6,50 +6,50 @@ import Sparkle struct UpdateViewModelTests { // MARK: - Text Formatting Tests - + @Test func testIdleText() { let viewModel = UpdateViewModel() viewModel.state = .idle #expect(viewModel.text == "") } - + @Test func testPermissionRequestText() { let viewModel = UpdateViewModel() let request = SPUUpdatePermissionRequest(systemProfile: []) viewModel.state = .permissionRequest(.init(request: request, reply: { _ in })) #expect(viewModel.text == "Enable Automatic Updates?") } - + @Test func testCheckingText() { let viewModel = UpdateViewModel() viewModel.state = .checking(.init(cancel: {})) #expect(viewModel.text == "Checking for Updates…") } - + @Test func testDownloadingTextWithKnownLength() { let viewModel = UpdateViewModel() viewModel.state = .downloading(.init(cancel: {}, expectedLength: 1000, progress: 500)) #expect(viewModel.text == "Downloading: 50%") } - + @Test func testDownloadingTextWithUnknownLength() { let viewModel = UpdateViewModel() viewModel.state = .downloading(.init(cancel: {}, expectedLength: nil, progress: 500)) #expect(viewModel.text == "Downloading…") } - + @Test func testDownloadingTextWithZeroExpectedLength() { let viewModel = UpdateViewModel() viewModel.state = .downloading(.init(cancel: {}, expectedLength: 0, progress: 500)) #expect(viewModel.text == "Downloading…") } - + @Test func testExtractingText() { let viewModel = UpdateViewModel() viewModel.state = .extracting(.init(progress: 0.75)) #expect(viewModel.text == "Preparing: 75%") } - + @Test func testInstallingText() { let viewModel = UpdateViewModel() viewModel.state = .installing(.init(isAutoUpdate: false, retryTerminatingApplication: {}, dismiss: {})) @@ -57,34 +57,34 @@ struct UpdateViewModelTests { viewModel.state = .installing(.init(isAutoUpdate: true, retryTerminatingApplication: {}, dismiss: {})) #expect(viewModel.text == "Restart to Complete Update") } - + @Test func testNotFoundText() { let viewModel = UpdateViewModel() viewModel.state = .notFound(.init(acknowledgement: {})) #expect(viewModel.text == "No Updates Available") } - + @Test func testErrorText() { let viewModel = UpdateViewModel() let error = NSError(domain: "Test", code: 1, userInfo: [NSLocalizedDescriptionKey: "Network error"]) viewModel.state = .error(.init(error: error, retry: {}, dismiss: {})) #expect(viewModel.text == "Network error") } - + // MARK: - Max Width Text Tests - + @Test func testMaxWidthTextForDownloading() { let viewModel = UpdateViewModel() viewModel.state = .downloading(.init(cancel: {}, expectedLength: 1000, progress: 50)) #expect(viewModel.maxWidthText == "Downloading: 100%") } - + @Test func testMaxWidthTextForExtracting() { let viewModel = UpdateViewModel() viewModel.state = .extracting(.init(progress: 0.5)) #expect(viewModel.maxWidthText == "Preparing: 100%") } - + @Test func testMaxWidthTextForNonProgressState() { let viewModel = UpdateViewModel() viewModel.state = .checking(.init(cancel: {}))