macos: autofixable swiftlint rules (#10878)

Apply fixes for all of the SwiftLint rules that can be automatically
fixed (`--fix`) or addressed via some minor reformatting.

Each rule is in its own commit for easier review.
This commit is contained in:
Mitchell Hashimoto
2026-02-19 19:57:39 -08:00
committed by GitHub
94 changed files with 1215 additions and 1289 deletions

View File

@@ -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

View File

@@ -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
}
}

View File

@@ -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
// `<filename>; 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()
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -22,7 +22,7 @@ struct CloseTerminalIntent: AppIntent {
guard await requestIntentPermission() else {
throw GhosttyIntentError.permissionDenied
}
guard let surfaceView = terminal.surfaceView else {
throw GhosttyIntentError.surfaceNotFound
}

View File

@@ -29,7 +29,7 @@ struct CommandPaletteIntent: AppIntent {
guard await requestIntentPermission() else {
throw GhosttyIntentError.permissionDenied
}
guard let surface = terminal.surfaceModel else {
throw GhosttyIntentError.surfaceNotFound
}

View File

@@ -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
}

View File

@@ -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() ?? []

View File

@@ -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)

View File

@@ -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

View File

@@ -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?",

View File

@@ -26,7 +26,7 @@ struct KeybindIntent: AppIntent {
guard await requestIntentPermission() else {
throw GhosttyIntentError.permissionDenied
}
guard let surface = terminal.surfaceModel else {
throw GhosttyIntentError.surfaceNotFound
}

View File

@@ -152,7 +152,7 @@ enum NewTerminalLocation: String {
case splitRight = "split:right"
case splitUp = "split:up"
case splitDown = "split:down"
var splitDirection: SplitTree<Ghostty.SurfaceView>.NewDirection? {
switch self {
case .splitLeft: return .left

View File

@@ -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
}

View File

@@ -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:

View File

@@ -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()
}
}

View File

@@ -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"

View File

@@ -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<String>, isTextFieldFocused: FocusState<Bool>, 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 {

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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.

View File

@@ -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)

View File

@@ -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)

View File

@@ -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,

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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 {

View File

@@ -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:

View File

@@ -90,7 +90,7 @@ struct SplitView<L: View, R: View>: 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<L: View, R: View>: 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<L: View, R: View>: 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<L: View, R: View>: 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<L: View, R: View>: 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<L: View, R: View>: View {
return "Vertical split view"
}
}
private var leftPaneLabel: String {
switch direction {
case .horizontal:
@@ -172,7 +172,7 @@ struct SplitView<L: View, R: View>: View {
return "Top pane"
}
}
private var rightPaneLabel: String {
switch direction {
case .horizontal:

View File

@@ -7,19 +7,19 @@ import SwiftUI
enum TerminalSplitOperation {
case resize(Resize)
case drop(Drop)
struct Resize {
let node: SplitTree<Ghostty.SurfaceView>.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<Ghostty.SurfaceView>.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
}
}

View File

@@ -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<Ghostty.SurfaceView>, to: SplitTree<Ghostty.SurfaceView>) {
// 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<Ghostty.SurfaceView>.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<Ghostty.SurfaceView>.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<Ghostty.SurfaceView>.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<Ghostty.SurfaceView>(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<Ghostty.SurfaceView>.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.

View File

@@ -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<AnyCancellable> = []
/// 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<Ghostty.SurfaceView>? = 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<Ghostty.SurfaceView>, to: SplitTree<Ghostty.SurfaceView>) {
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<Ghostty.SurfaceView>,
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)
}

View File

@@ -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)
}
}
}
}

View File

@@ -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],

View File

@@ -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<ViewModel: TerminalViewModel>: 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<Ghostty.SurfaceView> = .init()
@@ -76,7 +76,7 @@ struct TerminalView<ViewModel: TerminalViewModel>: 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<ViewModel: TerminalViewModel>: View {
self.delegate?.performAction(action, on: surfaceView)
}
}
// Show update information above all else.
if viewModel.updateOverlayIsVisible {
UpdateOverlay()
@@ -127,12 +127,12 @@ struct TerminalView<ViewModel: TerminalViewModel>: 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)

View File

@@ -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])

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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))

View File

@@ -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

View File

@@ -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"
}

View File

@@ -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

View File

@@ -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<Void, Never>?
/// 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]

View File

@@ -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()

View File

@@ -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,

View File

@@ -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

View File

@@ -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)

View File

@@ -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..<len).compactMap { i in
Ghostty.ClipboardContent.from(content: content[i])
}
guard !contentArray.isEmpty else { return }
// Assert there is only one text/plain entry. For security reasons we need
// to guarantee this for now since our confirmation dialog only shows one.
assert(contentArray.filter({ $0.mime == "text/plain" }).count <= 1,
"clipboard contents should have at most one text/plain entry")
if !confirm {
// Declare all types
let types = contentArray.compactMap { item in
NSPasteboard.PasteboardType(mimeType: item.mime)
}
pasteboard.declareTypes(types, owner: nil)
// Set data for each type
for item in contentArray {
guard let type = NSPasteboard.PasteboardType(mimeType: item.mime) else { continue }
@@ -410,7 +409,7 @@ extension Ghostty {
guard let textPlainContent = contentArray.first(where: { $0.mime == "text/plain" }) else {
return
}
NotificationCenter.default.post(
name: Notification.confirmClipboard,
object: surface,
@@ -463,7 +462,7 @@ extension Ghostty {
static func action(_ app: ghostty_app_t, target: ghostty_target_s, action: ghostty_action_s) -> 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<App>.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)

View File

@@ -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..<diagsCount {
let diag = ghostty_config_get_diagnostic(cfg, UInt32(i))
@@ -73,10 +73,10 @@ extension Ghostty {
// We only load CLI args when not running in Xcode because in Xcode we
// pass some special parameters to control the debugger.
if !isRunningInXcode() {
ghostty_config_load_cli_args(cfg);
ghostty_config_load_cli_args(cfg)
}
ghostty_config_load_recursive_files(cfg);
ghostty_config_load_recursive_files(cfg)
#endif
// TODO: we'd probably do some config loading here... for now we'd
@@ -92,7 +92,7 @@ extension Ghostty {
let diagsCount = ghostty_config_diagnostics_count(cfg)
if diagsCount > 0 {
logger.warning("config error: \(diagsCount) configuration errors on reload")
var diags: [String] = [];
var diags: [String] = []
for i in 0..<diagsCount {
let diag = ghostty_config_get_diagnostic(cfg, UInt32(i))
let message = String(cString: diag.message)
@@ -144,7 +144,7 @@ extension Ghostty {
var initialWindow: Bool {
guard let config = self.config else { return true }
var v = true;
var v = true
let key = "initial-window"
_ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8)))
return v
@@ -152,7 +152,7 @@ extension Ghostty {
var shouldQuitAfterLastWindowClosed: Bool {
guard let config = self.config else { return true }
var v = false;
var v = false
let key = "quit-after-last-window-closed"
_ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8)))
return v
@@ -160,7 +160,7 @@ extension Ghostty {
var title: String? {
guard let config = self.config else { return nil }
var v: UnsafePointer<Int8>? = nil
var v: UnsafePointer<Int8>?
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<Int8>? = nil
var v: UnsafePointer<Int8>?
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<Int8>? = nil
var v: UnsafePointer<Int8>?
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<Int8>? = nil
var v: UnsafePointer<Int8>?
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<Int8>? = nil
var v: UnsafePointer<Int8>?
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<Int8>? = nil
var v: UnsafePointer<Int8>?
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<Int8>? = nil
var v: UnsafePointer<Int8>?
let key = "fullscreen"
guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return false }
guard let ptr = v else { return false }
@@ -271,7 +271,7 @@ extension Ghostty {
var windowFullscreenMode: FullscreenMode {
let defaultValue: FullscreenMode = .native
guard let config = self.config else { return defaultValue }
var v: UnsafePointer<Int8>? = nil
var v: UnsafePointer<Int8>?
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<Int8>? = nil
var v: UnsafePointer<Int8>?
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<Int8>? = nil
var v: UnsafePointer<Int8>?
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<Int8>? = nil
var v: UnsafePointer<Int8>?
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<Int8>? = nil
var v: UnsafePointer<Int8>?
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<Int8>? = nil
var v: UnsafePointer<Int8>?
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<Int8>? = nil
var v: UnsafePointer<Int8>?
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<Int8>? = nil
var v: UnsafePointer<Int8>?
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<Int8>? = nil
var v: UnsafePointer<Int8>?
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<Int8>? = nil
var v: UnsafePointer<Int8>?
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<Int8>? = nil
var v: UnsafePointer<Int8>?
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<Int8>? = nil
var v: UnsafePointer<Int8>?
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<Int8>? = nil
var v: UnsafePointer<Int8>?
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<Int8>? = nil
var v: UnsafePointer<Int8>?
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<Int8>? = nil
var v: UnsafePointer<Int8>?
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<Int8>? = nil
var v: UnsafePointer<Int8>?
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<Int8>? = nil
var v: UnsafePointer<Int8>?
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<Int8>? = nil
var v: UnsafePointer<Int8>?
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<Int8>? = nil
var v: UnsafePointer<Int8>?
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
}
}
}

View File

@@ -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",

View File

@@ -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))
}

View File

@@ -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
}
}

View File

@@ -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 {}

View File

@@ -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)
}

View File

@@ -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"
}

View File

@@ -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)

View File

@@ -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 {
}
}

View File

@@ -19,12 +19,12 @@ class SurfaceScrollView: NSView {
private var observers: [NSObjectProtocol] = []
private var cancellables: Set<AnyCancellable> = []
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

View File

@@ -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)

View File

@@ -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<ghostty_env_var_s>()
var envVars = [ghostty_env_var_s]()
envVars.reserveCapacity(environmentVariables.count)
for i in 0..<environmentVariables.count {
envVars.append(ghostty_env_var_s(
@@ -764,24 +762,24 @@ extension Ghostty {
struct KeyStateIndicator: View {
let keyTables: [String]
let keySequence: [KeyboardShortcut]
@State private var isShowingPopover = false
@State private var position: Position = .bottom
@State private var dragOffset: CGSize = .zero
@State private var isDragging = false
private let padding: CGFloat = 8
enum Position {
case top, bottom
var alignment: Alignment {
switch self {
case .top: return .top
case .bottom: return .bottom
}
}
var popoverEdge: Edge {
switch self {
case .top: return .top
@@ -861,14 +859,14 @@ extension Ghostty {
Divider()
.frame(height: 14)
}
// Key sequence indicator
if !keySequence.isEmpty {
HStack(alignment: .center, spacing: 4) {
ForEach(Array(keySequence.enumerated()), id: \.offset) { index, key in
ForEach(Array(keySequence.enumerated()), id: \.offset) { _, key in
KeyCap(key.description)
}
// Animated ellipsis to indicate waiting for next key
PendingIndicator(paused: isDragging)
}
@@ -898,11 +896,11 @@ extension Ghostty {
.foregroundStyle(.secondary)
}
}
if !keyTables.isEmpty && !keySequence.isEmpty {
Divider()
}
if !keySequence.isEmpty {
VStack(alignment: .leading, spacing: 4) {
Label("Key Sequence", systemImage: "character.cursor.ibeam")
@@ -921,15 +919,15 @@ extension Ghostty {
isShowingPopover.toggle()
}
}
/// A small keycap-style view for displaying keyboard shortcuts
struct KeyCap: View {
let text: String
init(_ text: String) {
self.text = text
}
var body: some View {
Text(verbatim: text)
.font(.system(size: 12, weight: .medium, design: .rounded))
@@ -946,7 +944,7 @@ extension Ghostty {
)
}
}
/// Animated dots to indicate waiting for the next key
struct PendingIndicator: View {
@State private var animationPhase: Double = 0
@@ -967,7 +965,7 @@ extension Ghostty {
}
}
}
private func dotOpacity(for index: Int) -> 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 ?? ""

View File

@@ -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<String>
/// 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-<return> 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

View File

@@ -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() {

View File

@@ -4,7 +4,7 @@ import Foundation
struct AnySortKey: Comparable {
private let value: Any
private let comparator: (Any, Any) -> ComparisonResult
init<T: Comparable>(_ 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
}

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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))
}

View File

@@ -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)

View File

@@ -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
}
}

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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

View File

@@ -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)
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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.

View File

@@ -27,5 +27,4 @@ extension String {
}
#endif
}

View File

@@ -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)

View File

@@ -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 {

View File

@@ -10,7 +10,7 @@ struct MetalView<V: MTKView>: View {
}
}
fileprivate struct MetalViewRepresentable<V: MTKView>: NSViewRepresentable {
private struct MetalViewRepresentable<V: MTKView>: NSViewRepresentable {
@Binding var metalView: V
func makeNSView(context: Context) -> some NSView {

View File

@@ -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)

View File

@@ -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")

View File

@@ -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
}

View File

@@ -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")

View File

@@ -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)

View File

@@ -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: {}))