Merge pull request #2129 from pnodet/patch-4

style(macos): cleanup trailing spaces
This commit is contained in:
Mitchell Hashimoto
2024-08-22 13:48:01 -04:00
committed by GitHub
34 changed files with 782 additions and 783 deletions

View File

@ -3,7 +3,7 @@ import SwiftUI
@main @main
struct Ghostty_iOSApp: App { struct Ghostty_iOSApp: App {
@StateObject private var ghostty_app = Ghostty.App() @StateObject private var ghostty_app = Ghostty.App()
var body: some Scene { var body: some Scene {
WindowGroup { WindowGroup {
iOS_GhosttyTerminal() iOS_GhosttyTerminal()
@ -14,12 +14,12 @@ struct Ghostty_iOSApp: App {
struct iOS_GhosttyTerminal: View { struct iOS_GhosttyTerminal: View {
@EnvironmentObject private var ghostty_app: Ghostty.App @EnvironmentObject private var ghostty_app: Ghostty.App
var body: some View { var body: some View {
ZStack { ZStack {
// Make sure that our background color extends to all parts of the screen // Make sure that our background color extends to all parts of the screen
Color(ghostty_app.config.backgroundColor).ignoresSafeArea() Color(ghostty_app.config.backgroundColor).ignoresSafeArea()
Ghostty.Terminal() Ghostty.Terminal()
} }
} }
@ -27,7 +27,7 @@ struct iOS_GhosttyTerminal: View {
struct iOS_GhosttyInitView: View { struct iOS_GhosttyInitView: View {
@EnvironmentObject private var ghostty_app: Ghostty.App @EnvironmentObject private var ghostty_app: Ghostty.App
var body: some View { var body: some View {
VStack { VStack {
Image("AppIconImage") Image("AppIconImage")

View File

@ -4,10 +4,10 @@ import OSLog
import Sparkle import Sparkle
import GhosttyKit import GhosttyKit
class AppDelegate: NSObject, class AppDelegate: NSObject,
ObservableObject, ObservableObject,
NSApplicationDelegate, NSApplicationDelegate,
UNUserNotificationCenterDelegate, UNUserNotificationCenterDelegate,
GhosttyAppDelegate GhosttyAppDelegate
{ {
// The application logger. We should probably move this at some point to a dedicated // The application logger. We should probably move this at some point to a dedicated
@ -16,14 +16,14 @@ class AppDelegate: NSObject,
subsystem: Bundle.main.bundleIdentifier!, subsystem: Bundle.main.bundleIdentifier!,
category: String(describing: AppDelegate.self) category: String(describing: AppDelegate.self)
) )
/// Various menu items so that we can programmatically sync the keyboard shortcut with the Ghostty config /// Various menu items so that we can programmatically sync the keyboard shortcut with the Ghostty config
@IBOutlet private var menuServices: NSMenu? @IBOutlet private var menuServices: NSMenu?
@IBOutlet private var menuCheckForUpdates: NSMenuItem? @IBOutlet private var menuCheckForUpdates: NSMenuItem?
@IBOutlet private var menuOpenConfig: NSMenuItem? @IBOutlet private var menuOpenConfig: NSMenuItem?
@IBOutlet private var menuReloadConfig: NSMenuItem? @IBOutlet private var menuReloadConfig: NSMenuItem?
@IBOutlet private var menuQuit: NSMenuItem? @IBOutlet private var menuQuit: NSMenuItem?
@IBOutlet private var menuNewWindow: NSMenuItem? @IBOutlet private var menuNewWindow: NSMenuItem?
@IBOutlet private var menuNewTab: NSMenuItem? @IBOutlet private var menuNewTab: NSMenuItem?
@IBOutlet private var menuSplitRight: NSMenuItem? @IBOutlet private var menuSplitRight: NSMenuItem?
@ -31,7 +31,7 @@ class AppDelegate: NSObject,
@IBOutlet private var menuClose: NSMenuItem? @IBOutlet private var menuClose: NSMenuItem?
@IBOutlet private var menuCloseWindow: NSMenuItem? @IBOutlet private var menuCloseWindow: NSMenuItem?
@IBOutlet private var menuCloseAllWindows: NSMenuItem? @IBOutlet private var menuCloseAllWindows: NSMenuItem?
@IBOutlet private var menuCopy: NSMenuItem? @IBOutlet private var menuCopy: NSMenuItem?
@IBOutlet private var menuPaste: NSMenuItem? @IBOutlet private var menuPaste: NSMenuItem?
@IBOutlet private var menuSelectAll: NSMenuItem? @IBOutlet private var menuSelectAll: NSMenuItem?
@ -58,20 +58,20 @@ class AppDelegate: NSObject,
/// The dock menu /// The dock menu
private var dockMenu: NSMenu = NSMenu() private var dockMenu: NSMenu = NSMenu()
/// This is only true before application has become active. /// This is only true before application has become active.
private var applicationHasBecomeActive: Bool = false private var applicationHasBecomeActive: Bool = false
/// The ghostty global state. Only one per process. /// The ghostty global state. Only one per process.
let ghostty: Ghostty.App = Ghostty.App() let ghostty: Ghostty.App = Ghostty.App()
/// Manages our terminal windows. /// Manages our terminal windows.
let terminalManager: TerminalManager let terminalManager: TerminalManager
/// Manages updates /// Manages updates
let updaterController: SPUStandardUpdaterController let updaterController: SPUStandardUpdaterController
let updaterDelegate: UpdaterDelegate = UpdaterDelegate() let updaterDelegate: UpdaterDelegate = UpdaterDelegate()
override init() { override init() {
terminalManager = TerminalManager(ghostty) terminalManager = TerminalManager(ghostty)
updaterController = SPUStandardUpdaterController( updaterController = SPUStandardUpdaterController(
@ -81,12 +81,12 @@ class AppDelegate: NSObject,
) )
super.init() super.init()
ghostty.delegate = self ghostty.delegate = self
} }
//MARK: - NSApplicationDelegate //MARK: - NSApplicationDelegate
func applicationWillFinishLaunching(_ notification: Notification) { func applicationWillFinishLaunching(_ notification: Notification) {
UserDefaults.standard.register(defaults: [ UserDefaults.standard.register(defaults: [
// Disable the automatic full screen menu item because we handle // Disable the automatic full screen menu item because we handle
@ -94,24 +94,24 @@ class AppDelegate: NSObject,
"NSFullScreenMenuItemEverywhere": false, "NSFullScreenMenuItemEverywhere": false,
]) ])
} }
func applicationDidFinishLaunching(_ notification: Notification) { func applicationDidFinishLaunching(_ notification: Notification) {
// System settings overrides // System settings overrides
UserDefaults.standard.register(defaults: [ UserDefaults.standard.register(defaults: [
// Disable this so that repeated key events make it through to our terminal views. // Disable this so that repeated key events make it through to our terminal views.
"ApplePressAndHoldEnabled": false, "ApplePressAndHoldEnabled": false,
]) ])
// Hook up updater menu // Hook up updater menu
menuCheckForUpdates?.target = updaterController menuCheckForUpdates?.target = updaterController
menuCheckForUpdates?.action = #selector(SPUStandardUpdaterController.checkForUpdates(_:)) menuCheckForUpdates?.action = #selector(SPUStandardUpdaterController.checkForUpdates(_:))
// Initial config loading // Initial config loading
configDidReload(ghostty) configDidReload(ghostty)
// Register our service provider. This must happen after everything is initialized. // Register our service provider. This must happen after everything is initialized.
NSApp.servicesProvider = ServiceProvider() NSApp.servicesProvider = ServiceProvider()
// This registers the Ghostty => Services menu to exist. // This registers the Ghostty => Services menu to exist.
NSApp.servicesMenu = menuServices NSApp.servicesMenu = menuServices
@ -135,7 +135,7 @@ class AppDelegate: NSObject,
func applicationDidBecomeActive(_ notification: Notification) { func applicationDidBecomeActive(_ notification: Notification) {
guard !applicationHasBecomeActive else { return } guard !applicationHasBecomeActive else { return }
applicationHasBecomeActive = true applicationHasBecomeActive = true
// Let's launch our first window. We only do this if we have no other windows. It // Let's launch our first window. We only do this if we have no other windows. It
// is possible to have other windows in a few scenarios: // is possible to have other windows in a few scenarios:
// - if we're opening a URL since `application(_:openFile:)` is called before this. // - if we're opening a URL since `application(_:openFile:)` is called before this.
@ -152,39 +152,39 @@ class AppDelegate: NSObject,
func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
let windows = NSApplication.shared.windows let windows = NSApplication.shared.windows
if (windows.isEmpty) { return .terminateNow } if (windows.isEmpty) { return .terminateNow }
// This probably isn't fully safe. The isEmpty check above is aspirational, it doesn't // This probably isn't fully safe. The isEmpty check above is aspirational, it doesn't
// quite work with SwiftUI because windows are retained on close. So instead we check // quite work with SwiftUI because windows are retained on close. So instead we check
// if there are any that are visible. I'm guessing this breaks under certain scenarios. // if there are any that are visible. I'm guessing this breaks under certain scenarios.
if (windows.allSatisfy { !$0.isVisible }) { return .terminateNow } if (windows.allSatisfy { !$0.isVisible }) { return .terminateNow }
// If the user is shutting down, restarting, or logging out, we don't confirm quit. // If the user is shutting down, restarting, or logging out, we don't confirm quit.
why: if let event = NSAppleEventManager.shared().currentAppleEvent { why: if let event = NSAppleEventManager.shared().currentAppleEvent {
// If all Ghostty windows are in the background (i.e. you Cmd-Q from the Cmd-Tab // If all Ghostty windows are in the background (i.e. you Cmd-Q from the Cmd-Tab
// view), then this is null. I don't know why (pun intended) but we have to // view), then this is null. I don't know why (pun intended) but we have to
// guard against it. // guard against it.
guard let keyword = AEKeyword("why?") else { break why } guard let keyword = AEKeyword("why?") else { break why }
if let why = event.attributeDescriptor(forKeyword: keyword) { if let why = event.attributeDescriptor(forKeyword: keyword) {
switch (why.typeCodeValue) { switch (why.typeCodeValue) {
case kAEShutDown: case kAEShutDown:
fallthrough fallthrough
case kAERestart: case kAERestart:
fallthrough fallthrough
case kAEReallyLogOut: case kAEReallyLogOut:
return .terminateNow return .terminateNow
default: default:
break break
} }
} }
} }
// If our app says we don't need to confirm, we can exit now. // 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. // We have some visible window. Show an app-wide modal to confirm quitting.
let alert = NSAlert() let alert = NSAlert()
alert.messageText = "Quit Ghostty?" alert.messageText = "Quit Ghostty?"
@ -195,31 +195,31 @@ class AppDelegate: NSObject,
switch (alert.runModal()) { switch (alert.runModal()) {
case .alertFirstButtonReturn: case .alertFirstButtonReturn:
return .terminateNow return .terminateNow
default: default:
return .terminateCancel return .terminateCancel
} }
} }
/// This is called when the application is already open and someone double-clicks the icon /// This is called when the application is already open and someone double-clicks the icon
/// or clicks the dock icon. /// or clicks the dock icon.
func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool { func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
// If we have visible windows then we allow macOS to do its default behavior // If we have visible windows then we allow macOS to do its default behavior
// of focusing one of them. // of focusing one of them.
guard !flag else { return true } guard !flag else { return true }
// No visible windows, open a new one. // No visible windows, open a new one.
terminalManager.newWindow() terminalManager.newWindow()
return false return false
} }
func application(_ sender: NSApplication, openFile filename: String) -> Bool { func application(_ sender: NSApplication, openFile filename: String) -> Bool {
// Ghostty will validate as well but we can avoid creating an entirely new // 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 // surface by doing our own validation here. We can also show a useful error
// this way. // this way.
var isDirectory = ObjCBool(true) var isDirectory = ObjCBool(true)
guard FileManager.default.fileExists(atPath: filename, isDirectory: &isDirectory) else { return false } guard FileManager.default.fileExists(atPath: filename, isDirectory: &isDirectory) else { return false }
// Initialize the surface config which will be used to create the tab or window for the opened file. // Initialize the surface config which will be used to create the tab or window for the opened file.
var config = Ghostty.SurfaceConfiguration() var config = Ghostty.SurfaceConfiguration()
@ -238,20 +238,20 @@ class AppDelegate: NSObject,
return true return true
} }
/// This is called for the dock right-click menu. /// This is called for the dock right-click menu.
func applicationDockMenu(_ sender: NSApplication) -> NSMenu? { func applicationDockMenu(_ sender: NSApplication) -> NSMenu? {
return dockMenu return dockMenu
} }
/// Sync all of our menu item keyboard shortcuts with the Ghostty configuration. /// Sync all of our menu item keyboard shortcuts with the Ghostty configuration.
private func syncMenuShortcuts() { private func syncMenuShortcuts() {
guard ghostty.readiness == .ready else { return } guard ghostty.readiness == .ready else { return }
syncMenuShortcut(action: "open_config", menuItem: self.menuOpenConfig) syncMenuShortcut(action: "open_config", menuItem: self.menuOpenConfig)
syncMenuShortcut(action: "reload_config", menuItem: self.menuReloadConfig) syncMenuShortcut(action: "reload_config", menuItem: self.menuReloadConfig)
syncMenuShortcut(action: "quit", menuItem: self.menuQuit) syncMenuShortcut(action: "quit", menuItem: self.menuQuit)
syncMenuShortcut(action: "new_window", menuItem: self.menuNewWindow) syncMenuShortcut(action: "new_window", menuItem: self.menuNewWindow)
syncMenuShortcut(action: "new_tab", menuItem: self.menuNewTab) syncMenuShortcut(action: "new_tab", menuItem: self.menuNewTab)
syncMenuShortcut(action: "close_surface", menuItem: self.menuClose) syncMenuShortcut(action: "close_surface", menuItem: self.menuClose)
@ -259,11 +259,11 @@ class AppDelegate: NSObject,
syncMenuShortcut(action: "close_all_windows", menuItem: self.menuCloseAllWindows) syncMenuShortcut(action: "close_all_windows", menuItem: self.menuCloseAllWindows)
syncMenuShortcut(action: "new_split:right", menuItem: self.menuSplitRight) syncMenuShortcut(action: "new_split:right", menuItem: self.menuSplitRight)
syncMenuShortcut(action: "new_split:down", menuItem: self.menuSplitDown) syncMenuShortcut(action: "new_split:down", menuItem: self.menuSplitDown)
syncMenuShortcut(action: "copy_to_clipboard", menuItem: self.menuCopy) syncMenuShortcut(action: "copy_to_clipboard", menuItem: self.menuCopy)
syncMenuShortcut(action: "paste_from_clipboard", menuItem: self.menuPaste) syncMenuShortcut(action: "paste_from_clipboard", menuItem: self.menuPaste)
syncMenuShortcut(action: "select_all", menuItem: self.menuSelectAll) syncMenuShortcut(action: "select_all", menuItem: self.menuSelectAll)
syncMenuShortcut(action: "toggle_split_zoom", menuItem: self.menuZoomSplit) syncMenuShortcut(action: "toggle_split_zoom", menuItem: self.menuZoomSplit)
syncMenuShortcut(action: "goto_split:previous", menuItem: self.menuPreviousSplit) syncMenuShortcut(action: "goto_split:previous", menuItem: self.menuPreviousSplit)
syncMenuShortcut(action: "goto_split:next", menuItem: self.menuNextSplit) syncMenuShortcut(action: "goto_split:next", menuItem: self.menuNextSplit)
@ -281,7 +281,7 @@ class AppDelegate: NSObject,
syncMenuShortcut(action: "decrease_font_size:1", menuItem: self.menuDecreaseFontSize) syncMenuShortcut(action: "decrease_font_size:1", menuItem: self.menuDecreaseFontSize)
syncMenuShortcut(action: "reset_font_size", menuItem: self.menuResetFontSize) syncMenuShortcut(action: "reset_font_size", menuItem: self.menuResetFontSize)
syncMenuShortcut(action: "inspector:toggle", menuItem: self.menuTerminalInspector) syncMenuShortcut(action: "inspector:toggle", menuItem: self.menuTerminalInspector)
// This menu item is NOT synced with the configuration because it disables macOS // This menu item is NOT synced with the configuration because it disables macOS
// global fullscreen keyboard shortcut. The shortcut in the Ghostty config will continue // global fullscreen keyboard shortcut. The shortcut in the Ghostty config will continue
// to work but it won't be reflected in the menu item. // to work but it won't be reflected in the menu item.
@ -291,7 +291,7 @@ class AppDelegate: NSObject,
// Dock menu // Dock menu
reloadDockMenu() reloadDockMenu()
} }
/// Syncs a single menu shortcut for the given action. The action string is the same /// Syncs a single menu shortcut for the given action. The action string is the same
/// action string used for the Ghostty configuration. /// action string used for the Ghostty configuration.
private func syncMenuShortcut(action: String, menuItem: NSMenuItem?) { private func syncMenuShortcut(action: String, menuItem: NSMenuItem?) {
@ -302,17 +302,17 @@ class AppDelegate: NSObject,
menu.keyEquivalentModifierMask = [] menu.keyEquivalentModifierMask = []
return return
} }
menu.keyEquivalent = equiv.key menu.keyEquivalent = equiv.key
menu.keyEquivalentModifierMask = equiv.modifiers menu.keyEquivalentModifierMask = equiv.modifiers
} }
private func focusedSurface() -> ghostty_surface_t? { private func focusedSurface() -> ghostty_surface_t? {
return terminalManager.focusedSurface?.surface return terminalManager.focusedSurface?.surface
} }
//MARK: - Restorable State //MARK: - Restorable State
/// We support NSSecureCoding for restorable state. Required as of macOS Sonoma (14) but a good idea anyways. /// We support NSSecureCoding for restorable state. Required as of macOS Sonoma (14) but a good idea anyways.
func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
return true return true
@ -321,7 +321,7 @@ class AppDelegate: NSObject,
func application(_ app: NSApplication, willEncodeRestorableState coder: NSCoder) { func application(_ app: NSApplication, willEncodeRestorableState coder: NSCoder) {
Self.logger.debug("application will save window state") Self.logger.debug("application will save window state")
} }
func application(_ app: NSApplication, didDecodeRestorableState coder: NSCoder) { func application(_ app: NSApplication, didDecodeRestorableState coder: NSCoder) {
Self.logger.debug("application will restore window state") Self.logger.debug("application will restore window state")
} }
@ -348,17 +348,17 @@ class AppDelegate: NSObject,
} }
//MARK: - GhosttyAppDelegate //MARK: - GhosttyAppDelegate
func findSurface(forUUID uuid: UUID) -> Ghostty.SurfaceView? { func findSurface(forUUID uuid: UUID) -> Ghostty.SurfaceView? {
for c in terminalManager.windows { for c in terminalManager.windows {
if let v = c.controller.surfaceTree?.findUUID(uuid: uuid) { if let v = c.controller.surfaceTree?.findUUID(uuid: uuid) {
return v return v
} }
} }
return nil return nil
} }
func configDidReload(_ state: Ghostty.App) { func configDidReload(_ state: Ghostty.App) {
// Depending on the "window-save-state" setting we have to set the NSQuitAlwaysKeepsWindows // 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 // configuration. This is the only way to carefully control whether macOS invokes the
@ -369,21 +369,21 @@ class AppDelegate: NSObject,
case "default": fallthrough case "default": fallthrough
default: UserDefaults.standard.removeObject(forKey: "NSQuitAlwaysKeepsWindows") default: UserDefaults.standard.removeObject(forKey: "NSQuitAlwaysKeepsWindows")
} }
// Config could change keybindings, so update everything that depends on that // Config could change keybindings, so update everything that depends on that
syncMenuShortcuts() syncMenuShortcuts()
terminalManager.relabelAllTabs() terminalManager.relabelAllTabs()
// Config could change window appearance. We wrap this in an async queue because when // Config could change window appearance. We wrap this in an async queue because when
// this is called as part of application launch it can deadlock with an internal // this is called as part of application launch it can deadlock with an internal
// AppKit mutex on the appearance. // AppKit mutex on the appearance.
DispatchQueue.main.async { self.syncAppearance() } DispatchQueue.main.async { self.syncAppearance() }
// Update all of our windows // Update all of our windows
terminalManager.windows.forEach { window in terminalManager.windows.forEach { window in
window.controller.configDidReload() window.controller.configDidReload()
} }
// If we have configuration errors, we need to show them. // If we have configuration errors, we need to show them.
let c = ConfigurationErrorsController.sharedInstance let c = ConfigurationErrorsController.sharedInstance
c.errors = state.config.errors c.errors = state.config.errors
@ -393,7 +393,7 @@ class AppDelegate: NSObject,
} }
} }
} }
/// Sync the appearance of our app with the theme specified in the config. /// Sync the appearance of our app with the theme specified in the config.
private func syncAppearance() { private func syncAppearance() {
guard let theme = ghostty.config.windowTheme else { return } guard let theme = ghostty.config.windowTheme else { return }
@ -401,67 +401,67 @@ class AppDelegate: NSObject,
case "dark": case "dark":
let appearance = NSAppearance(named: .darkAqua) let appearance = NSAppearance(named: .darkAqua)
NSApplication.shared.appearance = appearance NSApplication.shared.appearance = appearance
case "light": case "light":
let appearance = NSAppearance(named: .aqua) let appearance = NSAppearance(named: .aqua)
NSApplication.shared.appearance = appearance NSApplication.shared.appearance = appearance
case "auto": case "auto":
let color = OSColor(ghostty.config.backgroundColor) let color = OSColor(ghostty.config.backgroundColor)
let appearance = NSAppearance(named: color.isLightColor ? .aqua : .darkAqua) let appearance = NSAppearance(named: color.isLightColor ? .aqua : .darkAqua)
NSApplication.shared.appearance = appearance NSApplication.shared.appearance = appearance
default: default:
NSApplication.shared.appearance = nil NSApplication.shared.appearance = nil
} }
} }
//MARK: - Dock Menu //MARK: - Dock Menu
private func reloadDockMenu() { private func reloadDockMenu() {
let newWindow = NSMenuItem(title: "New Window", action: #selector(newWindow), keyEquivalent: "") let newWindow = NSMenuItem(title: "New Window", action: #selector(newWindow), keyEquivalent: "")
let newTab = NSMenuItem(title: "New Tab", action: #selector(newTab), keyEquivalent: "") let newTab = NSMenuItem(title: "New Tab", action: #selector(newTab), keyEquivalent: "")
dockMenu.removeAllItems() dockMenu.removeAllItems()
dockMenu.addItem(newWindow) dockMenu.addItem(newWindow)
dockMenu.addItem(newTab) dockMenu.addItem(newTab)
} }
//MARK: - IB Actions //MARK: - IB Actions
@IBAction func openConfig(_ sender: Any?) { @IBAction func openConfig(_ sender: Any?) {
ghostty.openConfig() ghostty.openConfig()
} }
@IBAction func reloadConfig(_ sender: Any?) { @IBAction func reloadConfig(_ sender: Any?) {
ghostty.reloadConfig() ghostty.reloadConfig()
} }
@IBAction func newWindow(_ sender: Any?) { @IBAction func newWindow(_ sender: Any?) {
terminalManager.newWindow() terminalManager.newWindow()
// We also activate our app so that it becomes front. This may be // We also activate our app so that it becomes front. This may be
// necessary for the dock menu. // necessary for the dock menu.
NSApp.activate(ignoringOtherApps: true) NSApp.activate(ignoringOtherApps: true)
} }
@IBAction func newTab(_ sender: Any?) { @IBAction func newTab(_ sender: Any?) {
terminalManager.newTab() terminalManager.newTab()
// We also activate our app so that it becomes front. This may be // We also activate our app so that it becomes front. This may be
// necessary for the dock menu. // necessary for the dock menu.
NSApp.activate(ignoringOtherApps: true) NSApp.activate(ignoringOtherApps: true)
} }
@IBAction func closeAllWindows(_ sender: Any?) { @IBAction func closeAllWindows(_ sender: Any?) {
terminalManager.closeAllWindows() terminalManager.closeAllWindows()
AboutController.shared.hide() AboutController.shared.hide()
} }
@IBAction func showAbout(_ sender: Any?) { @IBAction func showAbout(_ sender: Any?) {
AboutController.shared.show() AboutController.shared.show()
} }
@IBAction func showHelp(_ sender: Any) { @IBAction func showHelp(_ sender: Any) {
guard let url = URL(string: "https://github.com/ghostty-org/ghostty") else { return } guard let url = URL(string: "https://github.com/ghostty-org/ghostty") else { return }
NSWorkspace.shared.open(url) NSWorkspace.shared.open(url)

View File

@ -4,35 +4,35 @@ import SwiftUI
class AboutController: NSWindowController, NSWindowDelegate { class AboutController: NSWindowController, NSWindowDelegate {
static let shared: AboutController = AboutController() static let shared: AboutController = AboutController()
override var windowNibName: NSNib.Name? { "About" } override var windowNibName: NSNib.Name? { "About" }
override func windowDidLoad() { override func windowDidLoad() {
guard let window = window else { return } guard let window = window else { return }
window.center() window.center()
window.contentView = NSHostingView(rootView: AboutView()) window.contentView = NSHostingView(rootView: AboutView())
} }
// MARK: - Functions // MARK: - Functions
func show() { func show() {
window?.makeKeyAndOrderFront(nil) window?.makeKeyAndOrderFront(nil)
} }
func hide() { func hide() {
window?.close() window?.close()
} }
//MARK: - First Responder //MARK: - First Responder
@IBAction func close(_ sender: Any) { @IBAction func close(_ sender: Any) {
self.window?.performClose(sender) self.window?.performClose(sender)
} }
@IBAction func closeWindow(_ sender: Any) { @IBAction func closeWindow(_ sender: Any) {
self.window?.performClose(sender) self.window?.performClose(sender)
} }
// This is called when "escape" is pressed. // This is called when "escape" is pressed.
@objc func cancel(_ sender: Any?) { @objc func cancel(_ sender: Any?) {
close() close()

View File

@ -5,24 +5,24 @@ struct AboutView: View {
var build: String? { Bundle.main.infoDictionary?["CFBundleVersion"] as? String } var build: String? { Bundle.main.infoDictionary?["CFBundleVersion"] as? String }
var commit: String? { Bundle.main.infoDictionary?["GhosttyCommit"] as? String } var commit: String? { Bundle.main.infoDictionary?["GhosttyCommit"] as? String }
var version: String? { Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String } var version: String? { Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String }
var body: some View { var body: some View {
VStack(alignment: .center) { VStack(alignment: .center) {
Image("AppIconImage") Image("AppIconImage")
.resizable() .resizable()
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)
.frame(maxHeight: 96) .frame(maxHeight: 96)
Text("Ghostty") Text("Ghostty")
.font(.title3) .font(.title3)
.textSelection(.enabled) .textSelection(.enabled)
if let version = self.version { if let version = self.version {
Text("Version: \(version)") Text("Version: \(version)")
.font(.body) .font(.body)
.textSelection(.enabled) .textSelection(.enabled)
} }
if let build = self.build { if let build = self.build {
Text("Build: \(build)") Text("Build: \(build)")
.font(.body) .font(.body)

View File

@ -3,13 +3,13 @@ import AppKit
class ServiceProvider: NSObject { class ServiceProvider: NSObject {
static private let errorNoString = NSString(string: "Could not load any text from the clipboard.") static private let errorNoString = NSString(string: "Could not load any text from the clipboard.")
/// The target for an open operation /// The target for an open operation
enum OpenTarget { enum OpenTarget {
case tab case tab
case window case window
} }
@objc func openTab( @objc func openTab(
_ pasteboard: NSPasteboard, _ pasteboard: NSPasteboard,
userData: String?, userData: String?,
@ -17,7 +17,7 @@ class ServiceProvider: NSObject {
) { ) {
openTerminalFromPasteboard(pasteboard: pasteboard, target: .tab, error: error) openTerminalFromPasteboard(pasteboard: pasteboard, target: .tab, error: error)
} }
@objc func openWindow( @objc func openWindow(
_ pasteboard: NSPasteboard, _ pasteboard: NSPasteboard,
userData: String?, userData: String?,
@ -37,10 +37,10 @@ class ServiceProvider: NSObject {
return return
} }
let filePaths = objs.map { $0.path }.compactMap { $0 } let filePaths = objs.map { $0.path }.compactMap { $0 }
openTerminal(filePaths, target: target) openTerminal(filePaths, target: target)
} }
private func openTerminal(_ paths: [String], target: OpenTarget) { private func openTerminal(_ paths: [String], target: OpenTarget) {
guard let delegateRaw = NSApp.delegate else { return } guard let delegateRaw = NSApp.delegate else { return }
guard let delegate = delegateRaw as? AppDelegate else { return } guard let delegate = delegateRaw as? AppDelegate else { return }
@ -51,7 +51,7 @@ class ServiceProvider: NSObject {
var isDirectory = ObjCBool(true) var isDirectory = ObjCBool(true)
guard FileManager.default.fileExists(atPath: path, isDirectory: &isDirectory) else { continue } guard FileManager.default.fileExists(atPath: path, isDirectory: &isDirectory) else { continue }
guard isDirectory.boolValue else { continue } guard isDirectory.boolValue else { continue }
// Build our config // Build our config
var config = Ghostty.SurfaceConfiguration() var config = Ghostty.SurfaceConfiguration()
config.workingDirectory = path config.workingDirectory = path

View File

@ -6,9 +6,9 @@ import Combine
class ConfigurationErrorsController: NSWindowController, NSWindowDelegate, ConfigurationErrorsViewModel { class ConfigurationErrorsController: NSWindowController, NSWindowDelegate, ConfigurationErrorsViewModel {
/// Singleton for the errors view. /// Singleton for the errors view.
static let sharedInstance = ConfigurationErrorsController() static let sharedInstance = ConfigurationErrorsController()
override var windowNibName: NSNib.Name? { "ConfigurationErrors" } override var windowNibName: NSNib.Name? { "ConfigurationErrors" }
/// The data model for this view. Update this directly and the associated view will be updated, too. /// The data model for this view. Update this directly and the associated view will be updated, too.
@Published var errors: [String] = [] { @Published var errors: [String] = [] {
didSet { didSet {
@ -17,13 +17,13 @@ class ConfigurationErrorsController: NSWindowController, NSWindowDelegate, Confi
} }
} }
} }
//MARK: - NSWindowController //MARK: - NSWindowController
override func windowWillLoad() { override func windowWillLoad() {
shouldCascadeWindows = false shouldCascadeWindows = false
} }
override func windowDidLoad() { override func windowDidLoad() {
guard let window = window else { return } guard let window = window else { return }
window.center() window.center()

View File

@ -6,7 +6,7 @@ protocol ConfigurationErrorsViewModel: ObservableObject {
struct ConfigurationErrorsView<ViewModel: ConfigurationErrorsViewModel>: View { struct ConfigurationErrorsView<ViewModel: ConfigurationErrorsViewModel>: View {
@ObservedObject var model: ViewModel @ObservedObject var model: ViewModel
var body: some View { var body: some View {
VStack { VStack {
HStack { HStack {
@ -15,7 +15,7 @@ struct ConfigurationErrorsView<ViewModel: ConfigurationErrorsViewModel>: View {
.font(.system(size: 52)) .font(.system(size: 52))
.padding() .padding()
.frame(alignment: .center) .frame(alignment: .center)
Text(""" Text("""
^[\(model.errors.count) error(s) were](inflect: true) found while loading the configuration. \ ^[\(model.errors.count) error(s) were](inflect: true) found while loading the configuration. \
Please review the errors below and reload your configuration or ignore the erroneous lines. Please review the errors below and reload your configuration or ignore the erroneous lines.
@ -34,7 +34,7 @@ struct ConfigurationErrorsView<ViewModel: ConfigurationErrorsViewModel>: View {
.textSelection(.enabled) .textSelection(.enabled)
.frame(maxWidth: .infinity, alignment: .topLeading) .frame(maxWidth: .infinity, alignment: .topLeading)
} }
Spacer() Spacer()
} }
.padding(.all) .padding(.all)
@ -42,7 +42,7 @@ struct ConfigurationErrorsView<ViewModel: ConfigurationErrorsViewModel>: View {
.background(Color(.controlBackgroundColor)) .background(Color(.controlBackgroundColor))
} }
} }
HStack { HStack {
Spacer() Spacer()
Button("Ignore") { model.errors = [] } Button("Ignore") { model.errors = [] }
@ -52,7 +52,7 @@ struct ConfigurationErrorsView<ViewModel: ConfigurationErrorsViewModel>: View {
} }
.frame(minWidth: 480, maxWidth: 960, minHeight: 270) .frame(minWidth: 480, maxWidth: 960, minHeight: 270)
} }
private func reloadConfig() { private func reloadConfig() {
guard let delegate = NSApplication.shared.delegate as? AppDelegate else { return } guard let delegate = NSApplication.shared.delegate as? AppDelegate else { return }
delegate.reloadConfig(nil) delegate.reloadConfig(nil)

View File

@ -3,14 +3,14 @@ import SwiftUI
struct SettingsView: View { struct SettingsView: View {
// We need access to our app delegate to know if we're quitting or not. // We need access to our app delegate to know if we're quitting or not.
@EnvironmentObject private var appDelegate: AppDelegate @EnvironmentObject private var appDelegate: AppDelegate
var body: some View { var body: some View {
HStack { HStack {
Image("AppIconImage") Image("AppIconImage")
.resizable() .resizable()
.scaledToFit() .scaledToFit()
.frame(width: 128, height: 128) .frame(width: 128, height: 128)
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text("Coming Soon. 🚧").font(.title) Text("Coming Soon. 🚧").font(.title)
Text("You can't configure settings in the GUI yet. To modify settings, " + Text("You can't configure settings in the GUI yet. To modify settings, " +

View File

@ -7,7 +7,7 @@ struct ErrorView: View {
.resizable() .resizable()
.scaledToFit() .scaledToFit()
.frame(width: 128, height: 128) .frame(width: 128, height: 128)
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text("Oh, no. 😭").font(.title) Text("Oh, no. 😭").font(.title)
Text("Something went fatally wrong.\nCheck the logs and restart Ghostty.") Text("Something went fatally wrong.\nCheck the logs and restart Ghostty.")

View File

@ -4,22 +4,22 @@ import SwiftUI
import GhosttyKit import GhosttyKit
/// The terminal controller is an NSWindowController that maps 1:1 to a terminal window. /// The terminal controller is an NSWindowController that maps 1:1 to a terminal window.
class TerminalController: NSWindowController, NSWindowDelegate, class TerminalController: NSWindowController, NSWindowDelegate,
TerminalViewDelegate, TerminalViewModel, TerminalViewDelegate, TerminalViewModel,
ClipboardConfirmationViewDelegate ClipboardConfirmationViewDelegate
{ {
override var windowNibName: NSNib.Name? { "Terminal" } override var windowNibName: NSNib.Name? { "Terminal" }
/// The app instance that this terminal view will represent. /// The app instance that this terminal view will represent.
let ghostty: Ghostty.App let ghostty: Ghostty.App
/// The currently focused surface. /// The currently focused surface.
var focusedSurface: Ghostty.SurfaceView? = nil { var focusedSurface: Ghostty.SurfaceView? = nil {
didSet { didSet {
syncFocusToSurfaceTree() syncFocusToSurfaceTree()
} }
} }
/// The surface tree for this window. /// The surface tree for this window.
@Published var surfaceTree: Ghostty.SplitNode? = nil { @Published var surfaceTree: Ghostty.SplitNode? = nil {
didSet { didSet {
@ -32,25 +32,25 @@ class TerminalController: NSWindowController, NSWindowDelegate,
} }
} }
} }
/// Fullscreen state management. /// Fullscreen state management.
let fullscreenHandler = FullScreenHandler() let fullscreenHandler = FullScreenHandler()
/// True when an alert is active so we don't overlap multiple. /// True when an alert is active so we don't overlap multiple.
private var alert: NSAlert? = nil private var alert: NSAlert? = nil
/// The clipboard confirmation window, if shown. /// The clipboard confirmation window, if shown.
private var clipboardConfirmation: ClipboardConfirmationController? = nil private var clipboardConfirmation: ClipboardConfirmationController? = nil
/// This is set to true when we care about frame changes. This is a small optimization since /// 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 /// this controller registers a listener for ALL frame change notifications and this lets us bail
/// early if we don't care. /// early if we don't care.
private var tabListenForFrame: Bool = false private var tabListenForFrame: Bool = false
/// This is the hash value of the last tabGroup.windows array. We use this to detect order /// This is the hash value of the last tabGroup.windows array. We use this to detect order
/// changes in the list. /// changes in the list.
private var tabWindowsHash: Int = 0 private var tabWindowsHash: Int = 0
/// This is set to false by init if the window managed by this controller should not be restorable. /// 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. /// For example, terminals executing custom scripts are not restorable.
private var restorable: Bool = true private var restorable: Bool = true
@ -60,20 +60,20 @@ class TerminalController: NSWindowController, NSWindowDelegate,
withSurfaceTree tree: Ghostty.SplitNode? = nil withSurfaceTree tree: Ghostty.SplitNode? = nil
) { ) {
self.ghostty = ghostty self.ghostty = ghostty
// The window we manage is not restorable if we've specified a command // The window we manage is not restorable if we've specified a command
// to execute. We do this because the restored window is meaningless at the // to execute. We do this because the restored window is meaningless at the
// time of writing this: it'd just restore to a shell in the same directory // time of writing this: it'd just restore to a shell in the same directory
// as the script. We may want to revisit this behavior when we have scrollback // as the script. We may want to revisit this behavior when we have scrollback
// restoration. // restoration.
self.restorable = (base?.command ?? "") == "" self.restorable = (base?.command ?? "") == ""
super.init(window: nil) super.init(window: nil)
// Initialize our initial surface. // Initialize our initial surface.
guard let ghostty_app = ghostty.app else { preconditionFailure("app must be loaded") } guard let ghostty_app = ghostty.app else { preconditionFailure("app must be loaded") }
self.surfaceTree = tree ?? .leaf(.init(ghostty_app, baseConfig: base)) self.surfaceTree = tree ?? .leaf(.init(ghostty_app, baseConfig: base))
// Setup our notifications for behaviors // Setup our notifications for behaviors
let center = NotificationCenter.default let center = NotificationCenter.default
center.addObserver( center.addObserver(
@ -97,25 +97,25 @@ class TerminalController: NSWindowController, NSWindowDelegate,
name: NSView.frameDidChangeNotification, name: NSView.frameDidChangeNotification,
object: nil) object: nil)
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
fatalError("init(coder:) is not supported for this view") fatalError("init(coder:) is not supported for this view")
} }
deinit { deinit {
// Remove all of our notificationcenter subscriptions // Remove all of our notificationcenter subscriptions
let center = NotificationCenter.default let center = NotificationCenter.default
center.removeObserver(self) center.removeObserver(self)
} }
//MARK: - Methods //MARK: - Methods
func configDidReload() { func configDidReload() {
guard let window = window as? TerminalWindow else { return } guard let window = window as? TerminalWindow else { return }
window.focusFollowsMouse = ghostty.config.focusFollowsMouse window.focusFollowsMouse = ghostty.config.focusFollowsMouse
syncAppearance() syncAppearance()
} }
/// Update the accessory view of each tab according to the keyboard /// Update the accessory view of each tab according to the keyboard
/// shortcut that activates it (if any). This is called when the key window /// shortcut that activates it (if any). This is called when the key window
/// changes, when a window is closed, and when tabs are reordered /// changes, when a window is closed, and when tabs are reordered
@ -129,7 +129,7 @@ class TerminalController: NSWindowController, NSWindowDelegate,
// We only listen for frame changes if we have more than 1 window, // We only listen for frame changes if we have more than 1 window,
// otherwise the accessory view doesn't matter. // otherwise the accessory view doesn't matter.
tabListenForFrame = windows.count > 1 tabListenForFrame = windows.count > 1
for (tab, window) in zip(1..., windows) { for (tab, window) in zip(1..., windows) {
// We need to clear any windows beyond this because they have had // We need to clear any windows beyond this because they have had
// a keyEquivalent set previously. // a keyEquivalent set previously.
@ -158,7 +158,7 @@ class TerminalController: NSWindowController, NSWindowDelegate,
window.isOpaque = false window.isOpaque = false
} }
} }
@objc private func onFrameDidChange(_ notification: NSNotification) { @objc private func onFrameDidChange(_ notification: NSNotification) {
// This is a huge hack to set the proper shortcut for tab selection // This is a huge hack to set the proper shortcut for tab selection
// on tab reordering using the mouse. There is no event, delegate, etc. // on tab reordering using the mouse. There is no event, delegate, etc.
@ -173,10 +173,10 @@ class TerminalController: NSWindowController, NSWindowDelegate,
tabWindowsHash = v tabWindowsHash = v
self.relabelTabs() self.relabelTabs()
} }
private func syncAppearance() { private func syncAppearance() {
guard let window = self.window as? TerminalWindow else { return } guard let window = self.window as? TerminalWindow else { return }
// If our window is not visible, then delay this. This is possible specifically // If our window is not visible, then delay this. This is possible specifically
// during state restoration but probably in other scenarios as well. To delay, // during state restoration but probably in other scenarios as well. To delay,
// we just loop directly on the dispatch queue. We have to delay because some // we just loop directly on the dispatch queue. We have to delay because some
@ -186,14 +186,14 @@ class TerminalController: NSWindowController, NSWindowDelegate,
DispatchQueue.main.async { [weak self] in self?.syncAppearance() } DispatchQueue.main.async { [weak self] in self?.syncAppearance() }
return return
} }
// Set the font for the window and tab titles. // Set the font for the window and tab titles.
if let titleFontName = ghostty.config.windowTitleFontFamily { if let titleFontName = ghostty.config.windowTitleFontFamily {
window.titlebarFont = NSFont(name: titleFontName, size: NSFont.systemFontSize) window.titlebarFont = NSFont(name: titleFontName, size: NSFont.systemFontSize)
} else { } else {
window.titlebarFont = nil window.titlebarFont = nil
} }
// If we have window transparency then set it transparent. Otherwise set it opaque. // If we have window transparency then set it transparent. Otherwise set it opaque.
if (ghostty.config.backgroundOpacity < 1) { if (ghostty.config.backgroundOpacity < 1) {
window.isOpaque = false window.isOpaque = false
@ -202,7 +202,7 @@ class TerminalController: NSWindowController, NSWindowDelegate,
// matches Terminal.app much more closer. This lets users transition from // matches Terminal.app much more closer. This lets users transition from
// Terminal.app more easily. // Terminal.app more easily.
window.backgroundColor = .white.withAlphaComponent(0.001) window.backgroundColor = .white.withAlphaComponent(0.001)
ghostty_set_window_background_blur(ghostty.app, Unmanaged.passUnretained(window).toOpaque()) ghostty_set_window_background_blur(ghostty.app, Unmanaged.passUnretained(window).toOpaque())
} else { } else {
window.isOpaque = true window.isOpaque = true
@ -217,19 +217,19 @@ class TerminalController: NSWindowController, NSWindowDelegate,
// because we handle it here. // because we handle it here.
let backgroundColor = OSColor(ghostty.config.backgroundColor) let backgroundColor = OSColor(ghostty.config.backgroundColor)
window.titlebarColor = backgroundColor.withAlphaComponent(ghostty.config.backgroundOpacity) window.titlebarColor = backgroundColor.withAlphaComponent(ghostty.config.backgroundOpacity)
if (window.isOpaque) { if (window.isOpaque) {
// Bg color is only synced if we have no transparency. This is because // Bg color is only synced if we have no transparency. This is because
// the transparency is handled at the surface level (window.backgroundColor // the transparency is handled at the surface level (window.backgroundColor
// ignores alpha components) // ignores alpha components)
window.backgroundColor = backgroundColor window.backgroundColor = backgroundColor
// If there is transparency, calling this will make the titlebar opaque // If there is transparency, calling this will make the titlebar opaque
// so we only call this if we are opaque. // so we only call this if we are opaque.
window.updateTabBar() window.updateTabBar()
} }
} }
/// Update all surfaces with the focus state. This ensures that libghostty has an accurate view about /// Update all surfaces with the focus state. This ensures that libghostty has an accurate view about
/// what surface is focused. This must be called whenever a surface OR window changes focus. /// what surface is focused. This must be called whenever a surface OR window changes focus.
private func syncFocusToSurfaceTree() { private func syncFocusToSurfaceTree() {
@ -246,25 +246,25 @@ class TerminalController: NSWindowController, NSWindowDelegate,
} }
//MARK: - NSWindowController //MARK: - NSWindowController
override func windowWillLoad() { override func windowWillLoad() {
// We do NOT want to cascade because we handle this manually from the manager. // We do NOT want to cascade because we handle this manually from the manager.
shouldCascadeWindows = false shouldCascadeWindows = false
} }
override func windowDidLoad() { override func windowDidLoad() {
guard let window = window as? TerminalWindow else { return } guard let window = window as? TerminalWindow else { return }
// Setting all three of these is required for restoration to work. // Setting all three of these is required for restoration to work.
window.isRestorable = restorable window.isRestorable = restorable
if (restorable) { if (restorable) {
window.restorationClass = TerminalWindowRestoration.self window.restorationClass = TerminalWindowRestoration.self
window.identifier = .init(String(describing: TerminalWindowRestoration.self)) window.identifier = .init(String(describing: TerminalWindowRestoration.self))
} }
// If window decorations are disabled, remove our title // If window decorations are disabled, remove our title
if (!ghostty.config.windowDecorations) { window.styleMask.remove(.titled) } if (!ghostty.config.windowDecorations) { window.styleMask.remove(.titled) }
// Terminals typically operate in sRGB color space and macOS defaults // Terminals typically operate in sRGB color space and macOS defaults
// to "native" which is typically P3. There is a lot more resources // to "native" which is typically P3. There is a lot more resources
// covered in this GitHub issue: https://github.com/mitchellh/ghostty/pull/376 // covered in this GitHub issue: https://github.com/mitchellh/ghostty/pull/376
@ -277,7 +277,7 @@ class TerminalController: NSWindowController, NSWindowDelegate,
default: default:
window.colorSpace = .sRGB window.colorSpace = .sRGB
} }
// If we have only a single surface (no splits) and that surface requested // If we have only a single surface (no splits) and that surface requested
// an initial size then we set it here now. // an initial size then we set it here now.
if case let .leaf(leaf) = surfaceTree { if case let .leaf(leaf) = surfaceTree {
@ -289,12 +289,12 @@ class TerminalController: NSWindowController, NSWindowDelegate,
frame.size.height -= leaf.surface.frame.size.height frame.size.height -= leaf.surface.frame.size.height
frame.size.width += initialSize.width frame.size.width += initialSize.width
frame.size.height += initialSize.height frame.size.height += initialSize.height
// We have no tabs and we are not a split, so set the initial size of the window. // We have no tabs and we are not a split, so set the initial size of the window.
window.setFrame(frame, display: true) window.setFrame(frame, display: true)
} }
} }
// Center the window to start, we'll move the window frame automatically // Center the window to start, we'll move the window frame automatically
// when cascading. // when cascading.
window.center() window.center()
@ -316,7 +316,7 @@ class TerminalController: NSWindowController, NSWindowDelegate,
} else if (ghostty.config.macosTitlebarStyle == "transparent") { } else if (ghostty.config.macosTitlebarStyle == "transparent") {
window.transparentTabs = true window.transparentTabs = true
} }
if window.hasStyledTabs { if window.hasStyledTabs {
// Set the background color of the window // Set the background color of the window
let backgroundColor = NSColor(ghostty.config.backgroundColor) let backgroundColor = NSColor(ghostty.config.backgroundColor)
@ -332,7 +332,7 @@ class TerminalController: NSWindowController, NSWindowDelegate,
viewModel: self, viewModel: self,
delegate: self delegate: self
)) ))
// In various situations, macOS automatically tabs new windows. Ghostty handles // In various situations, macOS automatically tabs new windows. Ghostty handles
// its own tabbing so we DONT want this behavior. This detects this scenario and undoes // its own tabbing so we DONT want this behavior. This detects this scenario and undoes
// it. // it.
@ -358,7 +358,7 @@ class TerminalController: NSWindowController, NSWindowDelegate,
// Apply any additional appearance-related properties to the new window. // Apply any additional appearance-related properties to the new window.
syncAppearance() syncAppearance()
} }
// Shows the "+" button in the tab bar, responds to that click. // Shows the "+" button in the tab bar, responds to that click.
override func newWindowForTab(_ sender: Any?) { override func newWindowForTab(_ sender: Any?) {
// Trigger the ghostty core event logic for a new tab. // Trigger the ghostty core event logic for a new tab.
@ -367,23 +367,23 @@ class TerminalController: NSWindowController, NSWindowDelegate,
} }
//MARK: - NSWindowDelegate //MARK: - NSWindowDelegate
// This is called when performClose is called on a window (NOT when close() // This is called when performClose is called on a window (NOT when close()
// is called directly). performClose is called primarily when UI elements such // is called directly). performClose is called primarily when UI elements such
// as the "red X" are pressed. // as the "red X" are pressed.
func windowShouldClose(_ sender: NSWindow) -> Bool { func windowShouldClose(_ sender: NSWindow) -> Bool {
// We must have a window. Is it even possible not to? // We must have a window. Is it even possible not to?
guard let window = self.window else { return true } guard let window = self.window else { return true }
// If we have no surfaces, close. // If we have no surfaces, close.
guard let node = self.surfaceTree else { return true } guard let node = self.surfaceTree else { return true }
// If we already have an alert, continue with it // If we already have an alert, continue with it
guard alert == nil else { return false } guard alert == nil else { return false }
// If our surfaces don't require confirmation, close. // If our surfaces don't require confirmation, close.
if (!node.needsConfirmQuit()) { return true } if (!node.needsConfirmQuit()) { return true }
// We require confirmation, so show an alert as long as we aren't already. // We require confirmation, so show an alert as long as we aren't already.
let alert = NSAlert() let alert = NSAlert()
alert.messageText = "Close Terminal?" alert.messageText = "Close Terminal?"
@ -397,45 +397,45 @@ class TerminalController: NSWindowController, NSWindowDelegate,
switch (response) { switch (response) {
case .alertFirstButtonReturn: case .alertFirstButtonReturn:
window.close() window.close()
default: default:
break break
} }
}) })
self.alert = alert self.alert = alert
return false return false
} }
func windowWillClose(_ notification: Notification) { func windowWillClose(_ notification: Notification) {
// I don't know if this is required anymore. We previously had a ref cycle between // I don't know if this is required anymore. We previously had a ref cycle between
// the view and the window so we had to nil this out to break it but I think this // the view and the window so we had to nil this out to break it but I think this
// may now be resolved. We should verify that no memory leaks and we can remove this. // may now be resolved. We should verify that no memory leaks and we can remove this.
self.window?.contentView = nil self.window?.contentView = nil
self.relabelTabs() self.relabelTabs()
} }
func windowDidBecomeKey(_ notification: Notification) { func windowDidBecomeKey(_ notification: Notification) {
self.relabelTabs() self.relabelTabs()
self.fixTabBar() self.fixTabBar()
// Becoming/losing key means we have to notify our surface(s) that we have focus // Becoming/losing key means we have to notify our surface(s) that we have focus
// so things like cursors blink, pty events are sent, etc. // so things like cursors blink, pty events are sent, etc.
self.syncFocusToSurfaceTree() self.syncFocusToSurfaceTree()
} }
func windowDidResignKey(_ notification: Notification) { func windowDidResignKey(_ notification: Notification) {
// Becoming/losing key means we have to notify our surface(s) that we have focus // Becoming/losing key means we have to notify our surface(s) that we have focus
// so things like cursors blink, pty events are sent, etc. // so things like cursors blink, pty events are sent, etc.
self.syncFocusToSurfaceTree() self.syncFocusToSurfaceTree()
} }
func windowDidMove(_ notification: Notification) { func windowDidMove(_ notification: Notification) {
self.fixTabBar() self.fixTabBar()
} }
func windowDidChangeOcclusionState(_ notification: Notification) { func windowDidChangeOcclusionState(_ notification: Notification) {
guard let surfaceTree = self.surfaceTree else { return } guard let surfaceTree = self.surfaceTree else { return }
let visible = self.window?.occlusionState.contains(.visible) ?? false let visible = self.window?.occlusionState.contains(.visible) ?? false
@ -452,24 +452,24 @@ class TerminalController: NSWindowController, NSWindowDelegate,
let data = TerminalRestorableState(from: self) let data = TerminalRestorableState(from: self)
data.encode(with: state) data.encode(with: state)
} }
//MARK: - First Responder //MARK: - First Responder
@IBAction func newWindow(_ sender: Any?) { @IBAction func newWindow(_ sender: Any?) {
guard let surface = focusedSurface?.surface else { return } guard let surface = focusedSurface?.surface else { return }
ghostty.newWindow(surface: surface) ghostty.newWindow(surface: surface)
} }
@IBAction func newTab(_ sender: Any?) { @IBAction func newTab(_ sender: Any?) {
guard let surface = focusedSurface?.surface else { return } guard let surface = focusedSurface?.surface else { return }
ghostty.newTab(surface: surface) ghostty.newTab(surface: surface)
} }
@IBAction func close(_ sender: Any) { @IBAction func close(_ sender: Any) {
guard let surface = focusedSurface?.surface else { return } guard let surface = focusedSurface?.surface else { return }
ghostty.requestClose(surface: surface) ghostty.requestClose(surface: surface)
} }
@IBAction func closeWindow(_ sender: Any) { @IBAction func closeWindow(_ sender: Any) {
guard let window = window else { return } guard let window = window else { return }
guard let tabGroup = window.tabGroup else { guard let tabGroup = window.tabGroup else {
@ -477,13 +477,13 @@ class TerminalController: NSWindowController, NSWindowDelegate,
window.performClose(sender) window.performClose(sender)
return return
} }
// If have one window then we just do a normal close // If have one window then we just do a normal close
if tabGroup.windows.count == 1 { if tabGroup.windows.count == 1 {
window.performClose(sender) window.performClose(sender)
return return
} }
// Check if any windows require close confirmation. // Check if any windows require close confirmation.
var needsConfirm: Bool = false var needsConfirm: Bool = false
for tabWindow in tabGroup.windows { for tabWindow in tabGroup.windows {
@ -493,16 +493,16 @@ class TerminalController: NSWindowController, NSWindowDelegate,
break break
} }
} }
// If none need confirmation then we can just close all the windows. // If none need confirmation then we can just close all the windows.
if (!needsConfirm) { if (!needsConfirm) {
for tabWindow in tabGroup.windows { for tabWindow in tabGroup.windows {
tabWindow.close() tabWindow.close()
} }
return return
} }
// If we need confirmation by any, show one confirmation for all windows // If we need confirmation by any, show one confirmation for all windows
// in the tab group. // in the tab group.
let alert = NSAlert() let alert = NSAlert()
@ -519,42 +519,42 @@ class TerminalController: NSWindowController, NSWindowDelegate,
} }
}) })
} }
@IBAction func splitRight(_ sender: Any) { @IBAction func splitRight(_ sender: Any) {
guard let surface = focusedSurface?.surface else { return } guard let surface = focusedSurface?.surface else { return }
ghostty.split(surface: surface, direction: GHOSTTY_SPLIT_RIGHT) ghostty.split(surface: surface, direction: GHOSTTY_SPLIT_RIGHT)
} }
@IBAction func splitDown(_ sender: Any) { @IBAction func splitDown(_ sender: Any) {
guard let surface = focusedSurface?.surface else { return } guard let surface = focusedSurface?.surface else { return }
ghostty.split(surface: surface, direction: GHOSTTY_SPLIT_DOWN) ghostty.split(surface: surface, direction: GHOSTTY_SPLIT_DOWN)
} }
@IBAction func splitZoom(_ sender: Any) { @IBAction func splitZoom(_ sender: Any) {
guard let surface = focusedSurface?.surface else { return } guard let surface = focusedSurface?.surface else { return }
ghostty.splitToggleZoom(surface: surface) ghostty.splitToggleZoom(surface: surface)
} }
@IBAction func splitMoveFocusPrevious(_ sender: Any) { @IBAction func splitMoveFocusPrevious(_ sender: Any) {
splitMoveFocus(direction: .previous) splitMoveFocus(direction: .previous)
} }
@IBAction func splitMoveFocusNext(_ sender: Any) { @IBAction func splitMoveFocusNext(_ sender: Any) {
splitMoveFocus(direction: .next) splitMoveFocus(direction: .next)
} }
@IBAction func splitMoveFocusAbove(_ sender: Any) { @IBAction func splitMoveFocusAbove(_ sender: Any) {
splitMoveFocus(direction: .top) splitMoveFocus(direction: .top)
} }
@IBAction func splitMoveFocusBelow(_ sender: Any) { @IBAction func splitMoveFocusBelow(_ sender: Any) {
splitMoveFocus(direction: .bottom) splitMoveFocus(direction: .bottom)
} }
@IBAction func splitMoveFocusLeft(_ sender: Any) { @IBAction func splitMoveFocusLeft(_ sender: Any) {
splitMoveFocus(direction: .left) splitMoveFocus(direction: .left)
} }
@IBAction func splitMoveFocusRight(_ sender: Any) { @IBAction func splitMoveFocusRight(_ sender: Any) {
splitMoveFocus(direction: .right) splitMoveFocus(direction: .right)
} }
@ -588,12 +588,12 @@ class TerminalController: NSWindowController, NSWindowDelegate,
guard let surface = focusedSurface?.surface else { return } guard let surface = focusedSurface?.surface else { return }
ghostty.splitMoveFocus(surface: surface, direction: direction) ghostty.splitMoveFocus(surface: surface, direction: direction)
} }
@IBAction func toggleGhosttyFullScreen(_ sender: Any) { @IBAction func toggleGhosttyFullScreen(_ sender: Any) {
guard let surface = focusedSurface?.surface else { return } guard let surface = focusedSurface?.surface else { return }
ghostty.toggleFullscreen(surface: surface) ghostty.toggleFullscreen(surface: surface)
} }
@IBAction func increaseFontSize(_ sender: Any) { @IBAction func increaseFontSize(_ sender: Any) {
guard let surface = focusedSurface?.surface else { return } guard let surface = focusedSurface?.surface else { return }
ghostty.changeFontSize(surface: surface, .increase(1)) ghostty.changeFontSize(surface: surface, .increase(1))
@ -608,44 +608,44 @@ class TerminalController: NSWindowController, NSWindowDelegate,
guard let surface = focusedSurface?.surface else { return } guard let surface = focusedSurface?.surface else { return }
ghostty.changeFontSize(surface: surface, .reset) ghostty.changeFontSize(surface: surface, .reset)
} }
@IBAction func toggleTerminalInspector(_ sender: Any) { @IBAction func toggleTerminalInspector(_ sender: Any) {
guard let surface = focusedSurface?.surface else { return } guard let surface = focusedSurface?.surface else { return }
ghostty.toggleTerminalInspector(surface: surface) ghostty.toggleTerminalInspector(surface: surface)
} }
@objc func resetTerminal(_ sender: Any) { @objc func resetTerminal(_ sender: Any) {
guard let surface = focusedSurface?.surface else { return } guard let surface = focusedSurface?.surface else { return }
ghostty.resetTerminal(surface: surface) ghostty.resetTerminal(surface: surface)
} }
//MARK: - TerminalViewDelegate //MARK: - TerminalViewDelegate
func focusedSurfaceDidChange(to: Ghostty.SurfaceView?) { func focusedSurfaceDidChange(to: Ghostty.SurfaceView?) {
self.focusedSurface = to self.focusedSurface = to
} }
func titleDidChange(to: String) { func titleDidChange(to: String) {
guard let window = window as? TerminalWindow else { return } guard let window = window as? TerminalWindow else { return }
// Set the main window title // Set the main window title
window.title = to window.title = to
// Custom toolbar-based title used when titlebar tabs are enabled. // Custom toolbar-based title used when titlebar tabs are enabled.
if let toolbar = window.toolbar as? TerminalToolbar { if let toolbar = window.toolbar as? TerminalToolbar {
toolbar.titleText = to toolbar.titleText = to
} }
} }
func cellSizeDidChange(to: NSSize) { func cellSizeDidChange(to: NSSize) {
guard ghostty.config.windowStepResize else { return } guard ghostty.config.windowStepResize else { return }
self.window?.contentResizeIncrements = to self.window?.contentResizeIncrements = to
} }
func lastSurfaceDidClose() { func lastSurfaceDidClose() {
self.window?.close() self.window?.close()
} }
func surfaceTreeDidChange() { func surfaceTreeDidChange() {
// Whenever our surface tree changes in any way (new split, close split, etc.) // Whenever our surface tree changes in any way (new split, close split, etc.)
// we want to invalidate our state. // we want to invalidate our state.
@ -658,7 +658,7 @@ class TerminalController: NSWindowController, NSWindowDelegate,
} }
//MARK: - Clipboard Confirmation //MARK: - Clipboard Confirmation
func clipboardConfirmationComplete(_ action: ClipboardConfirmationView.Action, _ request: Ghostty.ClipboardRequest) { func clipboardConfirmationComplete(_ action: ClipboardConfirmationView.Action, _ request: Ghostty.ClipboardRequest) {
// End our clipboard confirmation no matter what // End our clipboard confirmation no matter what
guard let cc = self.clipboardConfirmation else { return } guard let cc = self.clipboardConfirmation else { return }
@ -688,30 +688,30 @@ class TerminalController: NSWindowController, NSWindowDelegate,
Ghostty.App.completeClipboardRequest(cc.surface, data: str, state: cc.state, confirmed: true) Ghostty.App.completeClipboardRequest(cc.surface, data: str, state: cc.state, confirmed: true)
} }
} }
//MARK: - Notifications //MARK: - Notifications
@objc private func onGotoTab(notification: SwiftUI.Notification) { @objc private func onGotoTab(notification: SwiftUI.Notification) {
guard let target = notification.object as? Ghostty.SurfaceView else { return } guard let target = notification.object as? Ghostty.SurfaceView else { return }
guard target == self.focusedSurface else { return } guard target == self.focusedSurface else { return }
guard let window = self.window else { return } guard let window = self.window else { return }
// Get the tab index from the notification // Get the tab index from the notification
guard let tabIndexAny = notification.userInfo?[Ghostty.Notification.GotoTabKey] else { return } guard let tabIndexAny = notification.userInfo?[Ghostty.Notification.GotoTabKey] else { return }
guard let tabIndex = tabIndexAny as? Int32 else { return } guard let tabIndex = tabIndexAny as? Int32 else { return }
guard let windowController = window.windowController else { return } guard let windowController = window.windowController else { return }
guard let tabGroup = windowController.window?.tabGroup else { return } guard let tabGroup = windowController.window?.tabGroup else { return }
let tabbedWindows = tabGroup.windows let tabbedWindows = tabGroup.windows
// This will be the index we want to actual go to // This will be the index we want to actual go to
let finalIndex: Int let finalIndex: Int
// An index that is invalid is used to signal some special values. // 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 selectedWindow = tabGroup.selectedWindow else { return }
guard let selectedIndex = tabbedWindows.firstIndex(where: { $0 == selectedWindow }) else { return } guard let selectedIndex = tabbedWindows.firstIndex(where: { $0 == selectedWindow }) else { return }
if (tabIndex == GHOSTTY_TAB_PREVIOUS.rawValue) { if (tabIndex == GHOSTTY_TAB_PREVIOUS.rawValue) {
if (selectedIndex == 0) { if (selectedIndex == 0) {
finalIndex = tabbedWindows.count - 1 finalIndex = tabbedWindows.count - 1
@ -731,51 +731,51 @@ class TerminalController: NSWindowController, NSWindowDelegate,
// Tabs are 0-indexed here, so we subtract one from the key the user hit. // Tabs are 0-indexed here, so we subtract one from the key the user hit.
finalIndex = Int(tabIndex - 1) finalIndex = Int(tabIndex - 1)
} }
guard finalIndex >= 0 && finalIndex < tabbedWindows.count else { return } guard finalIndex >= 0 && finalIndex < tabbedWindows.count else { return }
let targetWindow = tabbedWindows[finalIndex] let targetWindow = tabbedWindows[finalIndex]
targetWindow.makeKeyAndOrderFront(nil) targetWindow.makeKeyAndOrderFront(nil)
} }
@objc private func onToggleFullscreen(notification: SwiftUI.Notification) { @objc private func onToggleFullscreen(notification: SwiftUI.Notification) {
guard let target = notification.object as? Ghostty.SurfaceView else { return } guard let target = notification.object as? Ghostty.SurfaceView else { return }
guard target == self.focusedSurface else { return } guard target == self.focusedSurface else { return }
// We need a window to fullscreen // We need a window to fullscreen
guard let window = self.window else { return } guard let window = self.window else { return }
// Check whether we use non-native fullscreen // Check whether we use non-native fullscreen
guard let useNonNativeFullscreenAny = notification.userInfo?[Ghostty.Notification.NonNativeFullscreenKey] else { return } guard let useNonNativeFullscreenAny = notification.userInfo?[Ghostty.Notification.NonNativeFullscreenKey] else { return }
guard let useNonNativeFullscreen = useNonNativeFullscreenAny as? ghostty_non_native_fullscreen_e else { return } guard let useNonNativeFullscreen = useNonNativeFullscreenAny as? ghostty_non_native_fullscreen_e else { return }
self.fullscreenHandler.toggleFullscreen(window: window, nonNativeFullscreen: useNonNativeFullscreen) self.fullscreenHandler.toggleFullscreen(window: window, nonNativeFullscreen: useNonNativeFullscreen)
// For some reason focus always gets lost when we toggle fullscreen, so we set it back. // For some reason focus always gets lost when we toggle fullscreen, so we set it back.
if let focusedSurface { if let focusedSurface {
Ghostty.moveFocus(to: focusedSurface) Ghostty.moveFocus(to: focusedSurface)
} }
} }
@objc private func onConfirmClipboardRequest(notification: SwiftUI.Notification) { @objc private func onConfirmClipboardRequest(notification: SwiftUI.Notification) {
guard let target = notification.object as? Ghostty.SurfaceView else { return } guard let target = notification.object as? Ghostty.SurfaceView else { return }
guard target == self.focusedSurface else { return } guard target == self.focusedSurface else { return }
guard let surface = target.surface else { return } guard let surface = target.surface else { return }
// We need a window // We need a window
guard let window = self.window else { return } guard let window = self.window else { return }
// Check whether we use non-native fullscreen // Check whether we use non-native fullscreen
guard let str = notification.userInfo?[Ghostty.Notification.ConfirmClipboardStrKey] as? String else { return } guard let str = notification.userInfo?[Ghostty.Notification.ConfirmClipboardStrKey] as? String else { return }
guard let state = notification.userInfo?[Ghostty.Notification.ConfirmClipboardStateKey] as? UnsafeMutableRawPointer? else { return } guard let state = notification.userInfo?[Ghostty.Notification.ConfirmClipboardStateKey] as? UnsafeMutableRawPointer? else { return }
guard let request = notification.userInfo?[Ghostty.Notification.ConfirmClipboardRequestKey] as? Ghostty.ClipboardRequest else { return } guard let request = notification.userInfo?[Ghostty.Notification.ConfirmClipboardRequestKey] as? Ghostty.ClipboardRequest else { return }
// If we already have a clipboard confirmation view up, we ignore this request. // If we already have a clipboard confirmation view up, we ignore this request.
// This shouldn't be possible... // This shouldn't be possible...
guard self.clipboardConfirmation == nil else { guard self.clipboardConfirmation == nil else {
Ghostty.App.completeClipboardRequest(surface, data: "", state: state, confirmed: true) Ghostty.App.completeClipboardRequest(surface, data: "", state: state, confirmed: true)
return return
} }
// Show our paste confirmation // Show our paste confirmation
self.clipboardConfirmation = ClipboardConfirmationController( self.clipboardConfirmation = ClipboardConfirmationController(
surface: surface, surface: surface,

View File

@ -10,20 +10,20 @@ class TerminalManager {
let controller: TerminalController let controller: TerminalController
let closePublisher: AnyCancellable let closePublisher: AnyCancellable
} }
let ghostty: Ghostty.App let ghostty: Ghostty.App
/// The currently focused surface of the main window. /// The currently focused surface of the main window.
var focusedSurface: Ghostty.SurfaceView? { mainWindow?.controller.focusedSurface } var focusedSurface: Ghostty.SurfaceView? { mainWindow?.controller.focusedSurface }
/// The set of windows we currently have. /// The set of windows we currently have.
var windows: [Window] = [] var windows: [Window] = []
// Keep track of the last point that our window was launched at so that new // Keep track of the last point that our window was launched at so that new
// windows "cascade" over each other and don't just launch directly on top // windows "cascade" over each other and don't just launch directly on top
// of each other. // of each other.
private static var lastCascadePoint = NSPoint(x: 0, y: 0) private static var lastCascadePoint = NSPoint(x: 0, y: 0)
/// Returns the main window of the managed window stack. If there is no window /// Returns the main window of the managed window stack. If there is no window
/// then an arbitrary window will be chosen. /// then an arbitrary window will be chosen.
private var mainWindow: Window? { private var mainWindow: Window? {
@ -32,14 +32,14 @@ class TerminalManager {
return window return window
} }
} }
// If we have no main window, just use the last window. // If we have no main window, just use the last window.
return windows.last return windows.last
} }
init(_ ghostty: Ghostty.App) { init(_ ghostty: Ghostty.App) {
self.ghostty = ghostty self.ghostty = ghostty
let center = NotificationCenter.default let center = NotificationCenter.default
center.addObserver( center.addObserver(
self, self,
@ -52,32 +52,32 @@ class TerminalManager {
name: Ghostty.Notification.ghosttyNewWindow, name: Ghostty.Notification.ghosttyNewWindow,
object: nil) object: nil)
} }
deinit { deinit {
let center = NotificationCenter.default let center = NotificationCenter.default
center.removeObserver(self) center.removeObserver(self)
} }
// MARK: - Window Management // MARK: - Window Management
/// Create a new terminal window. /// Create a new terminal window.
func newWindow(withBaseConfig base: Ghostty.SurfaceConfiguration? = nil) { func newWindow(withBaseConfig base: Ghostty.SurfaceConfiguration? = nil) {
let c = createWindow(withBaseConfig: base) let c = createWindow(withBaseConfig: base)
let window = c.window! let window = c.window!
// We want to go fullscreen if we're configured for new windows to go fullscreen // We want to go fullscreen if we're configured for new windows to go fullscreen
var toggleFullScreen = ghostty.config.windowFullscreen var toggleFullScreen = ghostty.config.windowFullscreen
// If the previous focused window prior to creating this window is fullscreen, // If the previous focused window prior to creating this window is fullscreen,
// then this window also becomes fullscreen. // then this window also becomes fullscreen.
if let parent = focusedSurface?.window, parent.styleMask.contains(.fullScreen) { if let parent = focusedSurface?.window, parent.styleMask.contains(.fullScreen) {
toggleFullScreen = true toggleFullScreen = true
} }
if (toggleFullScreen && !window.styleMask.contains(.fullScreen)) { if (toggleFullScreen && !window.styleMask.contains(.fullScreen)) {
window.toggleFullScreen(nil) window.toggleFullScreen(nil)
} }
// We're dispatching this async because otherwise the lastCascadePoint doesn't // We're dispatching this async because otherwise the lastCascadePoint doesn't
// take effect. Our best theory is there is some next-event-loop-tick logic // take effect. Our best theory is there is some next-event-loop-tick logic
// that Cocoa is doing that we need to be after. // that Cocoa is doing that we need to be after.
@ -86,11 +86,11 @@ class TerminalManager {
if (!window.styleMask.contains(.fullScreen)) { if (!window.styleMask.contains(.fullScreen)) {
Self.lastCascadePoint = window.cascadeTopLeft(from: Self.lastCascadePoint) Self.lastCascadePoint = window.cascadeTopLeft(from: Self.lastCascadePoint)
} }
c.showWindow(self) c.showWindow(self)
} }
} }
/// Creates a new tab in the current main window. If there are no windows, a window /// Creates a new tab in the current main window. If there are no windows, a window
/// is created. /// is created.
func newTab(withBaseConfig base: Ghostty.SurfaceConfiguration? = nil) { func newTab(withBaseConfig base: Ghostty.SurfaceConfiguration? = nil) {
@ -99,11 +99,11 @@ class TerminalManager {
newWindow(withBaseConfig: base) newWindow(withBaseConfig: base)
return return
} }
// Create a new window and add it to the parent // Create a new window and add it to the parent
newTab(to: parent, withBaseConfig: base) newTab(to: parent, withBaseConfig: base)
} }
private func newTab(to parent: NSWindow, withBaseConfig base: Ghostty.SurfaceConfiguration?) { private func newTab(to parent: NSWindow, withBaseConfig base: Ghostty.SurfaceConfiguration?) {
// If our parent is in non-native fullscreen, then new tabs do not work. // If our parent is in non-native fullscreen, then new tabs do not work.
// See: https://github.com/mitchellh/ghostty/issues/392 // See: https://github.com/mitchellh/ghostty/issues/392
@ -117,15 +117,15 @@ class TerminalManager {
alert.beginSheetModal(for: parent) alert.beginSheetModal(for: parent)
return return
} }
// Create a new window and add it to the parent // Create a new window and add it to the parent
let controller = createWindow(withBaseConfig: base) let controller = createWindow(withBaseConfig: base)
let window = controller.window! let window = controller.window!
// If the parent is miniaturized, then macOS exhibits really strange behaviors // If the parent is miniaturized, then macOS exhibits really strange behaviors
// so we have to bring it back out. // 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 // 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. // we need to remove it so we can set the correct order in the next line.
// If we don't do this, macOS gets really confused and the tabbedWindows // If we don't do this, macOS gets really confused and the tabbedWindows
@ -136,12 +136,12 @@ class TerminalManager {
if let tg = parent.tabGroup, tg.windows.firstIndex(of: window) != nil { if let tg = parent.tabGroup, tg.windows.firstIndex(of: window) != nil {
tg.removeWindow(window) tg.removeWindow(window)
} }
// Our windows start out invisible. We need to make it visible. If we // Our windows start out invisible. We need to make it visible. If we
// don't do this then various features such as window blur won't work because // don't do this then various features such as window blur won't work because
// the macOS APIs only work on a visible window. // the macOS APIs only work on a visible window.
controller.showWindow(self) controller.showWindow(self)
// Add the window to the tab group and show it. // Add the window to the tab group and show it.
switch ghostty.config.windowNewTabPosition { switch ghostty.config.windowNewTabPosition {
case "end": case "end":
@ -158,14 +158,14 @@ class TerminalManager {
} }
window.makeKeyAndOrderFront(self) window.makeKeyAndOrderFront(self)
// It takes an event loop cycle until the macOS tabGroup state becomes // It takes an event loop cycle until the macOS tabGroup state becomes
// consistent which causes our tab labeling to be off when the "+" button // consistent which causes our tab labeling to be off when the "+" button
// is used in the tab bar. This fixes that. If we can find a more robust // is used in the tab bar. This fixes that. If we can find a more robust
// solution we should do that. // solution we should do that.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { controller.relabelTabs() } DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { controller.relabelTabs() }
} }
/// Creates a window controller, adds it to our managed list, and returns it. /// Creates a window controller, adds it to our managed list, and returns it.
func createWindow(withBaseConfig base: Ghostty.SurfaceConfiguration? = nil, func createWindow(withBaseConfig base: Ghostty.SurfaceConfiguration? = nil,
withSurfaceTree tree: Ghostty.SplitNode? = nil) -> TerminalController { withSurfaceTree tree: Ghostty.SplitNode? = nil) -> TerminalController {
@ -181,25 +181,25 @@ class TerminalManager {
guard let c = window.windowController as? TerminalController else { return } guard let c = window.windowController as? TerminalController else { return }
self.removeWindow(c) self.removeWindow(c)
} }
// Keep track of every window we manage // Keep track of every window we manage
windows.append(Window( windows.append(Window(
controller: c, controller: c,
closePublisher: pubClose closePublisher: pubClose
)) ))
return c return c
} }
func removeWindow(_ controller: TerminalController) { func removeWindow(_ controller: TerminalController) {
// Remove it from our managed set // Remove it from our managed set
guard let idx = self.windows.firstIndex(where: { $0.controller == controller }) else { return } guard let idx = self.windows.firstIndex(where: { $0.controller == controller }) else { return }
let w = self.windows[idx] let w = self.windows[idx]
self.windows.remove(at: idx) self.windows.remove(at: idx)
// Ensure any publishers we have are cancelled // Ensure any publishers we have are cancelled
w.closePublisher.cancel() w.closePublisher.cancel()
// If we remove a window, we reset the cascade point to the key window so that // If we remove a window, we reset the cascade point to the key window so that
// the next window cascade's from that one. // the next window cascade's from that one.
if let focusedWindow = NSApplication.shared.keyWindow { if let focusedWindow = NSApplication.shared.keyWindow {
@ -210,19 +210,19 @@ class TerminalManager {
Self.lastCascadePoint = focusedWindow.cascadeTopLeft(from: NSZeroPoint) Self.lastCascadePoint = focusedWindow.cascadeTopLeft(from: NSZeroPoint)
return return
} }
// If we are the focused window, then we set the last cascade point to // If we are the focused window, then we set the last cascade point to
// our own frame so that it shows up in the same spot. // our own frame so that it shows up in the same spot.
let frame = focusedWindow.frame let frame = focusedWindow.frame
Self.lastCascadePoint = NSPoint(x: frame.minX, y: frame.maxY) Self.lastCascadePoint = NSPoint(x: frame.minX, y: frame.maxY)
} }
// I don't think we strictly have to do this but if a window is // I don't think we strictly have to do this but if a window is
// closed I want to make sure that the app state is invalided so // closed I want to make sure that the app state is invalided so
// we don't reopen closed windows. // we don't reopen closed windows.
NSApplication.shared.invalidateRestorableState() NSApplication.shared.invalidateRestorableState()
} }
/// Close all windows, asking for confirmation if necessary. /// Close all windows, asking for confirmation if necessary.
func closeAllWindows() { func closeAllWindows() {
var needsConfirm: Bool = false var needsConfirm: Bool = false
@ -232,15 +232,15 @@ class TerminalManager {
break break
} }
} }
if (!needsConfirm) { if (!needsConfirm) {
for w in self.windows { for w in self.windows {
w.controller.close() w.controller.close()
} }
return return
} }
// If we don't have a main window, we just close all windows because // If we don't have a main window, we just close all windows because
// we have no window to show the modal on top of. I'm sure there's a way // we have no window to show the modal on top of. I'm sure there's a way
// to do an app-level alert but I don't know how and this case should never // to do an app-level alert but I don't know how and this case should never
@ -249,10 +249,10 @@ class TerminalManager {
for w in self.windows { for w in self.windows {
w.controller.close() w.controller.close()
} }
return return
} }
// If we need confirmation by any, show one confirmation for all windows // If we need confirmation by any, show one confirmation for all windows
let alert = NSAlert() let alert = NSAlert()
alert.messageText = "Close All Windows?" alert.messageText = "Close All Windows?"
@ -268,29 +268,29 @@ class TerminalManager {
} }
}) })
} }
/// Relabels all the tabs with the proper keyboard shortcut. /// Relabels all the tabs with the proper keyboard shortcut.
func relabelAllTabs() { func relabelAllTabs() {
for w in windows { for w in windows {
w.controller.relabelTabs() w.controller.relabelTabs()
} }
} }
// MARK: - Notifications // MARK: - Notifications
@objc private func onNewWindow(notification: SwiftUI.Notification) { @objc private func onNewWindow(notification: SwiftUI.Notification) {
let configAny = notification.userInfo?[Ghostty.Notification.NewSurfaceConfigKey] let configAny = notification.userInfo?[Ghostty.Notification.NewSurfaceConfigKey]
let config = configAny as? Ghostty.SurfaceConfiguration let config = configAny as? Ghostty.SurfaceConfiguration
self.newWindow(withBaseConfig: config) self.newWindow(withBaseConfig: config)
} }
@objc private func onNewTab(notification: SwiftUI.Notification) { @objc private func onNewTab(notification: SwiftUI.Notification) {
guard let surfaceView = notification.object as? Ghostty.SurfaceView else { return } guard let surfaceView = notification.object as? Ghostty.SurfaceView else { return }
guard let window = surfaceView.window else { return } guard let window = surfaceView.window else { return }
let configAny = notification.userInfo?[Ghostty.Notification.NewSurfaceConfigKey] let configAny = notification.userInfo?[Ghostty.Notification.NewSurfaceConfigKey]
let config = configAny as? Ghostty.SurfaceConfiguration let config = configAny as? Ghostty.SurfaceConfiguration
self.newTab(to: window, withBaseConfig: config) self.newTab(to: window, withBaseConfig: config)
} }
} }

View File

@ -5,15 +5,15 @@ class TerminalRestorableState: Codable {
static let selfKey = "state" static let selfKey = "state"
static let versionKey = "version" static let versionKey = "version"
static let version: Int = 2 static let version: Int = 2
let focusedSurface: String? let focusedSurface: String?
let surfaceTree: Ghostty.SplitNode? let surfaceTree: Ghostty.SplitNode?
init(from controller: TerminalController) { init(from controller: TerminalController) {
self.focusedSurface = controller.focusedSurface?.uuid.uuidString self.focusedSurface = controller.focusedSurface?.uuid.uuidString
self.surfaceTree = controller.surfaceTree self.surfaceTree = controller.surfaceTree
} }
init?(coder aDecoder: NSCoder) { init?(coder aDecoder: NSCoder) {
// If the version doesn't match then we can't decode. In the future we can perform // If the version doesn't match then we can't decode. In the future we can perform
// version upgrading or something but for now we only have one version so we // version upgrading or something but for now we only have one version so we
@ -21,15 +21,15 @@ class TerminalRestorableState: Codable {
guard aDecoder.decodeInteger(forKey: Self.versionKey) == Self.version else { guard aDecoder.decodeInteger(forKey: Self.versionKey) == Self.version else {
return nil return nil
} }
guard let v = aDecoder.decodeObject(of: CodableBridge<Self>.self, forKey: Self.selfKey) else { guard let v = aDecoder.decodeObject(of: CodableBridge<Self>.self, forKey: Self.selfKey) else {
return nil return nil
} }
self.surfaceTree = v.value.surfaceTree self.surfaceTree = v.value.surfaceTree
self.focusedSurface = v.value.focusedSurface self.focusedSurface = v.value.focusedSurface
} }
func encode(with coder: NSCoder) { func encode(with coder: NSCoder) {
coder.encode(Self.version, forKey: Self.versionKey) coder.encode(Self.version, forKey: Self.versionKey)
coder.encode(CodableBridge(self), forKey: Self.selfKey) coder.encode(CodableBridge(self), forKey: Self.selfKey)
@ -56,27 +56,27 @@ class TerminalWindowRestoration: NSObject, NSWindowRestoration {
completionHandler(nil, TerminalRestoreError.identifierUnknown) completionHandler(nil, TerminalRestoreError.identifierUnknown)
return return
} }
// The app delegate is definitely setup by now. If it isn't our AppDelegate // The app delegate is definitely setup by now. If it isn't our AppDelegate
// then something is royally fucked up but protect against it anyhow. // then something is royally fucked up but protect against it anyhow.
guard let appDelegate = NSApplication.shared.delegate as? AppDelegate else { guard let appDelegate = NSApplication.shared.delegate as? AppDelegate else {
completionHandler(nil, TerminalRestoreError.delegateInvalid) completionHandler(nil, TerminalRestoreError.delegateInvalid)
return return
} }
// If our configuration is "never" then we never restore the state // If our configuration is "never" then we never restore the state
// no matter what. // no matter what.
if (appDelegate.terminalManager.ghostty.config.windowSaveState == "never") { if (appDelegate.terminalManager.ghostty.config.windowSaveState == "never") {
completionHandler(nil, nil) completionHandler(nil, nil)
return return
} }
// Decode the state. If we can't decode the state, then we can't restore. // Decode the state. If we can't decode the state, then we can't restore.
guard let state = TerminalRestorableState(coder: state) else { guard let state = TerminalRestorableState(coder: state) else {
completionHandler(nil, TerminalRestoreError.stateDecodeFailed) completionHandler(nil, TerminalRestoreError.stateDecodeFailed)
return return
} }
// The window creation has to go through our terminalManager so that it // The window creation has to go through our terminalManager so that it
// can be found for events from libghostty. This uses the low-level // can be found for events from libghostty. This uses the low-level
// createWindow so that AppKit can place the window wherever it should // createWindow so that AppKit can place the window wherever it should
@ -86,7 +86,7 @@ class TerminalWindowRestoration: NSObject, NSWindowRestoration {
completionHandler(nil, TerminalRestoreError.windowDidNotLoad) completionHandler(nil, TerminalRestoreError.windowDidNotLoad)
return return
} }
// Setup our restored state on the controller // Setup our restored state on the controller
if let focusedStr = state.focusedSurface, if let focusedStr = state.focusedSurface,
let focusedUUID = UUID(uuidString: focusedStr), let focusedUUID = UUID(uuidString: focusedStr),
@ -94,10 +94,10 @@ class TerminalWindowRestoration: NSObject, NSWindowRestoration {
c.focusedSurface = view c.focusedSurface = view
restoreFocus(to: view, inWindow: window) restoreFocus(to: view, inWindow: window)
} }
completionHandler(window, nil) completionHandler(window, nil)
} }
/// This restores the focus state of the surfaceview within the given window. When restoring, /// This restores the focus state of the surfaceview within the given window. When restoring,
/// the view isn't immediately attached to the window since we have to wait for SwiftUI to /// the view isn't immediately attached to the window since we have to wait for SwiftUI to
/// catch up. Therefore, we sit in an async loop waiting for the attachment to happen. /// catch up. Therefore, we sit in an async loop waiting for the attachment to happen.
@ -113,19 +113,19 @@ class TerminalWindowRestoration: NSObject, NSWindowRestoration {
} else { } else {
after = .now() + .milliseconds(50) after = .now() + .milliseconds(50)
} }
DispatchQueue.main.asyncAfter(deadline: after) { DispatchQueue.main.asyncAfter(deadline: after) {
// If the view is not attached to a window yet then we repeat. // If the view is not attached to a window yet then we repeat.
guard let viewWindow = to.window else { guard let viewWindow = to.window else {
restoreFocus(to: to, inWindow: inWindow, attempts: attempts + 1) restoreFocus(to: to, inWindow: inWindow, attempts: attempts + 1)
return return
} }
// If the view is attached to some other window, we give up // If the view is attached to some other window, we give up
guard viewWindow == inWindow else { return } guard viewWindow == inWindow else { return }
inWindow.makeFirstResponder(to) inWindow.makeFirstResponder(to)
// If the window is main, then we also make sure it comes forward. This // 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 // prevents a bug found in #1177 where sometimes on restore the windows
// would be behind other applications. // would be behind other applications.

View File

@ -4,12 +4,12 @@ import Cocoa
// in order to accommodate the titlebar tabs feature. // in order to accommodate the titlebar tabs feature.
class TerminalToolbar: NSToolbar, NSToolbarDelegate { class TerminalToolbar: NSToolbar, NSToolbarDelegate {
private let titleTextField = CenteredDynamicLabel(labelWithString: "👻 Ghostty") private let titleTextField = CenteredDynamicLabel(labelWithString: "👻 Ghostty")
var titleText: String { var titleText: String {
get { get {
titleTextField.stringValue titleTextField.stringValue
} }
set { set {
titleTextField.stringValue = newValue titleTextField.stringValue = newValue
} }
@ -27,16 +27,16 @@ class TerminalToolbar: NSToolbar, NSToolbarDelegate {
override init(identifier: NSToolbar.Identifier) { override init(identifier: NSToolbar.Identifier) {
super.init(identifier: identifier) super.init(identifier: identifier)
delegate = self delegate = self
if #available(macOS 13.0, *) { if #available(macOS 13.0, *) {
centeredItemIdentifiers.insert(.titleText) centeredItemIdentifiers.insert(.titleText)
} else { } else {
centeredItemIdentifier = .titleText centeredItemIdentifier = .titleText
} }
} }
func toolbar(_ toolbar: NSToolbar, func toolbar(_ toolbar: NSToolbar,
itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier,
willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? { willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? {
@ -68,11 +68,11 @@ class TerminalToolbar: NSToolbar, NSToolbarDelegate {
return item return item
} }
func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
return [.titleText, .flexibleSpace, .space, .resetZoom] return [.titleText, .flexibleSpace, .space, .resetZoom]
} }
func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
// These space items are here to ensure that the title remains centered when it starts // These space items are here to ensure that the title remains centered when it starts
// getting smaller than the max size so starts clipping. Lucky for us, two of the // getting smaller than the max size so starts clipping. Lucky for us, two of the
@ -88,11 +88,11 @@ fileprivate class CenteredDynamicLabel: NSTextField {
// Truncate the title when it gets too long, cutting it off with an ellipsis. // Truncate the title when it gets too long, cutting it off with an ellipsis.
cell?.truncatesLastVisibleLine = true cell?.truncatesLastVisibleLine = true
cell?.lineBreakMode = .byCharWrapping cell?.lineBreakMode = .byCharWrapping
// Make the text field as small as possible while fitting its text. // Make the text field as small as possible while fitting its text.
setContentHuggingPriority(.required, for: .horizontal) setContentHuggingPriority(.required, for: .horizontal)
cell?.alignment = .center cell?.alignment = .center
// We've changed some alignment settings, make sure the layout is updated immediately. // We've changed some alignment settings, make sure the layout is updated immediately.
needsLayout = true needsLayout = true
} }

View File

@ -7,13 +7,13 @@ import GhosttyKit
protocol TerminalViewDelegate: AnyObject { protocol TerminalViewDelegate: AnyObject {
/// Called when the currently focused surface changed. This can be nil. /// Called when the currently focused surface changed. This can be nil.
func focusedSurfaceDidChange(to: Ghostty.SurfaceView?) func focusedSurfaceDidChange(to: Ghostty.SurfaceView?)
/// The title of the terminal should change. /// The title of the terminal should change.
func titleDidChange(to: String) func titleDidChange(to: String)
/// The cell size changed. /// The cell size changed.
func cellSizeDidChange(to: NSSize) func cellSizeDidChange(to: NSSize)
/// The surface tree did change in some way, i.e. a split was added, removed, etc. This is /// The surface tree did change in some way, i.e. a split was added, removed, etc. This is
/// not called initially. /// not called initially.
func surfaceTreeDidChange() func surfaceTreeDidChange()
@ -41,35 +41,35 @@ protocol TerminalViewModel: ObservableObject {
/// The main terminal view. This terminal view supports splits. /// The main terminal view. This terminal view supports splits.
struct TerminalView<ViewModel: TerminalViewModel>: View { struct TerminalView<ViewModel: TerminalViewModel>: View {
@ObservedObject var ghostty: Ghostty.App @ObservedObject var ghostty: Ghostty.App
// The required view model // The required view model
@ObservedObject var viewModel: ViewModel @ObservedObject var viewModel: ViewModel
// An optional delegate to receive information about terminal changes. // An optional delegate to receive information about terminal changes.
weak var delegate: (any TerminalViewDelegate)? = nil weak var delegate: (any TerminalViewDelegate)? = nil
// This seems like a crutch after switching from SwiftUI to AppKit lifecycle. // This seems like a crutch after switching from SwiftUI to AppKit lifecycle.
@FocusState private var focused: Bool @FocusState private var focused: Bool
// Various state values sent back up from the currently focused terminals. // Various state values sent back up from the currently focused terminals.
@FocusedValue(\.ghosttySurfaceView) private var focusedSurface @FocusedValue(\.ghosttySurfaceView) private var focusedSurface
@FocusedValue(\.ghosttySurfaceTitle) private var surfaceTitle @FocusedValue(\.ghosttySurfaceTitle) private var surfaceTitle
@FocusedValue(\.ghosttySurfaceZoomed) private var zoomedSplit @FocusedValue(\.ghosttySurfaceZoomed) private var zoomedSplit
@FocusedValue(\.ghosttySurfaceCellSize) private var cellSize @FocusedValue(\.ghosttySurfaceCellSize) private var cellSize
// The title for our window // The title for our window
private var title: String { private var title: String {
var title = "👻" var title = "👻"
if let surfaceTitle = surfaceTitle { if let surfaceTitle = surfaceTitle {
if (surfaceTitle.count > 0) { if (surfaceTitle.count > 0) {
title = surfaceTitle title = surfaceTitle
} }
} }
return title return title
} }
var body: some View { var body: some View {
switch ghostty.readiness { switch ghostty.readiness {
case .loading: case .loading:
@ -83,7 +83,7 @@ struct TerminalView<ViewModel: TerminalViewModel>: View {
if (Ghostty.info.mode == GHOSTTY_BUILD_MODE_DEBUG) { if (Ghostty.info.mode == GHOSTTY_BUILD_MODE_DEBUG) {
DebugBuildWarningView() DebugBuildWarningView()
} }
Ghostty.TerminalSplit(node: $viewModel.surfaceTree) Ghostty.TerminalSplit(node: $viewModel.surfaceTree)
.environmentObject(ghostty) .environmentObject(ghostty)
.focused($focused) .focused($focused)
@ -114,14 +114,14 @@ struct TerminalView<ViewModel: TerminalViewModel>: View {
struct DebugBuildWarningView: View { struct DebugBuildWarningView: View {
@State private var isPopover = false @State private var isPopover = false
var body: some View { var body: some View {
HStack { HStack {
Spacer() Spacer()
Image(systemName: "exclamationmark.triangle.fill") Image(systemName: "exclamationmark.triangle.fill")
.foregroundColor(.yellow) .foregroundColor(.yellow)
Text("You're running a debug build of Ghostty! Performance will be degraded.") Text("You're running a debug build of Ghostty! Performance will be degraded.")
.padding(.all, 8) .padding(.all, 8)
.popover(isPresented: $isPopover, arrowEdge: .bottom) { .popover(isPresented: $isPopover, arrowEdge: .bottom) {
@ -132,7 +132,7 @@ struct DebugBuildWarningView: View {
""") """)
.padding(.all) .padding(.all)
} }
Spacer() Spacer()
} }
.background(Color(.windowBackgroundColor)) .background(Color(.windowBackgroundColor))

View File

@ -75,7 +75,7 @@ class TerminalWindow: NSWindow {
tab.attributedTitle = attributedTitle tab.attributedTitle = attributedTitle
} }
} }
// The window theme configuration from Ghostty. This is used to control some // The window theme configuration from Ghostty. This is used to control some
// behaviors that don't look quite right in certain situations. // behaviors that don't look quite right in certain situations.
var windowTheme: TerminalWindowTheme? var windowTheme: TerminalWindowTheme?
@ -92,7 +92,7 @@ class TerminalWindow: NSWindow {
if let tabGroup = self.tabGroup, tabGroup.windows.count < 2 { if let tabGroup = self.tabGroup, tabGroup.windows.count < 2 {
hideCustomTabBarViews() hideCustomTabBarViews()
} }
super.becomeKey() super.becomeKey()
updateNewTabButtonOpacity() updateNewTabButtonOpacity()
@ -168,10 +168,10 @@ class TerminalWindow: NSWindow {
hideTitleBarSeparators() hideTitleBarSeparators()
} }
} }
override func mergeAllWindows(_ sender: Any?) { override func mergeAllWindows(_ sender: Any?) {
super.mergeAllWindows(sender) super.mergeAllWindows(sender)
if let controller = self.windowController as? TerminalController { if let controller = self.windowController as? TerminalController {
// It takes an event loop cycle to merge all the windows so we set a // It takes an event loop cycle to merge all the windows so we set a
// short timer to relabel the tabs (issue #1902) // short timer to relabel the tabs (issue #1902)
@ -185,15 +185,15 @@ class TerminalWindow: NSWindow {
var hasStyledTabs: Bool { var hasStyledTabs: Bool {
// If we have titlebar tabs then we always style. // If we have titlebar tabs then we always style.
guard !titlebarTabs else { return true } guard !titlebarTabs else { return true }
// We style the tabs if they're transparent // We style the tabs if they're transparent
return transparentTabs return transparentTabs
} }
// Set to true if the background color should bleed through the titlebar/tab bar. // Set to true if the background color should bleed through the titlebar/tab bar.
// This only applies to non-titlebar tabs. // This only applies to non-titlebar tabs.
var transparentTabs: Bool = false var transparentTabs: Bool = false
var hasVeryDarkBackground: Bool { var hasVeryDarkBackground: Bool {
backgroundColor.luminance < 0.05 backgroundColor.luminance < 0.05
} }
@ -406,7 +406,7 @@ class TerminalWindow: NSWindow {
// MARK: - Titlebar Tabs // MARK: - Titlebar Tabs
private var windowButtonsBackdrop: WindowButtonsBackdropView? = nil private var windowButtonsBackdrop: WindowButtonsBackdropView? = nil
private var windowDragHandle: WindowDragView? = nil private var windowDragHandle: WindowDragView? = nil
// The tab bar controller ID from macOS // The tab bar controller ID from macOS
@ -459,27 +459,27 @@ class TerminalWindow: NSWindow {
childViewController.layoutAttribute == .bottom || childViewController.layoutAttribute == .bottom ||
childViewController.identifier == Self.TabBarController childViewController.identifier == Self.TabBarController
) )
if (isTabBar) { if (isTabBar) {
// Ensure it has the right layoutAttribute to force it next to our titlebar // Ensure it has the right layoutAttribute to force it next to our titlebar
childViewController.layoutAttribute = .right childViewController.layoutAttribute = .right
// If we don't set titleVisibility to hidden here, the toolbar will display a // If we don't set titleVisibility to hidden here, the toolbar will display a
// "collapsed items" indicator which interferes with the tab bar. // "collapsed items" indicator which interferes with the tab bar.
titleVisibility = .hidden titleVisibility = .hidden
// Mark the controller for future reference so we can easily find it. Otherwise // Mark the controller for future reference so we can easily find it. Otherwise
// the tab bar has no ID by default. // the tab bar has no ID by default.
childViewController.identifier = Self.TabBarController childViewController.identifier = Self.TabBarController
} }
super.addTitlebarAccessoryViewController(childViewController) super.addTitlebarAccessoryViewController(childViewController)
if (isTabBar) { if (isTabBar) {
pushTabsToTitlebar(childViewController) pushTabsToTitlebar(childViewController)
} }
} }
override func removeTitlebarAccessoryViewController(at index: Int) { override func removeTitlebarAccessoryViewController(at index: Int) {
let isTabBar = titlebarAccessoryViewControllers[index].identifier == Self.TabBarController let isTabBar = titlebarAccessoryViewControllers[index].identifier == Self.TabBarController
super.removeTitlebarAccessoryViewController(at: index) super.removeTitlebarAccessoryViewController(at: index)
@ -487,16 +487,16 @@ class TerminalWindow: NSWindow {
hideCustomTabBarViews() hideCustomTabBarViews()
} }
} }
// To be called immediately after the tab bar is disabled. // To be called immediately after the tab bar is disabled.
private func hideCustomTabBarViews() { private func hideCustomTabBarViews() {
// Hide the window buttons backdrop. // Hide the window buttons backdrop.
windowButtonsBackdrop?.isHidden = true windowButtonsBackdrop?.isHidden = true
// Hide the window drag handle. // Hide the window drag handle.
windowDragHandle?.isHidden = true windowDragHandle?.isHidden = true
} }
private func pushTabsToTitlebar(_ tabBarController: NSTitlebarAccessoryViewController) { private func pushTabsToTitlebar(_ tabBarController: NSTitlebarAccessoryViewController) {
let accessoryView = tabBarController.view let accessoryView = tabBarController.view
guard let accessoryClipView = accessoryView.superview else { return } guard let accessoryClipView = accessoryView.superview else { return }
@ -508,23 +508,23 @@ class TerminalWindow: NSWindow {
addWindowButtonsBackdrop(titlebarView: titlebarView, toolbarView: toolbarView) addWindowButtonsBackdrop(titlebarView: titlebarView, toolbarView: toolbarView)
guard let windowButtonsBackdrop = windowButtonsBackdrop else { return } guard let windowButtonsBackdrop = windowButtonsBackdrop else { return }
addWindowDragHandle(titlebarView: titlebarView, toolbarView: toolbarView) addWindowDragHandle(titlebarView: titlebarView, toolbarView: toolbarView)
accessoryClipView.translatesAutoresizingMaskIntoConstraints = false accessoryClipView.translatesAutoresizingMaskIntoConstraints = false
accessoryClipView.leftAnchor.constraint(equalTo: windowButtonsBackdrop.rightAnchor).isActive = true accessoryClipView.leftAnchor.constraint(equalTo: windowButtonsBackdrop.rightAnchor).isActive = true
accessoryClipView.rightAnchor.constraint(equalTo: toolbarView.rightAnchor).isActive = true accessoryClipView.rightAnchor.constraint(equalTo: toolbarView.rightAnchor).isActive = true
accessoryClipView.topAnchor.constraint(equalTo: toolbarView.topAnchor).isActive = true accessoryClipView.topAnchor.constraint(equalTo: toolbarView.topAnchor).isActive = true
accessoryClipView.heightAnchor.constraint(equalTo: toolbarView.heightAnchor).isActive = true accessoryClipView.heightAnchor.constraint(equalTo: toolbarView.heightAnchor).isActive = true
accessoryClipView.needsLayout = true accessoryClipView.needsLayout = true
accessoryView.translatesAutoresizingMaskIntoConstraints = false accessoryView.translatesAutoresizingMaskIntoConstraints = false
accessoryView.leftAnchor.constraint(equalTo: accessoryClipView.leftAnchor).isActive = true accessoryView.leftAnchor.constraint(equalTo: accessoryClipView.leftAnchor).isActive = true
accessoryView.rightAnchor.constraint(equalTo: accessoryClipView.rightAnchor).isActive = true accessoryView.rightAnchor.constraint(equalTo: accessoryClipView.rightAnchor).isActive = true
accessoryView.topAnchor.constraint(equalTo: accessoryClipView.topAnchor).isActive = true accessoryView.topAnchor.constraint(equalTo: accessoryClipView.topAnchor).isActive = true
accessoryView.heightAnchor.constraint(equalTo: accessoryClipView.heightAnchor).isActive = true accessoryView.heightAnchor.constraint(equalTo: accessoryClipView.heightAnchor).isActive = true
accessoryView.needsLayout = true accessoryView.needsLayout = true
// This is a horrible hack. During the transition while things are resizing to make room for // This is a horrible hack. During the transition while things are resizing to make room for
// new tabs or expand existing tabs to fill the empty space after one is closed, the centering // new tabs or expand existing tabs to fill the empty space after one is closed, the centering
// of the tab titles can't be properly calculated, so we wait for 0.2 seconds and then mark // of the tab titles can't be properly calculated, so we wait for 0.2 seconds and then mark
@ -541,7 +541,7 @@ class TerminalWindow: NSWindow {
let view = WindowButtonsBackdropView(window: self) let view = WindowButtonsBackdropView(window: self)
view.identifier = NSUserInterfaceItemIdentifier("_windowButtonsBackdrop") view.identifier = NSUserInterfaceItemIdentifier("_windowButtonsBackdrop")
titlebarView.addSubview(view) titlebarView.addSubview(view)
view.translatesAutoresizingMaskIntoConstraints = false view.translatesAutoresizingMaskIntoConstraints = false
view.leftAnchor.constraint(equalTo: toolbarView.leftAnchor).isActive = true view.leftAnchor.constraint(equalTo: toolbarView.leftAnchor).isActive = true
view.rightAnchor.constraint(equalTo: toolbarView.leftAnchor, constant: 78).isActive = true view.rightAnchor.constraint(equalTo: toolbarView.leftAnchor, constant: 78).isActive = true
@ -550,7 +550,7 @@ class TerminalWindow: NSWindow {
windowButtonsBackdrop = view windowButtonsBackdrop = view
} }
private func addWindowDragHandle(titlebarView: NSView, toolbarView: NSView) { private func addWindowDragHandle(titlebarView: NSView, toolbarView: NSView) {
// If we already made the view, just make sure it's unhidden and correctly placed as a subview. // If we already made the view, just make sure it's unhidden and correctly placed as a subview.
if let view = windowDragHandle { if let view = windowDragHandle {
@ -563,7 +563,7 @@ class TerminalWindow: NSWindow {
view.bottomAnchor.constraint(equalTo: toolbarView.topAnchor, constant: 12).isActive = true view.bottomAnchor.constraint(equalTo: toolbarView.topAnchor, constant: 12).isActive = true
return return
} }
let view = WindowDragView() let view = WindowDragView()
view.identifier = NSUserInterfaceItemIdentifier("_windowDragHandle") view.identifier = NSUserInterfaceItemIdentifier("_windowDragHandle")
titlebarView.superview?.addSubview(view) titlebarView.superview?.addSubview(view)
@ -572,10 +572,10 @@ class TerminalWindow: NSWindow {
view.rightAnchor.constraint(equalTo: toolbarView.rightAnchor).isActive = true view.rightAnchor.constraint(equalTo: toolbarView.rightAnchor).isActive = true
view.topAnchor.constraint(equalTo: toolbarView.topAnchor).isActive = true view.topAnchor.constraint(equalTo: toolbarView.topAnchor).isActive = true
view.bottomAnchor.constraint(equalTo: toolbarView.topAnchor, constant: 12).isActive = true view.bottomAnchor.constraint(equalTo: toolbarView.topAnchor, constant: 12).isActive = true
windowDragHandle = view windowDragHandle = view
} }
// This forces this view and all subviews to update layout and redraw. This is // This forces this view and all subviews to update layout and redraw. This is
// a hack (see the caller). // a hack (see the caller).
private func markHierarchyForLayout(_ view: NSView) { private func markHierarchyForLayout(_ view: NSView) {
@ -600,19 +600,19 @@ fileprivate class WindowDragView: NSView {
super.mouseDown(with: event) super.mouseDown(with: event)
} }
} }
override public func mouseEntered(with event: NSEvent) { override public func mouseEntered(with event: NSEvent) {
super.mouseEntered(with: event) super.mouseEntered(with: event)
window?.disableCursorRects() window?.disableCursorRects()
NSCursor.openHand.set() NSCursor.openHand.set()
} }
override func mouseExited(with event: NSEvent) { override func mouseExited(with event: NSEvent) {
super.mouseExited(with: event) super.mouseExited(with: event)
window?.enableCursorRects() window?.enableCursorRects()
NSCursor.arrow.set() NSCursor.arrow.set()
} }
override func resetCursorRects() { override func resetCursorRects() {
addCursorRect(bounds, cursor: .openHand) addCursorRect(bounds, cursor: .openHand)
} }

View File

@ -9,7 +9,7 @@ class UpdaterDelegate: NSObject, SPUUpdaterDelegate {
// tip appcast URL since it is all we support. // tip appcast URL since it is all we support.
return "https://tip.files.ghostty.dev/appcast.xml" return "https://tip.files.ghostty.dev/appcast.xml"
} }
func updaterWillRelaunchApplication(_ updater: SPUUpdater) { func updaterWillRelaunchApplication(_ updater: SPUUpdater) {
// When the updater is relaunching the application we want to get macOS // When the updater is relaunching the application we want to get macOS
// to invalidate and re-encode all of our restorable state so that when // to invalidate and re-encode all of our restorable state so that when

View File

@ -5,7 +5,7 @@ import GhosttyKit
protocol GhosttyAppDelegate: AnyObject { protocol GhosttyAppDelegate: AnyObject {
/// Called when the configuration did finish reloading. /// Called when the configuration did finish reloading.
func configDidReload(_ app: Ghostty.App) func configDidReload(_ app: Ghostty.App)
#if os(macOS) #if os(macOS)
/// Called when a callback needs access to a specific surface. This should return nil /// Called when a callback needs access to a specific surface. This should return nil
/// when the surface is no longer valid. /// when the surface is no longer valid.
@ -20,18 +20,18 @@ extension Ghostty {
enum Readiness: String { enum Readiness: String {
case loading, error, ready case loading, error, ready
} }
/// Optional delegate /// Optional delegate
weak var delegate: GhosttyAppDelegate? weak var delegate: GhosttyAppDelegate?
/// The readiness value of the state. /// The readiness value of the state.
@Published var readiness: Readiness = .loading @Published var readiness: Readiness = .loading
/// The global app configuration. This defines the app level configuration plus any behavior /// The global app configuration. This defines the app level configuration plus any behavior
/// for new windows, tabs, etc. Note that when creating a new window, it may inherit some /// for new windows, tabs, etc. Note that when creating a new window, it may inherit some
/// configuration (i.e. font size) from the previously focused window. This would override this. /// configuration (i.e. font size) from the previously focused window. This would override this.
@Published private(set) var config: Config @Published private(set) var config: Config
/// The ghostty app instance. We only have one of these for the entire app, although I guess /// 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... /// 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? = nil {
@ -40,13 +40,13 @@ extension Ghostty {
ghostty_app_free(old) ghostty_app_free(old)
} }
} }
/// True if we need to confirm before quitting. /// True if we need to confirm before quitting.
var needsConfirmQuit: Bool { var needsConfirmQuit: Bool {
guard let app = app else { return false } guard let app = app else { return false }
return ghostty_app_needs_confirm_quit(app) return ghostty_app_needs_confirm_quit(app)
} }
init() { init() {
// Initialize ghostty global state. This happens once per process. // Initialize ghostty global state. This happens once per process.
if ghostty_init() != GHOSTTY_SUCCESS { if ghostty_init() != GHOSTTY_SUCCESS {
@ -60,7 +60,7 @@ extension Ghostty {
readiness = .error readiness = .error
return return
} }
// Create our "runtime" config. The "runtime" is the configuration that ghostty // Create our "runtime" config. The "runtime" is the configuration that ghostty
// uses to interface with the application runtime environment. // uses to interface with the application runtime environment.
var runtime_cfg = ghostty_runtime_config_s( var runtime_cfg = ghostty_runtime_config_s(
@ -96,7 +96,7 @@ extension Ghostty {
update_renderer_health_cb: { userdata, health in App.updateRendererHealth(userdata, health: health) }, update_renderer_health_cb: { userdata, health in App.updateRendererHealth(userdata, health: health) },
mouse_over_link_cb: { userdata, ptr, len in App.mouseOverLink(userdata, uri: ptr, len: len) } mouse_over_link_cb: { userdata, ptr, len in App.mouseOverLink(userdata, uri: ptr, len: len) }
) )
// Create the ghostty app. // Create the ghostty app.
guard let app = ghostty_app_new(&runtime_cfg, config.config) else { guard let app = ghostty_app_new(&runtime_cfg, config.config) else {
logger.critical("ghostty_app_new failed") logger.critical("ghostty_app_new failed")
@ -104,7 +104,7 @@ extension Ghostty {
return return
} }
self.app = app self.app = app
#if os(macOS) #if os(macOS)
// Subscribe to notifications for keyboard layout change so that we can update Ghostty. // Subscribe to notifications for keyboard layout change so that we can update Ghostty.
NotificationCenter.default.addObserver( NotificationCenter.default.addObserver(
@ -113,14 +113,14 @@ extension Ghostty {
name: NSTextInputContext.keyboardSelectionDidChangeNotification, name: NSTextInputContext.keyboardSelectionDidChangeNotification,
object: nil) object: nil)
#endif #endif
self.readiness = .ready self.readiness = .ready
} }
deinit { deinit {
// This will force the didSet callbacks to run which free. // This will force the didSet callbacks to run which free.
self.app = nil self.app = nil
#if os(macOS) #if os(macOS)
// Remove our observer // Remove our observer
NotificationCenter.default.removeObserver( NotificationCenter.default.removeObserver(
@ -129,16 +129,16 @@ extension Ghostty {
object: nil) object: nil)
#endif #endif
} }
// MARK: App Operations // MARK: App Operations
func appTick() { func appTick() {
guard let app = self.app else { return } guard let app = self.app else { return }
// Tick our app, which lets us know if we want to quit // Tick our app, which lets us know if we want to quit
let exit = ghostty_app_tick(app) let exit = ghostty_app_tick(app)
if (!exit) { return } if (!exit) { return }
// On iOS, applications do not terminate programmatically like they do // On iOS, applications do not terminate programmatically like they do
// on macOS. On iOS, applications are only terminated when a user physically // on macOS. On iOS, applications are only terminated when a user physically
// closes the application (i.e. going to the home screen). If we request // closes the application (i.e. going to the home screen). If we request
@ -152,7 +152,7 @@ extension Ghostty {
NSApplication.shared.terminate(nil) NSApplication.shared.terminate(nil)
#endif #endif
} }
func openConfig() { func openConfig() {
guard let app = self.app else { return } guard let app = self.app else { return }
ghostty_app_open_config(app) ghostty_app_open_config(app)
@ -162,7 +162,7 @@ extension Ghostty {
guard let app = self.app else { return } guard let app = self.app else { return }
ghostty_app_reload_config(app) ghostty_app_reload_config(app)
} }
/// Request that the given surface is closed. This will trigger the full normal surface close event /// Request that the given surface is closed. This will trigger the full normal surface close event
/// cycle which will call our close surface callback. /// cycle which will call our close surface callback.
func requestClose(surface: ghostty_surface_t) { func requestClose(surface: ghostty_surface_t) {
@ -205,14 +205,14 @@ extension Ghostty {
logger.warning("action failed action=\(action)") logger.warning("action failed action=\(action)")
} }
} }
func toggleFullscreen(surface: ghostty_surface_t) { func toggleFullscreen(surface: ghostty_surface_t) {
let action = "toggle_fullscreen" let action = "toggle_fullscreen"
if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) { if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) {
logger.warning("action failed action=\(action)") logger.warning("action failed action=\(action)")
} }
} }
enum FontSizeModification { enum FontSizeModification {
case increase(Int) case increase(Int)
case decrease(Int) case decrease(Int)
@ -233,24 +233,24 @@ extension Ghostty {
logger.warning("action failed action=\(action)") logger.warning("action failed action=\(action)")
} }
} }
func toggleTerminalInspector(surface: ghostty_surface_t) { func toggleTerminalInspector(surface: ghostty_surface_t) {
let action = "inspector:toggle" let action = "inspector:toggle"
if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) { if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) {
logger.warning("action failed action=\(action)") logger.warning("action failed action=\(action)")
} }
} }
func resetTerminal(surface: ghostty_surface_t) { func resetTerminal(surface: ghostty_surface_t) {
let action = "reset" let action = "reset"
if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) { if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) {
logger.warning("action failed action=\(action)") logger.warning("action failed action=\(action)")
} }
} }
#if os(iOS) #if os(iOS)
// MARK: Ghostty Callbacks (iOS) // MARK: Ghostty Callbacks (iOS)
static func wakeup(_ userdata: UnsafeMutableRawPointer?) {} static func wakeup(_ userdata: UnsafeMutableRawPointer?) {}
static func reloadConfig(_ userdata: UnsafeMutableRawPointer?) -> ghostty_config_t? { return nil } static func reloadConfig(_ userdata: UnsafeMutableRawPointer?) -> ghostty_config_t? { return nil }
static func openConfig(_ userdata: UnsafeMutableRawPointer?) {} static func openConfig(_ userdata: UnsafeMutableRawPointer?) {}
@ -262,27 +262,27 @@ extension Ghostty {
location: ghostty_clipboard_e, location: ghostty_clipboard_e,
state: UnsafeMutableRawPointer? state: UnsafeMutableRawPointer?
) {} ) {}
static func confirmReadClipboard( static func confirmReadClipboard(
_ userdata: UnsafeMutableRawPointer?, _ userdata: UnsafeMutableRawPointer?,
string: UnsafePointer<CChar>?, string: UnsafePointer<CChar>?,
state: UnsafeMutableRawPointer?, state: UnsafeMutableRawPointer?,
request: ghostty_clipboard_request_e request: ghostty_clipboard_request_e
) {} ) {}
static func writeClipboard( static func writeClipboard(
_ userdata: UnsafeMutableRawPointer?, _ userdata: UnsafeMutableRawPointer?,
string: UnsafePointer<CChar>?, string: UnsafePointer<CChar>?,
location: ghostty_clipboard_e, location: ghostty_clipboard_e,
confirm: Bool confirm: Bool
) {} ) {}
static func newSplit( static func newSplit(
_ userdata: UnsafeMutableRawPointer?, _ userdata: UnsafeMutableRawPointer?,
direction: ghostty_split_direction_e, direction: ghostty_split_direction_e,
config: ghostty_surface_config_s config: ghostty_surface_config_s
) {} ) {}
static func newTab(_ userdata: UnsafeMutableRawPointer?, config: ghostty_surface_config_s) {} static func newTab(_ userdata: UnsafeMutableRawPointer?, config: ghostty_surface_config_s) {}
static func newWindow(_ userdata: UnsafeMutableRawPointer?, config: ghostty_surface_config_s) {} static func newWindow(_ userdata: UnsafeMutableRawPointer?, config: ghostty_surface_config_s) {}
static func controlInspector(_ userdata: UnsafeMutableRawPointer?, mode: ghostty_inspector_mode_e) {} static func controlInspector(_ userdata: UnsafeMutableRawPointer?, mode: ghostty_inspector_mode_e) {}
@ -300,18 +300,18 @@ extension Ghostty {
static func updateRendererHealth(_ userdata: UnsafeMutableRawPointer?, health: ghostty_renderer_health_e) {} static func updateRendererHealth(_ userdata: UnsafeMutableRawPointer?, health: ghostty_renderer_health_e) {}
static func mouseOverLink(_ userdata: UnsafeMutableRawPointer?, uri: UnsafePointer<CChar>?, len: Int) {} static func mouseOverLink(_ userdata: UnsafeMutableRawPointer?, uri: UnsafePointer<CChar>?, len: Int) {}
#endif #endif
#if os(macOS) #if os(macOS)
// MARK: Notifications // MARK: Notifications
// Called when the selected keyboard changes. We have to notify Ghostty so that // Called when the selected keyboard changes. We have to notify Ghostty so that
// it can reload the keyboard mapping for input. // it can reload the keyboard mapping for input.
@objc private func keyboardSelectionDidChange(notification: NSNotification) { @objc private func keyboardSelectionDidChange(notification: NSNotification) {
guard let app = self.app else { return } guard let app = self.app else { return }
ghostty_app_keyboard_changed(app) ghostty_app_keyboard_changed(app)
} }
// MARK: Ghostty Callbacks (macOS) // MARK: Ghostty Callbacks (macOS)
static func newSplit(_ userdata: UnsafeMutableRawPointer?, direction: ghostty_split_direction_e, config: ghostty_surface_config_s) { static func newSplit(_ userdata: UnsafeMutableRawPointer?, direction: ghostty_split_direction_e, config: ghostty_surface_config_s) {
@ -384,17 +384,17 @@ extension Ghostty {
// to leak "state". // to leak "state".
let surfaceView = self.surfaceUserdata(from: userdata) let surfaceView = self.surfaceUserdata(from: userdata)
guard let surface = surfaceView.surface else { return } guard let surface = surfaceView.surface else { return }
// We only support the standard clipboard // We only support the standard clipboard
if (location != GHOSTTY_CLIPBOARD_STANDARD) { if (location != GHOSTTY_CLIPBOARD_STANDARD) {
return completeClipboardRequest(surface, data: "", state: state) return completeClipboardRequest(surface, data: "", state: state)
} }
// Get our string // Get our string
let str = NSPasteboard.general.getOpinionatedStringContents() ?? "" let str = NSPasteboard.general.getOpinionatedStringContents() ?? ""
completeClipboardRequest(surface, data: str, state: state) completeClipboardRequest(surface, data: str, state: state)
} }
static func confirmReadClipboard( static func confirmReadClipboard(
_ userdata: UnsafeMutableRawPointer?, _ userdata: UnsafeMutableRawPointer?,
string: UnsafePointer<CChar>?, string: UnsafePointer<CChar>?,
@ -414,7 +414,7 @@ extension Ghostty {
] ]
) )
} }
static func completeClipboardRequest( static func completeClipboardRequest(
_ surface: ghostty_surface_t, _ surface: ghostty_surface_t,
data: String, data: String,
@ -439,7 +439,7 @@ extension Ghostty {
pb.setString(valueStr, forType: .string) pb.setString(valueStr, forType: .string)
return return
} }
NotificationCenter.default.post( NotificationCenter.default.post(
name: Notification.confirmClipboard, name: Notification.confirmClipboard,
object: surface, object: surface,
@ -483,7 +483,7 @@ extension Ghostty {
// standpoint since we don't do this much. // standpoint since we don't do this much.
DispatchQueue.main.async { state.appTick() } DispatchQueue.main.async { state.appTick() }
} }
static func renderInspector(_ userdata: UnsafeMutableRawPointer?) { static func renderInspector(_ userdata: UnsafeMutableRawPointer?) {
let surface = self.surfaceUserdata(from: userdata) let surface = self.surfaceUserdata(from: userdata)
NotificationCenter.default.post( NotificationCenter.default.post(
@ -520,7 +520,7 @@ extension Ghostty {
] ]
) )
} }
static func setInitialWindowSize(_ userdata: UnsafeMutableRawPointer?, width: UInt32, height: UInt32) { static func setInitialWindowSize(_ userdata: UnsafeMutableRawPointer?, width: UInt32, height: UInt32) {
// We need a window to set the frame // We need a window to set the frame
let surfaceView = self.surfaceUserdata(from: userdata) let surfaceView = self.surfaceUserdata(from: userdata)
@ -532,14 +532,14 @@ extension Ghostty {
let backingSize = NSSize(width: Double(width), height: Double(height)) let backingSize = NSSize(width: Double(width), height: Double(height))
surfaceView.cellSize = surfaceView.convertFromBacking(backingSize) surfaceView.cellSize = surfaceView.convertFromBacking(backingSize)
} }
static func mouseOverLink(_ userdata: UnsafeMutableRawPointer?, uri: UnsafePointer<CChar>?, len: Int) { static func mouseOverLink(_ userdata: UnsafeMutableRawPointer?, uri: UnsafePointer<CChar>?, len: Int) {
let surfaceView = self.surfaceUserdata(from: userdata) let surfaceView = self.surfaceUserdata(from: userdata)
guard len > 0 else { guard len > 0 else {
surfaceView.hoverUrl = nil surfaceView.hoverUrl = nil
return return
} }
let buffer = Data(bytes: uri!, count: len) let buffer = Data(bytes: uri!, count: len)
surfaceView.hoverUrl = String(data: buffer, encoding: .utf8) surfaceView.hoverUrl = String(data: buffer, encoding: .utf8)
} }
@ -593,7 +593,7 @@ extension Ghostty {
static func newTab(_ userdata: UnsafeMutableRawPointer?, config: ghostty_surface_config_s) { static func newTab(_ userdata: UnsafeMutableRawPointer?, config: ghostty_surface_config_s) {
let surface = self.surfaceUserdata(from: userdata) let surface = self.surfaceUserdata(from: userdata)
guard let appState = self.appState(fromView: surface) else { return } guard let appState = self.appState(fromView: surface) else { return }
guard appState.config.windowDecorations else { guard appState.config.windowDecorations else {
let alert = NSAlert() let alert = NSAlert()
@ -604,7 +604,7 @@ extension Ghostty {
_ = alert.runModal() _ = alert.runModal()
return return
} }
NotificationCenter.default.post( NotificationCenter.default.post(
name: Notification.ghosttyNewTab, name: Notification.ghosttyNewTab,
object: surface, object: surface,
@ -625,18 +625,18 @@ extension Ghostty {
] ]
) )
} }
static func controlInspector(_ userdata: UnsafeMutableRawPointer?, mode: ghostty_inspector_mode_e) { static func controlInspector(_ userdata: UnsafeMutableRawPointer?, mode: ghostty_inspector_mode_e) {
let surface = self.surfaceUserdata(from: userdata) let surface = self.surfaceUserdata(from: userdata)
NotificationCenter.default.post(name: Notification.didControlInspector, object: surface, userInfo: [ NotificationCenter.default.post(name: Notification.didControlInspector, object: surface, userInfo: [
"mode": mode, "mode": mode,
]) ])
} }
static func updateRendererHealth(_ userdata: UnsafeMutableRawPointer?, health: ghostty_renderer_health_e) { static func updateRendererHealth(_ userdata: UnsafeMutableRawPointer?, health: ghostty_renderer_health_e) {
let surface = self.surfaceUserdata(from: userdata) let surface = self.surfaceUserdata(from: userdata)
NotificationCenter.default.post( NotificationCenter.default.post(
name: Notification.didUpdateRendererHealth, name: Notification.didUpdateRendererHealth,
object: surface, object: surface,
userInfo: [ userInfo: [
"health": health, "health": health,
@ -656,7 +656,7 @@ extension Ghostty {
static private func surfaceUserdata(from userdata: UnsafeMutableRawPointer?) -> SurfaceView { static private func surfaceUserdata(from userdata: UnsafeMutableRawPointer?) -> SurfaceView {
return Unmanaged<SurfaceView>.fromOpaque(userdata!).takeUnretainedValue() return Unmanaged<SurfaceView>.fromOpaque(userdata!).takeUnretainedValue()
} }
#endif #endif
} }
} }

View File

@ -14,14 +14,14 @@ extension Ghostty {
ghostty_config_free(old) ghostty_config_free(old)
} }
} }
/// True if the configuration is loaded /// True if the configuration is loaded
var loaded: Bool { config != nil } var loaded: Bool { config != nil }
/// Return the errors found while loading the configuration. /// Return the errors found while loading the configuration.
var errors: [String] { var errors: [String] {
guard let cfg = self.config else { return [] } guard let cfg = self.config else { return [] }
var errors: [String] = []; var errors: [String] = [];
let errCount = ghostty_config_errors_count(cfg) let errCount = ghostty_config_errors_count(cfg)
for i in 0..<errCount { for i in 0..<errCount {
@ -29,20 +29,20 @@ extension Ghostty {
let message = String(cString: err.message) let message = String(cString: err.message)
errors.append(message) errors.append(message)
} }
return errors return errors
} }
init() { init() {
if let cfg = Self.loadConfig() { if let cfg = Self.loadConfig() {
self.config = cfg self.config = cfg
} }
} }
deinit { deinit {
self.config = nil self.config = nil
} }
/// Initializes a new configuration and loads all the values. /// Initializes a new configuration and loads all the values.
static private func loadConfig() -> ghostty_config_t? { static private func loadConfig() -> ghostty_config_t? {
// Initialize the global configuration. // Initialize the global configuration.
@ -50,7 +50,7 @@ extension Ghostty {
logger.critical("ghostty_config_new failed") logger.critical("ghostty_config_new failed")
return nil return nil
} }
// Load our configuration from files, CLI args, and then any referenced files. // Load our configuration from files, CLI args, and then any referenced files.
// We only do this on macOS because other Apple platforms do not have the // We only do this on macOS because other Apple platforms do not have the
// same filesystem concept. // same filesystem concept.
@ -59,14 +59,14 @@ extension Ghostty {
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 #endif
// TODO: we'd probably do some config loading here... for now we'd // TODO: we'd probably do some config loading here... for now we'd
// have to do this synchronously. When we support config updating we can do // have to do this synchronously. When we support config updating we can do
// this async and update later. // this async and update later.
// Finalize will make our defaults available. // Finalize will make our defaults available.
ghostty_config_finalize(cfg) ghostty_config_finalize(cfg)
// Log any configuration errors. These will be automatically shown in a // Log any configuration errors. These will be automatically shown in a
// pop-up window too. // pop-up window too.
let errCount = ghostty_config_errors_count(cfg) let errCount = ghostty_config_errors_count(cfg)
@ -80,32 +80,32 @@ extension Ghostty {
logger.warning("config error: \(message)") logger.warning("config error: \(message)")
} }
} }
return cfg return cfg
} }
#if os(macOS) #if os(macOS)
// MARK: - Keybindings // MARK: - Keybindings
/// A convenience struct that has the key + modifiers for some keybinding. /// A convenience struct that has the key + modifiers for some keybinding.
struct KeyEquivalent: CustomStringConvertible { struct KeyEquivalent: CustomStringConvertible {
let key: String let key: String
let modifiers: NSEvent.ModifierFlags let modifiers: NSEvent.ModifierFlags
var description: String { var description: String {
var key = self.key var key = self.key
// Note: the order below matters; it matches the ordering modifiers // Note: the order below matters; it matches the ordering modifiers
// shown for macOS menu shortcut labels. // shown for macOS menu shortcut labels.
if modifiers.contains(.command) { key = "\(key)" } if modifiers.contains(.command) { key = "\(key)" }
if modifiers.contains(.shift) { key = "\(key)" } if modifiers.contains(.shift) { key = "\(key)" }
if modifiers.contains(.option) { key = "\(key)" } if modifiers.contains(.option) { key = "\(key)" }
if modifiers.contains(.control) { key = "\(key)" } if modifiers.contains(.control) { key = "\(key)" }
return key return key
} }
} }
/// Return the key equivalent for the given action. The action is the name of the action /// Return the key equivalent for the given action. The action is the name of the action
/// in the Ghostty configuration. For example `keybind = cmd+q=quit` in Ghostty /// in the Ghostty configuration. For example `keybind = cmd+q=quit` in Ghostty
/// configuration would be "quit" action. /// configuration would be "quit" action.
@ -113,7 +113,7 @@ extension Ghostty {
/// Returns nil if there is no key equivalent for the given action. /// Returns nil if there is no key equivalent for the given action.
func keyEquivalent(for action: String) -> KeyEquivalent? { func keyEquivalent(for action: String) -> KeyEquivalent? {
guard let cfg = self.config else { return nil } guard let cfg = self.config else { return nil }
let trigger = ghostty_config_trigger(cfg, action, UInt(action.count)) let trigger = ghostty_config_trigger(cfg, action, UInt(action.count))
let equiv: String let equiv: String
switch (trigger.tag) { switch (trigger.tag) {
@ -123,34 +123,34 @@ extension Ghostty {
} else { } else {
return nil return nil
} }
case GHOSTTY_TRIGGER_PHYSICAL: case GHOSTTY_TRIGGER_PHYSICAL:
if let v = Ghostty.keyEquivalent(key: trigger.key.physical) { if let v = Ghostty.keyEquivalent(key: trigger.key.physical) {
equiv = v equiv = v
} else { } else {
return nil return nil
} }
case GHOSTTY_TRIGGER_UNICODE: case GHOSTTY_TRIGGER_UNICODE:
equiv = String(trigger.key.unicode) equiv = String(trigger.key.unicode)
default: default:
return nil return nil
} }
return KeyEquivalent( return KeyEquivalent(
key: equiv, key: equiv,
modifiers: Ghostty.eventModifierFlags(mods: trigger.mods) modifiers: Ghostty.eventModifierFlags(mods: trigger.mods)
) )
} }
#endif #endif
// MARK: - Configuration Values // MARK: - Configuration Values
/// For all of the configuration values below, see the associated Ghostty documentation for /// For all of the configuration values below, see the associated Ghostty documentation for
/// details on what each means. We only add documentation if there is a strange conversion /// details on what each means. We only add documentation if there is a strange conversion
/// due to the embedded library and Swift. /// due to the embedded library and Swift.
var shouldQuitAfterLastWindowClosed: Bool { var shouldQuitAfterLastWindowClosed: Bool {
guard let config = self.config else { return true } guard let config = self.config else { return true }
var v = false; var v = false;
@ -158,7 +158,7 @@ extension Ghostty {
_ = ghostty_config_get(config, &v, key, UInt(key.count)) _ = ghostty_config_get(config, &v, key, UInt(key.count))
return v return v
} }
var windowColorspace: String { var windowColorspace: String {
guard let config = self.config else { return "" } guard let config = self.config else { return "" }
var v: UnsafePointer<Int8>? = nil var v: UnsafePointer<Int8>? = nil
@ -167,7 +167,7 @@ extension Ghostty {
guard let ptr = v else { return "" } guard let ptr = v else { return "" }
return String(cString: ptr) return String(cString: ptr)
} }
var windowSaveState: String { var windowSaveState: String {
guard let config = self.config else { return "" } guard let config = self.config else { return "" }
var v: UnsafePointer<Int8>? = nil var v: UnsafePointer<Int8>? = nil
@ -176,7 +176,7 @@ extension Ghostty {
guard let ptr = v else { return "" } guard let ptr = v else { return "" }
return String(cString: ptr) return String(cString: ptr)
} }
var windowNewTabPosition: String { var windowNewTabPosition: String {
guard let config = self.config else { return "" } guard let config = self.config else { return "" }
var v: UnsafePointer<Int8>? = nil var v: UnsafePointer<Int8>? = nil
@ -185,7 +185,7 @@ extension Ghostty {
guard let ptr = v else { return "" } guard let ptr = v else { return "" }
return String(cString: ptr) return String(cString: ptr)
} }
var windowDecorations: Bool { var windowDecorations: Bool {
guard let config = self.config else { return true } guard let config = self.config else { return true }
var v = false; var v = false;
@ -193,7 +193,7 @@ extension Ghostty {
_ = ghostty_config_get(config, &v, key, UInt(key.count)) _ = ghostty_config_get(config, &v, key, UInt(key.count))
return v; return v;
} }
var windowTheme: String? { var windowTheme: String? {
guard let config = self.config else { return nil } guard let config = self.config else { return nil }
var v: UnsafePointer<Int8>? = nil var v: UnsafePointer<Int8>? = nil
@ -202,7 +202,7 @@ extension Ghostty {
guard let ptr = v else { return nil } guard let ptr = v else { return nil }
return String(cString: ptr) return String(cString: ptr)
} }
var windowStepResize: Bool { var windowStepResize: Bool {
guard let config = self.config else { return true } guard let config = self.config else { return true }
var v = false var v = false
@ -210,7 +210,7 @@ extension Ghostty {
_ = ghostty_config_get(config, &v, key, UInt(key.count)) _ = ghostty_config_get(config, &v, key, UInt(key.count))
return v return v
} }
var windowFullscreen: Bool { var windowFullscreen: Bool {
guard let config = self.config else { return true } guard let config = self.config else { return true }
var v = false var v = false
@ -237,7 +237,7 @@ extension Ghostty {
guard let ptr = v else { return defaultValue } guard let ptr = v else { return defaultValue }
return String(cString: ptr) return String(cString: ptr)
} }
var macosWindowShadow: Bool { var macosWindowShadow: Bool {
guard let config = self.config else { return false } guard let config = self.config else { return false }
var v = false; var v = false;
@ -266,18 +266,18 @@ extension Ghostty {
#error("unsupported") #error("unsupported")
#endif #endif
} }
let red = Double(rgb & 0xff) let red = Double(rgb & 0xff)
let green = Double((rgb >> 8) & 0xff) let green = Double((rgb >> 8) & 0xff)
let blue = Double((rgb >> 16) & 0xff) let blue = Double((rgb >> 16) & 0xff)
return Color( return Color(
red: red / 255, red: red / 255,
green: green / 255, green: green / 255,
blue: blue / 255 blue: blue / 255
) )
} }
var backgroundOpacity: Double { var backgroundOpacity: Double {
guard let config = self.config else { return 1 } guard let config = self.config else { return 1 }
var v: Double = 1 var v: Double = 1
@ -285,7 +285,7 @@ extension Ghostty {
_ = ghostty_config_get(config, &v, key, UInt(key.count)) _ = ghostty_config_get(config, &v, key, UInt(key.count))
return v; return v;
} }
var backgroundBlurRadius: Int { var backgroundBlurRadius: Int {
guard let config = self.config else { return 1 } guard let config = self.config else { return 1 }
var v: Int = 0 var v: Int = 0
@ -293,7 +293,7 @@ extension Ghostty {
_ = ghostty_config_get(config, &v, key, UInt(key.count)) _ = ghostty_config_get(config, &v, key, UInt(key.count))
return v; return v;
} }
var unfocusedSplitOpacity: Double { var unfocusedSplitOpacity: Double {
guard let config = self.config else { return 1 } guard let config = self.config else { return 1 }
var opacity: Double = 0.85 var opacity: Double = 0.85
@ -301,28 +301,28 @@ extension Ghostty {
_ = ghostty_config_get(config, &opacity, key, UInt(key.count)) _ = ghostty_config_get(config, &opacity, key, UInt(key.count))
return 1 - opacity return 1 - opacity
} }
var unfocusedSplitFill: Color { var unfocusedSplitFill: Color {
guard let config = self.config else { return .white } guard let config = self.config else { return .white }
var rgb: UInt32 = 16777215 // white default var rgb: UInt32 = 16777215 // white default
let key = "unfocused-split-fill" let key = "unfocused-split-fill"
if (!ghostty_config_get(config, &rgb, key, UInt(key.count))) { if (!ghostty_config_get(config, &rgb, key, UInt(key.count))) {
let bg_key = "background" let bg_key = "background"
_ = ghostty_config_get(config, &rgb, bg_key, UInt(bg_key.count)); _ = ghostty_config_get(config, &rgb, bg_key, UInt(bg_key.count));
} }
let red = Double(rgb & 0xff) let red = Double(rgb & 0xff)
let green = Double((rgb >> 8) & 0xff) let green = Double((rgb >> 8) & 0xff)
let blue = Double((rgb >> 16) & 0xff) let blue = Double((rgb >> 16) & 0xff)
return Color( return Color(
red: red / 255, red: red / 255,
green: green / 255, green: green / 255,
blue: blue / 255 blue: blue / 255
) )
} }
// This isn't actually a configurable value currently but it could be done day. // This isn't actually a configurable value currently but it could be done day.
// We put it here because it is a color that changes depending on the configuration. // We put it here because it is a color that changes depending on the configuration.
var splitDividerColor: Color { var splitDividerColor: Color {
@ -331,7 +331,7 @@ extension Ghostty {
let newColor = isLightBackground ? backgroundColor.darken(by: 0.08) : backgroundColor.darken(by: 0.4) let newColor = isLightBackground ? backgroundColor.darken(by: 0.08) : backgroundColor.darken(by: 0.4)
return Color(newColor) return Color(newColor)
} }
var resizeOverlay: ResizeOverlay { var resizeOverlay: ResizeOverlay {
guard let config = self.config else { return .after_first } guard let config = self.config else { return .after_first }
var v: UnsafePointer<Int8>? = nil var v: UnsafePointer<Int8>? = nil
@ -341,7 +341,7 @@ extension Ghostty {
let str = String(cString: ptr) let str = String(cString: ptr)
return ResizeOverlay(rawValue: str) ?? .after_first return ResizeOverlay(rawValue: str) ?? .after_first
} }
var resizeOverlayPosition: ResizeOverlayPosition { var resizeOverlayPosition: ResizeOverlayPosition {
let defaultValue = ResizeOverlayPosition.center let defaultValue = ResizeOverlayPosition.center
guard let config = self.config else { return defaultValue } guard let config = self.config else { return defaultValue }
@ -352,7 +352,7 @@ extension Ghostty {
let str = String(cString: ptr) let str = String(cString: ptr)
return ResizeOverlayPosition(rawValue: str) ?? defaultValue return ResizeOverlayPosition(rawValue: str) ?? defaultValue
} }
var resizeOverlayDuration: UInt { var resizeOverlayDuration: UInt {
guard let config = self.config else { return 1000 } guard let config = self.config else { return 1000 }
var v: UInt = 0 var v: UInt = 0
@ -371,7 +371,7 @@ extension Ghostty.Config {
case never case never
case after_first = "after-first" case after_first = "after-first"
} }
enum ResizeOverlayPosition : String { enum ResizeOverlayPosition : String {
case center case center
case top_left = "top-left" case top_left = "top-left"
@ -380,28 +380,28 @@ extension Ghostty.Config {
case bottom_left = "bottom-left" case bottom_left = "bottom-left"
case bottom_center = "bottom-center" case bottom_center = "bottom-center"
case bottom_right = "bottom-right" case bottom_right = "bottom-right"
func top() -> Bool { func top() -> Bool {
switch (self) { switch (self) {
case .top_left, .top_center, .top_right: return true; case .top_left, .top_center, .top_right: return true;
default: return false; default: return false;
} }
} }
func bottom() -> Bool { func bottom() -> Bool {
switch (self) { switch (self) {
case .bottom_left, .bottom_center, .bottom_right: return true; case .bottom_left, .bottom_center, .bottom_right: return true;
default: return false; default: return false;
} }
} }
func left() -> Bool { func left() -> Bool {
switch (self) { switch (self) {
case .top_left, .bottom_left: return true; case .top_left, .bottom_left: return true;
default: return false; default: return false;
} }
} }
func right() -> Bool { func right() -> Bool {
switch (self) { switch (self) {
case .top_right, .bottom_right: return true; case .top_right, .bottom_right: return true;

View File

@ -16,7 +16,7 @@ extension Ghostty {
if (mods.rawValue & GHOSTTY_MODS_SUPER.rawValue != 0) { flags.insert(.command) } if (mods.rawValue & GHOSTTY_MODS_SUPER.rawValue != 0) { flags.insert(.command) }
return flags return flags
} }
/// Translate event modifier flags to a ghostty mods enum. /// Translate event modifier flags to a ghostty mods enum.
static func ghosttyMods(_ flags: NSEvent.ModifierFlags) -> ghostty_input_mods_e { static func ghosttyMods(_ flags: NSEvent.ModifierFlags) -> ghostty_input_mods_e {
var mods: UInt32 = GHOSTTY_MODS_NONE.rawValue var mods: UInt32 = GHOSTTY_MODS_NONE.rawValue
@ -37,7 +37,7 @@ extension Ghostty {
return ghostty_input_mods_e(mods) return ghostty_input_mods_e(mods)
} }
/// A map from the Ghostty key enum to the keyEquivalent string for shortcuts. /// A map from the Ghostty key enum to the keyEquivalent string for shortcuts.
static let keyToEquivalent: [ghostty_input_key_e : String] = [ static let keyToEquivalent: [ghostty_input_key_e : String] = [
// 0-9 // 0-9
@ -220,7 +220,7 @@ extension Ghostty {
0x3B: GHOSTTY_KEY_SEMICOLON, 0x3B: GHOSTTY_KEY_SEMICOLON,
0x2F: GHOSTTY_KEY_SLASH, 0x2F: GHOSTTY_KEY_SLASH,
] ]
// Mapping of event keyCode to ghostty input key values. This is cribbed from // Mapping of event keyCode to ghostty input key values. This is cribbed from
// glfw mostly since we started as a glfw-based app way back in the day! // glfw mostly since we started as a glfw-based app way back in the day!
static let keycodeToKey: [UInt16 : ghostty_input_key_e] = [ static let keycodeToKey: [UInt16 : ghostty_input_key_e] = [
@ -338,4 +338,3 @@ extension Ghostty {
0x4E: GHOSTTY_KEY_KP_SUBTRACT, 0x4E: GHOSTTY_KEY_KP_SUBTRACT,
]; ];
} }

View File

@ -2,7 +2,7 @@ extension Ghostty {
struct Shell { struct Shell {
// Characters to escape in the shell. // Characters to escape in the shell.
static let escapeCharacters = "\\ ()[]{}<>\"'`!#$&;|*?\t" static let escapeCharacters = "\\ ()[]{}<>\"'`!#$&;|*?\t"
/// Escape shell-sensitive characters in string. /// Escape shell-sensitive characters in string.
static func escape(_ str: String) -> String { static func escape(_ str: String) -> String {
var result = str var result = str
@ -12,7 +12,7 @@ extension Ghostty {
with: "\\\(char)" with: "\\\(char)"
) )
} }
return result return result
} }
} }

View File

@ -14,7 +14,7 @@ extension Ghostty {
enum SplitNode: Equatable, Hashable, Codable, Sequence { enum SplitNode: Equatable, Hashable, Codable, Sequence {
case leaf(Leaf) case leaf(Leaf)
case split(Container) case split(Container)
/// The parent of this node. /// The parent of this node.
var parent: Container? { var parent: Container? {
get { get {
@ -26,7 +26,7 @@ extension Ghostty {
return container.parent return container.parent
} }
} }
set { set {
switch (self) { switch (self) {
case .leaf(let leaf): case .leaf(let leaf):
@ -37,7 +37,7 @@ extension Ghostty {
} }
} }
} }
/// Returns the view that would prefer receiving focus in this tree. This is always the /// Returns the view that would prefer receiving focus in this tree. This is always the
/// top-left-most view. This is used when creating a split or closing a split to find the /// top-left-most view. This is used when creating a split or closing a split to find the
/// next view to send focus to. /// next view to send focus to.
@ -51,16 +51,16 @@ extension Ghostty {
case .split(let c): case .split(let c):
container = c container = c
} }
let node: SplitNode let node: SplitNode
switch (direction) { switch (direction) {
case .previous, .top, .left: case .previous, .top, .left:
node = container.bottomRight node = container.bottomRight
case .next, .bottom, .right: case .next, .bottom, .right:
node = container.topLeft node = container.topLeft
} }
return node.preferredFocus(direction) return node.preferredFocus(direction)
} }
@ -95,7 +95,7 @@ extension Ghostty {
container.bottomRight.close() container.bottomRight.close()
} }
} }
/// Returns true if any surface in the split stack requires quit confirmation. /// Returns true if any surface in the split stack requires quit confirmation.
func needsConfirmQuit() -> Bool { func needsConfirmQuit() -> Bool {
switch (self) { switch (self) {
@ -119,7 +119,7 @@ extension Ghostty {
container.bottomRight.contains(view: view) container.bottomRight.contains(view: view)
} }
} }
/// Find a surface view by UUID. /// Find a surface view by UUID.
func findUUID(uuid: UUID) -> SurfaceView? { func findUUID(uuid: UUID) -> SurfaceView? {
switch (self) { switch (self) {
@ -127,7 +127,7 @@ extension Ghostty {
if (leaf.surface.uuid == uuid) { if (leaf.surface.uuid == uuid) {
return leaf.surface return leaf.surface
} }
return nil return nil
case .split(let container): case .split(let container):
@ -135,13 +135,13 @@ extension Ghostty {
container.bottomRight.findUUID(uuid: uuid) container.bottomRight.findUUID(uuid: uuid)
} }
} }
// MARK: - Sequence // MARK: - Sequence
func makeIterator() -> IndexingIterator<[Leaf]> { func makeIterator() -> IndexingIterator<[Leaf]> {
return leaves().makeIterator() return leaves().makeIterator()
} }
/// Return all the leaves in this split node. This isn't very efficient but our split trees are never super /// Return all the leaves in this split node. This isn't very efficient but our split trees are never super
/// deep so its not an issue. /// deep so its not an issue.
private func leaves() -> [Leaf] { private func leaves() -> [Leaf] {
@ -153,9 +153,9 @@ extension Ghostty {
return container.topLeft.leaves() + container.bottomRight.leaves() return container.topLeft.leaves() + container.bottomRight.leaves()
} }
} }
// MARK: - Equatable // MARK: - Equatable
static func == (lhs: SplitNode, rhs: SplitNode) -> Bool { static func == (lhs: SplitNode, rhs: SplitNode) -> Bool {
switch (lhs, rhs) { switch (lhs, rhs) {
case (.leaf(let lhs_v), .leaf(let rhs_v)): case (.leaf(let lhs_v), .leaf(let rhs_v)):
@ -178,27 +178,27 @@ extension Ghostty {
self.app = app self.app = app
self.surface = SurfaceView(app, baseConfig: baseConfig, uuid: uuid) self.surface = SurfaceView(app, baseConfig: baseConfig, uuid: uuid)
} }
// MARK: - Hashable // MARK: - Hashable
func hash(into hasher: inout Hasher) { func hash(into hasher: inout Hasher) {
hasher.combine(app) hasher.combine(app)
hasher.combine(surface) hasher.combine(surface)
} }
// MARK: - Equatable // MARK: - Equatable
static func == (lhs: Leaf, rhs: Leaf) -> Bool { static func == (lhs: Leaf, rhs: Leaf) -> Bool {
return lhs.app == rhs.app && lhs.surface === rhs.surface return lhs.app == rhs.app && lhs.surface === rhs.surface
} }
// MARK: - Codable // MARK: - Codable
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
case pwd case pwd
case uuid case uuid
} }
required convenience init(from decoder: Decoder) throws { required convenience init(from decoder: Decoder) throws {
// Decoding uses the global Ghostty app // Decoding uses the global Ghostty app
guard let del = NSApplication.shared.delegate, guard let del = NSApplication.shared.delegate,
@ -206,15 +206,15 @@ extension Ghostty {
let app = appDel.ghostty.app else { let app = appDel.ghostty.app else {
throw TerminalRestoreError.delegateInvalid throw TerminalRestoreError.delegateInvalid
} }
let container = try decoder.container(keyedBy: CodingKeys.self) let container = try decoder.container(keyedBy: CodingKeys.self)
let uuid = UUID(uuidString: try container.decode(String.self, forKey: .uuid)) let uuid = UUID(uuidString: try container.decode(String.self, forKey: .uuid))
var config = SurfaceConfiguration() var config = SurfaceConfiguration()
config.workingDirectory = try container.decode(String?.self, forKey: .pwd) config.workingDirectory = try container.decode(String?.self, forKey: .pwd)
self.init(app, baseConfig: config, uuid: uuid) self.init(app, baseConfig: config, uuid: uuid)
} }
func encode(to encoder: Encoder) throws { func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self) var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(surface.pwd, forKey: .pwd) try container.encode(surface.pwd, forKey: .pwd)
@ -333,32 +333,32 @@ extension Ghostty {
} }
// MARK: - Hashable // MARK: - Hashable
func hash(into hasher: inout Hasher) { func hash(into hasher: inout Hasher) {
hasher.combine(app) hasher.combine(app)
hasher.combine(direction) hasher.combine(direction)
hasher.combine(topLeft) hasher.combine(topLeft)
hasher.combine(bottomRight) hasher.combine(bottomRight)
} }
// MARK: - Equatable // MARK: - Equatable
static func == (lhs: Container, rhs: Container) -> Bool { static func == (lhs: Container, rhs: Container) -> Bool {
return lhs.app == rhs.app && return lhs.app == rhs.app &&
lhs.direction == rhs.direction && lhs.direction == rhs.direction &&
lhs.topLeft == rhs.topLeft && lhs.topLeft == rhs.topLeft &&
lhs.bottomRight == rhs.bottomRight lhs.bottomRight == rhs.bottomRight
} }
// MARK: - Codable // MARK: - Codable
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
case direction case direction
case split case split
case topLeft case topLeft
case bottomRight case bottomRight
} }
required init(from decoder: Decoder) throws { required init(from decoder: Decoder) throws {
// Decoding uses the global Ghostty app // Decoding uses the global Ghostty app
guard let del = NSApplication.shared.delegate, guard let del = NSApplication.shared.delegate,
@ -366,19 +366,19 @@ extension Ghostty {
let app = appDel.ghostty.app else { let app = appDel.ghostty.app else {
throw TerminalRestoreError.delegateInvalid throw TerminalRestoreError.delegateInvalid
} }
let container = try decoder.container(keyedBy: CodingKeys.self) let container = try decoder.container(keyedBy: CodingKeys.self)
self.app = app self.app = app
self.direction = try container.decode(SplitViewDirection.self, forKey: .direction) self.direction = try container.decode(SplitViewDirection.self, forKey: .direction)
self.split = try container.decode(CGFloat.self, forKey: .split) self.split = try container.decode(CGFloat.self, forKey: .split)
self.topLeft = try container.decode(SplitNode.self, forKey: .topLeft) self.topLeft = try container.decode(SplitNode.self, forKey: .topLeft)
self.bottomRight = try container.decode(SplitNode.self, forKey: .bottomRight) self.bottomRight = try container.decode(SplitNode.self, forKey: .bottomRight)
// Fix up the parent references // Fix up the parent references
self.topLeft.parent = self self.topLeft.parent = self
self.bottomRight.parent = self self.bottomRight.parent = self
} }
func encode(to encoder: Encoder) throws { func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self) var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(direction, forKey: .direction) try container.encode(direction, forKey: .direction)
@ -429,7 +429,7 @@ extension Ghostty {
} }
return clone return clone
} }
/// True if there are no neighbors /// True if there are no neighbors
func isEmpty() -> Bool { func isEmpty() -> Bool {
return self.previous == nil && self.next == nil return self.previous == nil && self.next == nil

View File

@ -61,7 +61,7 @@ extension Ghostty {
switch (node) { switch (node) {
case nil: case nil:
Color(.clear) Color(.clear)
case .leaf(let leaf): case .leaf(let leaf):
TerminalSplitLeaf( TerminalSplitLeaf(
leaf: leaf, leaf: leaf,
@ -94,7 +94,7 @@ extension Ghostty {
.onReceive(pubFocus) { onZoomReset(notification: $0) } .onReceive(pubFocus) { onZoomReset(notification: $0) }
} }
} }
func onZoom(notification: SwiftUI.Notification) { func onZoom(notification: SwiftUI.Notification) {
// Our node must be split to receive zooms. You can't zoom an unsplit terminal. // Our node must be split to receive zooms. You can't zoom an unsplit terminal.
if case .leaf = node { if case .leaf = node {
@ -182,14 +182,14 @@ extension Ghostty {
node = nil node = nil
return return
} }
// If we don't have a window to attach our modal to, we also exit immediately. // If we don't have a window to attach our modal to, we also exit immediately.
// This should NOT happen. // This should NOT happen.
guard let window = leaf.surface.window else { guard let window = leaf.surface.window else {
node = nil node = nil
return return
} }
// Confirm close. We use an NSAlert instead of a SwiftUI confirmationDialog // Confirm close. We use an NSAlert instead of a SwiftUI confirmationDialog
// due to SwiftUI bugs (see Ghostty #560). To repeat from #560, the bug is that // due to SwiftUI bugs (see Ghostty #560). To repeat from #560, the bug is that
// confirmationDialog allows the user to Cmd-W close the alert, but when doing // confirmationDialog allows the user to Cmd-W close the alert, but when doing
@ -206,7 +206,7 @@ extension Ghostty {
switch (response) { switch (response) {
case .alertFirstButtonReturn: case .alertFirstButtonReturn:
node = nil node = nil
default: default:
break break
} }
@ -277,7 +277,7 @@ extension Ghostty {
parent.resize(direction: direction, amount: amount) parent.resize(direction: direction, amount: amount)
} }
} }
/// This represents a split view that is in the horizontal or vertical split state. /// This represents a split view that is in the horizontal or vertical split state.
private struct TerminalSplitContainer: View { private struct TerminalSplitContainer: View {
@EnvironmentObject var ghostty: Ghostty.App @EnvironmentObject var ghostty: Ghostty.App
@ -315,7 +315,7 @@ extension Ghostty {
) )
}) })
} }
private func closeableTopLeft() -> Binding<SplitNode?> { private func closeableTopLeft() -> Binding<SplitNode?> {
return .init(get: { return .init(get: {
container.topLeft container.topLeft
@ -324,7 +324,7 @@ extension Ghostty {
container.topLeft = newValue container.topLeft = newValue
return return
} }
// Closing // Closing
container.topLeft.close() container.topLeft.close()
node = container.bottomRight node = container.bottomRight
@ -346,7 +346,7 @@ extension Ghostty {
} }
}) })
} }
private func closeableBottomRight() -> Binding<SplitNode?> { private func closeableBottomRight() -> Binding<SplitNode?> {
return .init(get: { return .init(get: {
container.bottomRight container.bottomRight
@ -355,7 +355,7 @@ extension Ghostty {
container.bottomRight = newValue container.bottomRight = newValue
return return
} }
// Closing // Closing
container.bottomRight.close() container.bottomRight.close()
node = container.topLeft node = container.topLeft
@ -379,7 +379,7 @@ extension Ghostty {
} }
} }
/// This is like TerminalSplitRoot, but... not the root. This renders a SplitNode in any state but /// This is like TerminalSplitRoot, but... not the root. This renders a SplitNode in any state but
/// requires there be a binding to the parent node. /// requires there be a binding to the parent node.
private struct TerminalSplitNested: View { private struct TerminalSplitNested: View {
@ -410,7 +410,7 @@ extension Ghostty {
.id(node) .id(node)
} }
} }
/// When changing the split state, or going full screen (native or non), the terminal view /// When changing the split state, or going full screen (native or non), the terminal view
/// will lose focus. There has to be some nice SwiftUI-native way to fix this but I can't /// will lose focus. There has to be some nice SwiftUI-native way to fix this but I can't
/// figure it out so we're going to do this hacky thing to bring focus back to the terminal /// figure it out so we're going to do this hacky thing to bring focus back to the terminal

View File

@ -11,7 +11,7 @@ extension Ghostty {
/// Same as SurfaceWrapper, see the doc comments there. /// Same as SurfaceWrapper, see the doc comments there.
@ObservedObject var surfaceView: SurfaceView @ObservedObject var surfaceView: SurfaceView
var isSplit: Bool = false var isSplit: Bool = false
// Maintain whether our view has focus or not // Maintain whether our view has focus or not
@FocusState private var inspectorFocus: Bool @FocusState private var inspectorFocus: Bool
@ -21,7 +21,7 @@ extension Ghostty {
var body: some View { var body: some View {
let center = NotificationCenter.default let center = NotificationCenter.default
let pubInspector = center.publisher(for: Notification.didControlInspector, object: surfaceView) let pubInspector = center.publisher(for: Notification.didControlInspector, object: surfaceView)
ZStack { ZStack {
if (!surfaceView.inspectorVisible) { if (!surfaceView.inspectorVisible) {
SurfaceWrapper(surfaceView: surfaceView, isSplit: isSplit) SurfaceWrapper(surfaceView: surfaceView, isSplit: isSplit)
@ -51,28 +51,28 @@ extension Ghostty {
} }
} }
} }
private func onControlInspector(_ notification: SwiftUI.Notification) { private func onControlInspector(_ notification: SwiftUI.Notification) {
// Determine our mode // Determine our mode
guard let modeAny = notification.userInfo?["mode"] else { return } guard let modeAny = notification.userInfo?["mode"] else { return }
guard let mode = modeAny as? ghostty_inspector_mode_e else { return } guard let mode = modeAny as? ghostty_inspector_mode_e else { return }
switch (mode) { switch (mode) {
case GHOSTTY_INSPECTOR_TOGGLE: case GHOSTTY_INSPECTOR_TOGGLE:
surfaceView.inspectorVisible = !surfaceView.inspectorVisible surfaceView.inspectorVisible = !surfaceView.inspectorVisible
case GHOSTTY_INSPECTOR_SHOW: case GHOSTTY_INSPECTOR_SHOW:
surfaceView.inspectorVisible = true surfaceView.inspectorVisible = true
case GHOSTTY_INSPECTOR_HIDE: case GHOSTTY_INSPECTOR_HIDE:
surfaceView.inspectorVisible = false surfaceView.inspectorVisible = false
default: default:
return return
} }
} }
} }
struct InspectorViewRepresentable: NSViewRepresentable { struct InspectorViewRepresentable: NSViewRepresentable {
/// The surface that this inspector represents. /// The surface that this inspector represents.
let surfaceView: SurfaceView let surfaceView: SurfaceView
@ -87,25 +87,25 @@ extension Ghostty {
view.surfaceView = self.surfaceView view.surfaceView = self.surfaceView
} }
} }
/// Inspector view is the view for the surface inspector (similar to a web inspector). /// Inspector view is the view for the surface inspector (similar to a web inspector).
class InspectorView: MTKView, NSTextInputClient { class InspectorView: MTKView, NSTextInputClient {
let commandQueue: MTLCommandQueue let commandQueue: MTLCommandQueue
var surfaceView: SurfaceView? = nil { var surfaceView: SurfaceView? = nil {
didSet { surfaceViewDidChange() } didSet { surfaceViewDidChange() }
} }
private var inspector: ghostty_inspector_t? { private var inspector: ghostty_inspector_t? {
guard let surfaceView = self.surfaceView else { return nil } guard let surfaceView = self.surfaceView else { return nil }
return surfaceView.inspector return surfaceView.inspector
} }
private var markedText: NSMutableAttributedString = NSMutableAttributedString() private var markedText: NSMutableAttributedString = NSMutableAttributedString()
// We need to support being a first responder so that we can get input events // We need to support being a first responder so that we can get input events
override var acceptsFirstResponder: Bool { return true } override var acceptsFirstResponder: Bool { return true }
override init(frame: CGRect, device: MTLDevice?) { override init(frame: CGRect, device: MTLDevice?) {
// Initialize our Metal primitives // Initialize our Metal primitives
guard guard
@ -113,44 +113,44 @@ extension Ghostty {
let commandQueue = device.makeCommandQueue() else { let commandQueue = device.makeCommandQueue() else {
fatalError("GPU not available") fatalError("GPU not available")
} }
// Setup our properties before initializing the parent // Setup our properties before initializing the parent
self.commandQueue = commandQueue self.commandQueue = commandQueue
super.init(frame: frame, device: device) super.init(frame: frame, device: device)
// This makes it so renders only happen when we request // This makes it so renders only happen when we request
self.enableSetNeedsDisplay = true self.enableSetNeedsDisplay = true
self.isPaused = true self.isPaused = true
// After initializing the parent we can set our own properties // After initializing the parent we can set our own properties
self.device = MTLCreateSystemDefaultDevice() self.device = MTLCreateSystemDefaultDevice()
self.clearColor = MTLClearColor(red: 0x28 / 0xFF, green: 0x2C / 0xFF, blue: 0x34 / 0xFF, alpha: 1.0) self.clearColor = MTLClearColor(red: 0x28 / 0xFF, green: 0x2C / 0xFF, blue: 0x34 / 0xFF, alpha: 1.0)
// Setup our tracking areas for mouse events // Setup our tracking areas for mouse events
updateTrackingAreas() updateTrackingAreas()
} }
required init(coder: NSCoder) { required init(coder: NSCoder) {
fatalError("init(coder:) is not supported for this view") fatalError("init(coder:) is not supported for this view")
} }
deinit { deinit {
trackingAreas.forEach { removeTrackingArea($0) } trackingAreas.forEach { removeTrackingArea($0) }
NotificationCenter.default.removeObserver(self) NotificationCenter.default.removeObserver(self)
} }
// MARK: Internal Inspector Funcs // MARK: Internal Inspector Funcs
private func surfaceViewDidChange() { private func surfaceViewDidChange() {
let center = NotificationCenter.default let center = NotificationCenter.default
center.removeObserver(self) center.removeObserver(self)
guard let surfaceView = self.surfaceView else { return } guard let surfaceView = self.surfaceView else { return }
guard let inspector = self.inspector else { return } guard let inspector = self.inspector else { return }
guard let device = self.device else { return } guard let device = self.device else { return }
let devicePtr = Unmanaged.passRetained(device).toOpaque() let devicePtr = Unmanaged.passRetained(device).toOpaque()
ghostty_inspector_metal_init(inspector, devicePtr) ghostty_inspector_metal_init(inspector, devicePtr)
// Register an observer for render requests // Register an observer for render requests
center.addObserver( center.addObserver(
self, self,
@ -158,11 +158,11 @@ extension Ghostty {
name: Ghostty.Notification.inspectorNeedsDisplay, name: Ghostty.Notification.inspectorNeedsDisplay,
object: surfaceView) object: surfaceView)
} }
@objc private func didRequestRender(notification: SwiftUI.Notification) { @objc private func didRequestRender(notification: SwiftUI.Notification) {
self.needsDisplay = true self.needsDisplay = true
} }
private func updateSize() { private func updateSize() {
guard let inspector = self.inspector else { return } guard let inspector = self.inspector else { return }
@ -175,9 +175,9 @@ extension Ghostty {
// When our scale factor changes, so does our fb size so we send that too // When our scale factor changes, so does our fb size so we send that too
ghostty_inspector_set_size(inspector, UInt32(fbFrame.size.width), UInt32(fbFrame.size.height)) ghostty_inspector_set_size(inspector, UInt32(fbFrame.size.width), UInt32(fbFrame.size.height))
} }
// MARK: NSView // MARK: NSView
override func becomeFirstResponder() -> Bool { override func becomeFirstResponder() -> Bool {
let result = super.becomeFirstResponder() let result = super.becomeFirstResponder()
if (result) { if (result) {
@ -197,7 +197,7 @@ extension Ghostty {
} }
return result return result
} }
override func updateTrackingAreas() { override func updateTrackingAreas() {
// To update our tracking area we just recreate it all. // To update our tracking area we just recreate it all.
trackingAreas.forEach { removeTrackingArea($0) } trackingAreas.forEach { removeTrackingArea($0) }
@ -207,7 +207,7 @@ extension Ghostty {
rect: frame, rect: frame,
options: [ options: [
.mouseMoved, .mouseMoved,
// Only send mouse events that happen in our visible (not obscured) rect // Only send mouse events that happen in our visible (not obscured) rect
.inVisibleRect, .inVisibleRect,
@ -218,12 +218,12 @@ extension Ghostty {
owner: self, owner: self,
userInfo: nil)) userInfo: nil))
} }
override func viewDidChangeBackingProperties() { override func viewDidChangeBackingProperties() {
super.viewDidChangeBackingProperties() super.viewDidChangeBackingProperties()
updateSize() updateSize()
} }
override func mouseDown(with event: NSEvent) { override func mouseDown(with event: NSEvent) {
guard let inspector = self.inspector else { return } guard let inspector = self.inspector else { return }
let mods = Ghostty.ghosttyMods(event.modifierFlags) let mods = Ghostty.ghosttyMods(event.modifierFlags)
@ -247,10 +247,10 @@ extension Ghostty {
let mods = Ghostty.ghosttyMods(event.modifierFlags) let mods = Ghostty.ghosttyMods(event.modifierFlags)
ghostty_inspector_mouse_button(inspector, GHOSTTY_MOUSE_RELEASE, GHOSTTY_MOUSE_RIGHT, mods) ghostty_inspector_mouse_button(inspector, GHOSTTY_MOUSE_RELEASE, GHOSTTY_MOUSE_RIGHT, mods)
} }
override func mouseMoved(with event: NSEvent) { override func mouseMoved(with event: NSEvent) {
guard let inspector = self.inspector else { return } guard let inspector = self.inspector else { return }
// Convert window position to view position. Note (0, 0) is bottom left. // Convert window position to view position. Note (0, 0) is bottom left.
let pos = self.convert(event.locationInWindow, from: nil) let pos = self.convert(event.locationInWindow, from: nil)
ghostty_inspector_mouse_pos(inspector, pos.x, frame.height - pos.y) ghostty_inspector_mouse_pos(inspector, pos.x, frame.height - pos.y)
@ -260,7 +260,7 @@ extension Ghostty {
override func mouseDragged(with event: NSEvent) { override func mouseDragged(with event: NSEvent) {
self.mouseMoved(with: event) self.mouseMoved(with: event)
} }
override func scrollWheel(with event: NSEvent) { override func scrollWheel(with event: NSEvent) {
guard let inspector = self.inspector else { return } guard let inspector = self.inspector else { return }
@ -303,7 +303,7 @@ extension Ghostty {
ghostty_inspector_mouse_scroll(inspector, x, y, mods) ghostty_inspector_mouse_scroll(inspector, x, y, mods)
} }
override func keyDown(with event: NSEvent) { override func keyDown(with event: NSEvent) {
let action = event.isARepeat ? GHOSTTY_ACTION_REPEAT : GHOSTTY_ACTION_PRESS let action = event.isARepeat ? GHOSTTY_ACTION_REPEAT : GHOSTTY_ACTION_PRESS
keyAction(action, event: event) keyAction(action, event: event)
@ -342,7 +342,7 @@ extension Ghostty {
let mods = Ghostty.ghosttyMods(event.modifierFlags) let mods = Ghostty.ghosttyMods(event.modifierFlags)
ghostty_inspector_key(inspector, action, key, mods) ghostty_inspector_key(inspector, action, key, mods)
} }
// MARK: NSTextInputClient // MARK: NSTextInputClient
func hasMarkedText() -> Bool { func hasMarkedText() -> Bool {
@ -406,10 +406,10 @@ extension Ghostty {
default: default:
return return
} }
let len = chars.utf8CString.count let len = chars.utf8CString.count
if (len == 0) { return } if (len == 0) { return }
chars.withCString { ptr in chars.withCString { ptr in
ghostty_inspector_text(inspector, ptr) ghostty_inspector_text(inspector, ptr)
} }
@ -419,25 +419,25 @@ extension Ghostty {
// This currently just prevents NSBeep from interpretKeyEvents but in the future // This currently just prevents NSBeep from interpretKeyEvents but in the future
// we may want to make some of this work. // we may want to make some of this work.
} }
// MARK: MTKView // MARK: MTKView
override func draw(_ dirtyRect: NSRect) { override func draw(_ dirtyRect: NSRect) {
guard guard
let commandBuffer = self.commandQueue.makeCommandBuffer(), let commandBuffer = self.commandQueue.makeCommandBuffer(),
let descriptor = self.currentRenderPassDescriptor else { let descriptor = self.currentRenderPassDescriptor else {
return return
} }
// If the inspector is nil, then our surface is freed and it is unsafe // If the inspector is nil, then our surface is freed and it is unsafe
// to use. // to use.
guard let inspector = self.inspector else { return } guard let inspector = self.inspector else { return }
// We always update our size because sometimes draw is called // We always update our size because sometimes draw is called
// between resize events and if our size is wrong with the underlying // between resize events and if our size is wrong with the underlying
// drawable we will crash. // drawable we will crash.
updateSize() updateSize()
// Render // Render
ghostty_inspector_metal_render( ghostty_inspector_metal_render(
inspector, inspector,

View File

@ -8,7 +8,7 @@ struct Ghostty {
subsystem: Bundle.main.bundleIdentifier!, subsystem: Bundle.main.bundleIdentifier!,
category: "ghostty" category: "ghostty"
) )
// All the notifications that will be emitted will be put here. // All the notifications that will be emitted will be put here.
struct Notification {} struct Notification {}
@ -26,7 +26,7 @@ extension Ghostty {
var mode: ghostty_build_mode_e var mode: ghostty_build_mode_e
var version: String var version: String
} }
static var info: Info { static var info: Info {
let raw = ghostty_info() let raw = ghostty_info()
let version = NSString( let version = NSString(
@ -45,50 +45,50 @@ extension Ghostty {
/// An enum that is used for the directions that a split focus event can change. /// An enum that is used for the directions that a split focus event can change.
enum SplitFocusDirection { enum SplitFocusDirection {
case previous, next, top, bottom, left, right case previous, next, top, bottom, left, right
/// Initialize from a Ghostty API enum. /// Initialize from a Ghostty API enum.
static func from(direction: ghostty_split_focus_direction_e) -> Self? { static func from(direction: ghostty_split_focus_direction_e) -> Self? {
switch (direction) { switch (direction) {
case GHOSTTY_SPLIT_FOCUS_PREVIOUS: case GHOSTTY_SPLIT_FOCUS_PREVIOUS:
return .previous return .previous
case GHOSTTY_SPLIT_FOCUS_NEXT: case GHOSTTY_SPLIT_FOCUS_NEXT:
return .next return .next
case GHOSTTY_SPLIT_FOCUS_TOP: case GHOSTTY_SPLIT_FOCUS_TOP:
return .top return .top
case GHOSTTY_SPLIT_FOCUS_BOTTOM: case GHOSTTY_SPLIT_FOCUS_BOTTOM:
return .bottom return .bottom
case GHOSTTY_SPLIT_FOCUS_LEFT: case GHOSTTY_SPLIT_FOCUS_LEFT:
return .left return .left
case GHOSTTY_SPLIT_FOCUS_RIGHT: case GHOSTTY_SPLIT_FOCUS_RIGHT:
return .right return .right
default: default:
return nil return nil
} }
} }
func toNative() -> ghostty_split_focus_direction_e { func toNative() -> ghostty_split_focus_direction_e {
switch (self) { switch (self) {
case .previous: case .previous:
return GHOSTTY_SPLIT_FOCUS_PREVIOUS return GHOSTTY_SPLIT_FOCUS_PREVIOUS
case .next: case .next:
return GHOSTTY_SPLIT_FOCUS_NEXT return GHOSTTY_SPLIT_FOCUS_NEXT
case .top: case .top:
return GHOSTTY_SPLIT_FOCUS_TOP return GHOSTTY_SPLIT_FOCUS_TOP
case .bottom: case .bottom:
return GHOSTTY_SPLIT_FOCUS_BOTTOM return GHOSTTY_SPLIT_FOCUS_BOTTOM
case .left: case .left:
return GHOSTTY_SPLIT_FOCUS_LEFT return GHOSTTY_SPLIT_FOCUS_LEFT
case .right: case .right:
return GHOSTTY_SPLIT_FOCUS_RIGHT return GHOSTTY_SPLIT_FOCUS_RIGHT
} }
@ -177,49 +177,49 @@ extension Ghostty {
extension Ghostty.Notification { extension Ghostty.Notification {
/// Used to pass a configuration along when creating a new tab/window/split. /// Used to pass a configuration along when creating a new tab/window/split.
static let NewSurfaceConfigKey = "com.mitchellh.ghostty.newSurfaceConfig" static let NewSurfaceConfigKey = "com.mitchellh.ghostty.newSurfaceConfig"
/// Posted when a new split is requested. The sending object will be the surface that had focus. The /// Posted when a new split is requested. The sending object will be the surface that had focus. The
/// userdata has one key "direction" with the direction to split to. /// userdata has one key "direction" with the direction to split to.
static let ghosttyNewSplit = Notification.Name("com.mitchellh.ghostty.newSplit") static let ghosttyNewSplit = Notification.Name("com.mitchellh.ghostty.newSplit")
/// Close the calling surface. /// Close the calling surface.
static let ghosttyCloseSurface = Notification.Name("com.mitchellh.ghostty.closeSurface") static let ghosttyCloseSurface = Notification.Name("com.mitchellh.ghostty.closeSurface")
/// Focus previous/next split. Has a SplitFocusDirection in the userinfo. /// Focus previous/next split. Has a SplitFocusDirection in the userinfo.
static let ghosttyFocusSplit = Notification.Name("com.mitchellh.ghostty.focusSplit") static let ghosttyFocusSplit = Notification.Name("com.mitchellh.ghostty.focusSplit")
static let SplitDirectionKey = ghosttyFocusSplit.rawValue static let SplitDirectionKey = ghosttyFocusSplit.rawValue
/// Goto tab. Has tab index in the userinfo. /// Goto tab. Has tab index in the userinfo.
static let ghosttyGotoTab = Notification.Name("com.mitchellh.ghostty.gotoTab") static let ghosttyGotoTab = Notification.Name("com.mitchellh.ghostty.gotoTab")
static let GotoTabKey = ghosttyGotoTab.rawValue static let GotoTabKey = ghosttyGotoTab.rawValue
/// New tab. Has base surface config requested in userinfo. /// New tab. Has base surface config requested in userinfo.
static let ghosttyNewTab = Notification.Name("com.mitchellh.ghostty.newTab") static let ghosttyNewTab = Notification.Name("com.mitchellh.ghostty.newTab")
/// New window. Has base surface config requested in userinfo. /// New window. Has base surface config requested in userinfo.
static let ghosttyNewWindow = Notification.Name("com.mitchellh.ghostty.newWindow") static let ghosttyNewWindow = Notification.Name("com.mitchellh.ghostty.newWindow")
/// Toggle fullscreen of current window /// Toggle fullscreen of current window
static let ghosttyToggleFullscreen = Notification.Name("com.mitchellh.ghostty.toggleFullscreen") static let ghosttyToggleFullscreen = Notification.Name("com.mitchellh.ghostty.toggleFullscreen")
static let NonNativeFullscreenKey = ghosttyToggleFullscreen.rawValue static let NonNativeFullscreenKey = ghosttyToggleFullscreen.rawValue
/// Notification that a surface is becoming focused. This is only sent on macOS 12 to /// Notification that a surface is becoming focused. This is only sent on macOS 12 to
/// work around bugs. macOS 13+ should use the ".focused()" attribute. /// work around bugs. macOS 13+ should use the ".focused()" attribute.
static let didBecomeFocusedSurface = Notification.Name("com.mitchellh.ghostty.didBecomeFocusedSurface") static let didBecomeFocusedSurface = Notification.Name("com.mitchellh.ghostty.didBecomeFocusedSurface")
/// Notification sent to toggle split maximize/unmaximize. /// Notification sent to toggle split maximize/unmaximize.
static let didToggleSplitZoom = Notification.Name("com.mitchellh.ghostty.didToggleSplitZoom") static let didToggleSplitZoom = Notification.Name("com.mitchellh.ghostty.didToggleSplitZoom")
/// Notification /// Notification
static let didReceiveInitialWindowFrame = Notification.Name("com.mitchellh.ghostty.didReceiveInitialWindowFrame") static let didReceiveInitialWindowFrame = Notification.Name("com.mitchellh.ghostty.didReceiveInitialWindowFrame")
static let FrameKey = "com.mitchellh.ghostty.frame" static let FrameKey = "com.mitchellh.ghostty.frame"
/// Notification to render the inspector for a surface /// Notification to render the inspector for a surface
static let inspectorNeedsDisplay = Notification.Name("com.mitchellh.ghostty.inspectorNeedsDisplay") static let inspectorNeedsDisplay = Notification.Name("com.mitchellh.ghostty.inspectorNeedsDisplay")
/// Notification to show/hide the inspector /// Notification to show/hide the inspector
static let didControlInspector = Notification.Name("com.mitchellh.ghostty.didControlInspector") static let didControlInspector = Notification.Name("com.mitchellh.ghostty.didControlInspector")
static let confirmClipboard = Notification.Name("com.mitchellh.ghostty.confirmClipboard") static let confirmClipboard = Notification.Name("com.mitchellh.ghostty.confirmClipboard")
static let ConfirmClipboardStrKey = confirmClipboard.rawValue + ".str" static let ConfirmClipboardStrKey = confirmClipboard.rawValue + ".str"
static let ConfirmClipboardStateKey = confirmClipboard.rawValue + ".state" static let ConfirmClipboardStateKey = confirmClipboard.rawValue + ".state"
@ -232,7 +232,7 @@ extension Ghostty.Notification {
/// Notification sent to the split root to equalize split sizes /// Notification sent to the split root to equalize split sizes
static let didEqualizeSplits = Notification.Name("com.mitchellh.ghostty.didEqualizeSplits") static let didEqualizeSplits = Notification.Name("com.mitchellh.ghostty.didEqualizeSplits")
/// Notification that renderer health changed /// Notification that renderer health changed
static let didUpdateRendererHealth = Notification.Name("com.mitchellh.ghostty.didUpdateRendererHealth") static let didUpdateRendererHealth = Notification.Name("com.mitchellh.ghostty.didUpdateRendererHealth")
} }

View File

@ -33,7 +33,7 @@ extension Ghostty {
content(surfaceView) content(surfaceView)
} }
} }
struct SurfaceWrapper: View { struct SurfaceWrapper: View {
// The surface to create a view for. This must be created upstream. As long as this // The surface to create a view for. This must be created upstream. As long as this
// remains the same, the surface that is being rendered remains the same. // remains the same, the surface that is being rendered remains the same.
@ -42,21 +42,21 @@ extension Ghostty {
// True if this surface is part of a split view. This is important to know so // True if this surface is part of a split view. This is important to know so
// we know whether to dim the surface out of focus. // we know whether to dim the surface out of focus.
var isSplit: Bool = false var isSplit: Bool = false
// Maintain whether our view has focus or not // Maintain whether our view has focus or not
@FocusState private var surfaceFocus: Bool @FocusState private var surfaceFocus: Bool
// Maintain whether our window has focus (is key) or not // Maintain whether our window has focus (is key) or not
@State private var windowFocus: Bool = true @State private var windowFocus: Bool = true
// True if we're hovering over the left URL view, so we can show it on the right. // True if we're hovering over the left URL view, so we can show it on the right.
@State private var isHoveringURLLeft: Bool = false @State private var isHoveringURLLeft: Bool = false
@EnvironmentObject private var ghostty: Ghostty.App @EnvironmentObject private var ghostty: Ghostty.App
var body: some View { var body: some View {
let center = NotificationCenter.default let center = NotificationCenter.default
ZStack { ZStack {
// We use a GeometryReader to get the frame bounds so that our metal surface // We use a GeometryReader to get the frame bounds so that our metal surface
// is up to date. See TerminalSurfaceView for why we don't use the NSView // is up to date. See TerminalSurfaceView for why we don't use the NSView
@ -65,7 +65,7 @@ extension Ghostty {
// We use these notifications to determine when the window our surface is // We use these notifications to determine when the window our surface is
// attached to is or is not focused. // attached to is or is not focused.
let pubBecomeFocused = center.publisher(for: Notification.didBecomeFocusedSurface, object: surfaceView) let pubBecomeFocused = center.publisher(for: Notification.didBecomeFocusedSurface, object: surfaceView)
#if canImport(AppKit) #if canImport(AppKit)
let pubBecomeKey = center.publisher(for: NSWindow.didBecomeKeyNotification) let pubBecomeKey = center.publisher(for: NSWindow.didBecomeKeyNotification)
let pubResign = center.publisher(for: NSWindow.didResignKeyNotification) let pubResign = center.publisher(for: NSWindow.didResignKeyNotification)
@ -102,7 +102,7 @@ extension Ghostty {
} }
} }
} }
return true return true
} }
#endif #endif
@ -145,8 +145,8 @@ extension Ghostty {
// I don't know how older macOS versions behave but Ghostty only // I don't know how older macOS versions behave but Ghostty only
// supports back to macOS 12 so its moot. // supports back to macOS 12 so its moot.
} }
// If our geo size changed then we show the resize overlay as configured. // If our geo size changed then we show the resize overlay as configured.
if let surfaceSize = surfaceView.surfaceSize { if let surfaceSize = surfaceView.surfaceSize {
SurfaceResizeOverlay( SurfaceResizeOverlay(
geoSize: geo.size, geoSize: geo.size,
@ -155,11 +155,11 @@ extension Ghostty {
position: ghostty.config.resizeOverlayPosition, position: ghostty.config.resizeOverlayPosition,
duration: ghostty.config.resizeOverlayDuration, duration: ghostty.config.resizeOverlayDuration,
focusInstant: surfaceView.focusInstant) focusInstant: surfaceView.focusInstant)
} }
} }
.ghosttySurfaceView(surfaceView) .ghosttySurfaceView(surfaceView)
// If we have a URL from hovering a link, we show that. // If we have a URL from hovering a link, we show that.
if let url = surfaceView.hoverUrl { if let url = surfaceView.hoverUrl {
let padding: CGFloat = 3 let padding: CGFloat = 3
@ -168,7 +168,7 @@ extension Ghostty {
Spacer() Spacer()
VStack(alignment: .leading) { VStack(alignment: .leading) {
Spacer() Spacer()
Text(verbatim: url) Text(verbatim: url)
.padding(.init(top: padding, leading: padding, bottom: padding, trailing: padding)) .padding(.init(top: padding, leading: padding, bottom: padding, trailing: padding))
.background(.background) .background(.background)
@ -177,11 +177,11 @@ extension Ghostty {
.opacity(isHoveringURLLeft ? 1 : 0) .opacity(isHoveringURLLeft ? 1 : 0)
} }
} }
HStack { HStack {
VStack(alignment: .leading) { VStack(alignment: .leading) {
Spacer() Spacer()
Text(verbatim: url) Text(verbatim: url)
.padding(.init(top: padding, leading: padding, bottom: padding, trailing: padding)) .padding(.init(top: padding, leading: padding, bottom: padding, trailing: padding))
.background(.background) .background(.background)
@ -196,7 +196,7 @@ extension Ghostty {
} }
} }
} }
// If our surface is not healthy, then we render an error view over it. // 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) Rectangle().fill(ghostty.config.backgroundColor)
@ -222,7 +222,7 @@ extension Ghostty {
} }
} }
} }
struct SurfaceRendererUnhealthyView: View { struct SurfaceRendererUnhealthyView: View {
var body: some View { var body: some View {
HStack { HStack {
@ -230,7 +230,7 @@ extension Ghostty {
.resizable() .resizable()
.scaledToFit() .scaledToFit()
.frame(width: 128, height: 128) .frame(width: 128, height: 128)
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text("Oh, no. 😭").font(.title) Text("Oh, no. 😭").font(.title)
Text(""" Text("""
@ -244,7 +244,7 @@ extension Ghostty {
.padding() .padding()
} }
} }
struct SurfaceErrorView: View { struct SurfaceErrorView: View {
var body: some View { var body: some View {
HStack { HStack {
@ -252,7 +252,7 @@ extension Ghostty {
.resizable() .resizable()
.scaledToFit() .scaledToFit()
.frame(width: 128, height: 128) .frame(width: 128, height: 128)
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text("Oh, no. 😭").font(.title) Text("Oh, no. 😭").font(.title)
Text(""" Text("""
@ -266,7 +266,7 @@ extension Ghostty {
.padding() .padding()
} }
} }
// This is the resize overlay that shows on top of a surface to show the current // This is the resize overlay that shows on top of a surface to show the current
// size during a resize operation. // size during a resize operation.
struct SurfaceResizeOverlay: View { struct SurfaceResizeOverlay: View {
@ -276,26 +276,26 @@ extension Ghostty {
let position: Ghostty.Config.ResizeOverlayPosition let position: Ghostty.Config.ResizeOverlayPosition
let duration: UInt let duration: UInt
let focusInstant: Any? let focusInstant: Any?
// This is the last size that we processed. This is how we handle our // This is the last size that we processed. This is how we handle our
// timer state. // timer state.
@State var lastSize: CGSize? = nil @State var lastSize: CGSize? = nil
// Ready is set to true after a short delay. This avoids some of the // Ready is set to true after a short delay. This avoids some of the
// challenges of initial view sizing from SwiftUI. // challenges of initial view sizing from SwiftUI.
@State var ready: Bool = false @State var ready: Bool = false
// Fixed value set based on personal taste. // Fixed value set based on personal taste.
private let padding: CGFloat = 5 private let padding: CGFloat = 5
// This computed boolean is set to true when the overlay should be hidden. // This computed boolean is set to true when the overlay should be hidden.
private var hidden: Bool { private var hidden: Bool {
// If we aren't ready yet then we wait... // If we aren't ready yet then we wait...
if (!ready) { return true; } if (!ready) { return true; }
// Hidden if we already processed this size. // 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 // If we were focused recently we hide it as well. This avoids showing
// the resize overlay when SwiftUI is lazily resizing. // the resize overlay when SwiftUI is lazily resizing.
if #available(macOS 13, iOS 16, *) { if #available(macOS 13, iOS 16, *) {
@ -308,7 +308,7 @@ extension Ghostty {
} }
} }
} }
// Hidden depending on overlay config // Hidden depending on overlay config
switch (overlay) { switch (overlay) {
case .never: return true; case .never: return true;
@ -316,18 +316,18 @@ extension Ghostty {
case .after_first: return lastSize == nil; case .after_first: return lastSize == nil;
} }
} }
var body: some View { var body: some View {
VStack { VStack {
if (!position.top()) { if (!position.top()) {
Spacer() Spacer()
} }
HStack { HStack {
if (!position.left()) { if (!position.left()) {
Spacer() Spacer()
} }
Text(verbatim: "\(size.columns)c \(size.rows)r") Text(verbatim: "\(size.columns)c \(size.rows)r")
.padding(.init(top: padding, leading: padding, bottom: padding, trailing: padding)) .padding(.init(top: padding, leading: padding, bottom: padding, trailing: padding))
.background( .background(
@ -337,12 +337,12 @@ extension Ghostty {
) )
.lineLimit(1) .lineLimit(1)
.truncationMode(.tail) .truncationMode(.tail)
if (!position.right()) { if (!position.right()) {
Spacer() Spacer()
} }
} }
if (!position.bottom()) { if (!position.bottom()) {
Spacer() Spacer()
} }
@ -360,18 +360,18 @@ extension Ghostty {
// By ID-ing the task on the geoSize, we get the task to restart if our // By ID-ing the task on the geoSize, we get the task to restart if our
// geoSize changes. This also ensures that future resize overlays are shown // geoSize changes. This also ensures that future resize overlays are shown
// properly. // properly.
// We only sleep if we're ready. If we're not ready then we want to set // 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. // our last size right away to avoid a flash.
if (ready) { if (ready) {
try? await Task.sleep(nanoseconds: UInt64(duration) * 1_000_000) try? await Task.sleep(nanoseconds: UInt64(duration) * 1_000_000)
} }
lastSize = geoSize lastSize = geoSize
} }
} }
} }
/// A surface is terminology in Ghostty for a terminal surface, or a place where a terminal is actually drawn /// A surface is terminology in Ghostty for a terminal surface, or a place where a terminal is actually drawn
/// and interacted with. The word "surface" is used because a surface may represent a window, a tab, /// and interacted with. The word "surface" is used because a surface may represent a window, a tab,
/// a split, a small preview pane, etc. It is ANYTHING that has a terminal drawn to it. /// a split, a small preview pane, etc. It is ANYTHING that has a terminal drawn to it.
@ -404,27 +404,27 @@ extension Ghostty {
view.sizeDidChange(size) view.sizeDidChange(size)
} }
} }
/// The configuration for a surface. For any configuration not set, defaults will be chosen from /// The configuration for a surface. For any configuration not set, defaults will be chosen from
/// libghostty, usually from the Ghostty configuration. /// libghostty, usually from the Ghostty configuration.
struct SurfaceConfiguration { struct SurfaceConfiguration {
/// Explicit font size to use in points /// Explicit font size to use in points
var fontSize: Float32? = nil var fontSize: Float32? = nil
/// Explicit working directory to set /// Explicit working directory to set
var workingDirectory: String? = nil var workingDirectory: String? = nil
/// Explicit command to set /// Explicit command to set
var command: String? = nil var command: String? = nil
init() {} init() {}
init(from config: ghostty_surface_config_s) { init(from config: ghostty_surface_config_s) {
self.fontSize = config.font_size self.fontSize = config.font_size
self.workingDirectory = String.init(cString: config.working_directory, encoding: .utf8) self.workingDirectory = String.init(cString: config.working_directory, encoding: .utf8)
self.command = String.init(cString: config.command, encoding: .utf8) self.command = String.init(cString: config.command, encoding: .utf8)
} }
/// Returns the ghostty configuration for this surface configuration struct. The memory /// Returns the ghostty configuration for this surface configuration struct. The memory
/// in the returned struct is only valid as long as this struct is retained. /// in the returned struct is only valid as long as this struct is retained.
func ghosttyConfig(view: SurfaceView) -> ghostty_surface_config_s { func ghosttyConfig(view: SurfaceView) -> ghostty_surface_config_s {
@ -436,7 +436,7 @@ extension Ghostty {
nsview: Unmanaged.passUnretained(view).toOpaque() nsview: Unmanaged.passUnretained(view).toOpaque()
)) ))
config.scale_factor = NSScreen.main!.backingScaleFactor config.scale_factor = NSScreen.main!.backingScaleFactor
#elseif os(iOS) #elseif os(iOS)
config.platform_tag = GHOSTTY_PLATFORM_IOS config.platform_tag = GHOSTTY_PLATFORM_IOS
config.platform = ghostty_platform_u(ios: ghostty_platform_ios_s( config.platform = ghostty_platform_u(ios: ghostty_platform_ios_s(
@ -450,7 +450,7 @@ extension Ghostty {
#else #else
#error("unsupported target") #error("unsupported target")
#endif #endif
if let fontSize = fontSize { config.font_size = fontSize } if let fontSize = fontSize { config.font_size = fontSize }
if let workingDirectory = workingDirectory { if let workingDirectory = workingDirectory {
config.working_directory = (workingDirectory as NSString).utf8String config.working_directory = (workingDirectory as NSString).utf8String
@ -458,7 +458,7 @@ extension Ghostty {
if let command = command { if let command = command {
config.command = (command as NSString).utf8String config.command = (command as NSString).utf8String
} }
return config return config
} }
} }

View File

@ -8,7 +8,7 @@ extension Ghostty {
class SurfaceView: OSView, ObservableObject { class SurfaceView: OSView, ObservableObject {
/// Unique ID per surface /// Unique ID per surface
let uuid: UUID let uuid: UUID
// The current title of the surface as defined by the pty. This can be // The current title of the surface as defined by the pty. This can be
// changed with escape codes. This is public because the callbacks go // changed with escape codes. This is public because the callbacks go
// to the app level and it is set from there. // to the app level and it is set from there.
@ -19,57 +19,57 @@ extension Ghostty {
// when the font size changes). This is used to allow windows to be // when the font size changes). This is used to allow windows to be
// resized in discrete steps of a single cell. // resized in discrete steps of a single cell.
@Published var cellSize: NSSize = .zero @Published var cellSize: NSSize = .zero
// The health state of the surface. This currently only reflects the // The health state of the surface. This currently only reflects the
// renderer health. In the future we may want to make this an enum. // renderer health. In the future we may want to make this an enum.
@Published var healthy: Bool = true @Published var healthy: Bool = true
// Any error while initializing the surface. // Any error while initializing the surface.
@Published var error: Error? = nil @Published var error: Error? = nil
// The hovered URL string // The hovered URL string
@Published var hoverUrl: String? = nil @Published var hoverUrl: String? = nil
// The time this surface last became focused. This is a ContinuousClock.Instant // The time this surface last became focused. This is a ContinuousClock.Instant
// on supported platforms. // on supported platforms.
@Published var focusInstant: Any? = nil @Published var focusInstant: Any? = nil
// An initial size to request for a window. This will only affect // An initial size to request for a window. This will only affect
// then the view is moved to a new window. // then the view is moved to a new window.
var initialSize: NSSize? = nil var initialSize: NSSize? = nil
// Returns true if quit confirmation is required for this surface to // Returns true if quit confirmation is required for this surface to
// exit safely. // exit safely.
var needsConfirmQuit: Bool { var needsConfirmQuit: Bool {
guard let surface = self.surface else { return false } guard let surface = self.surface else { return false }
return ghostty_surface_needs_confirm_quit(surface) return ghostty_surface_needs_confirm_quit(surface)
} }
/// Returns the pwd of the surface if it has one. /// Returns the pwd of the surface if it has one.
var pwd: String? { var pwd: String? {
guard let surface = self.surface else { return nil } guard let surface = self.surface else { return nil }
let v = String(unsafeUninitializedCapacity: 1024) { let v = String(unsafeUninitializedCapacity: 1024) {
Int(ghostty_surface_pwd(surface, $0.baseAddress, UInt($0.count))) Int(ghostty_surface_pwd(surface, $0.baseAddress, UInt($0.count)))
} }
if (v.count == 0) { return nil } if (v.count == 0) { return nil }
return v return v
} }
// Returns sizing information for the surface. This is the raw C // Returns sizing information for the surface. This is the raw C
// structure because I'm lazy. // structure because I'm lazy.
var surfaceSize: ghostty_surface_size_s? { var surfaceSize: ghostty_surface_size_s? {
guard let surface = self.surface else { return nil } guard let surface = self.surface else { return nil }
return ghostty_surface_size(surface) return ghostty_surface_size(surface)
} }
// Returns the inspector instance for this surface, or nil if the // Returns the inspector instance for this surface, or nil if the
// surface has been closed. // surface has been closed.
var inspector: ghostty_inspector_t? { var inspector: ghostty_inspector_t? {
guard let surface = self.surface else { return nil } guard let surface = self.surface else { return nil }
return ghostty_surface_inspector(surface) return ghostty_surface_inspector(surface)
} }
// True if the inspector should be visible // True if the inspector should be visible
@Published var inspectorVisible: Bool = false { @Published var inspectorVisible: Bool = false {
didSet { didSet {
@ -82,7 +82,7 @@ extension Ghostty {
// Notification identifiers associated with this surface // Notification identifiers associated with this surface
var notificationIdentifiers: Set<String> = [] var notificationIdentifiers: Set<String> = []
private(set) var surface: ghostty_surface_t? private(set) var surface: ghostty_surface_t?
private var markedText: NSMutableAttributedString private var markedText: NSMutableAttributedString
private var mouseEntered: Bool = false private var mouseEntered: Bool = false
@ -91,10 +91,10 @@ extension Ghostty {
private var cursor: NSCursor = .iBeam private var cursor: NSCursor = .iBeam
private var cursorVisible: CursorVisibility = .visible private var cursorVisible: CursorVisibility = .visible
private var appearanceObserver: NSKeyValueObservation? = nil private var appearanceObserver: NSKeyValueObservation? = nil
// This is set to non-null during keyDown to accumulate insertText contents // This is set to non-null during keyDown to accumulate insertText contents
private var keyTextAccumulator: [String]? = nil private var keyTextAccumulator: [String]? = nil
// We need to support being a first responder so that we can get input events // We need to support being a first responder so that we can get input events
override var acceptsFirstResponder: Bool { return true } override var acceptsFirstResponder: Bool { return true }
@ -119,7 +119,7 @@ extension Ghostty {
// is non-zero so that our layer bounds are non-zero so that our renderer // is non-zero so that our layer bounds are non-zero so that our renderer
// can do SOMETHING. // can do SOMETHING.
super.init(frame: NSMakeRect(0, 0, 800, 600)) super.init(frame: NSMakeRect(0, 0, 800, 600))
// Before we initialize the surface we want to register our notifications // Before we initialize the surface we want to register our notifications
// so there is no window where we can't receive them. // so there is no window where we can't receive them.
let center = NotificationCenter.default let center = NotificationCenter.default
@ -133,7 +133,7 @@ extension Ghostty {
selector: #selector(windowDidChangeScreen), selector: #selector(windowDidChangeScreen),
name: NSWindow.didChangeScreenNotification, name: NSWindow.didChangeScreenNotification,
object: nil) object: nil)
// Setup our surface. This will also initialize all the terminal IO. // Setup our surface. This will also initialize all the terminal IO.
let surface_cfg = baseConfig ?? SurfaceConfiguration() let surface_cfg = baseConfig ?? SurfaceConfiguration()
var surface_cfg_c = surface_cfg.ghosttyConfig(view: self) var surface_cfg_c = surface_cfg.ghosttyConfig(view: self)
@ -142,10 +142,10 @@ extension Ghostty {
return return
} }
self.surface = surface; self.surface = surface;
// Setup our tracking area so we get mouse moved events // Setup our tracking area so we get mouse moved events
updateTrackingAreas() updateTrackingAreas()
// Observe our appearance so we can report the correct value to libghostty. // Observe our appearance so we can report the correct value to libghostty.
// This is the best way I know of to get appearance change notifications. // This is the best way I know of to get appearance change notifications.
self.appearanceObserver = observe(\.effectiveAppearance, options: [.new, .initial]) { view, change in self.appearanceObserver = observe(\.effectiveAppearance, options: [.new, .initial]) { view, change in
@ -155,14 +155,14 @@ extension Ghostty {
switch (appearance.name) { switch (appearance.name) {
case .aqua, .vibrantLight: case .aqua, .vibrantLight:
scheme = GHOSTTY_COLOR_SCHEME_LIGHT scheme = GHOSTTY_COLOR_SCHEME_LIGHT
case .darkAqua, .vibrantDark: case .darkAqua, .vibrantDark:
scheme = GHOSTTY_COLOR_SCHEME_DARK scheme = GHOSTTY_COLOR_SCHEME_DARK
default: default:
return return
} }
ghostty_surface_set_color_scheme(surface, scheme) ghostty_surface_set_color_scheme(surface, scheme)
} }
} }
@ -175,20 +175,20 @@ extension Ghostty {
// Remove all of our notificationcenter subscriptions // Remove all of our notificationcenter subscriptions
let center = NotificationCenter.default let center = NotificationCenter.default
center.removeObserver(self) center.removeObserver(self)
// Whenever the surface is removed, we need to note that our restorable // Whenever the surface is removed, we need to note that our restorable
// state is invalid to prevent the surface from being restored. // state is invalid to prevent the surface from being restored.
invalidateRestorableState() invalidateRestorableState()
trackingAreas.forEach { removeTrackingArea($0) } trackingAreas.forEach { removeTrackingArea($0) }
// mouseExited is not called by AppKit one last time when the view // mouseExited is not called by AppKit one last time when the view
// closes so we do it manually to ensure our NSCursor state remains // closes so we do it manually to ensure our NSCursor state remains
// accurate. // accurate.
if (mouseEntered) { if (mouseEntered) {
mouseExited(with: NSEvent()) mouseExited(with: NSEvent())
} }
guard let surface = self.surface else { return } guard let surface = self.surface else { return }
ghostty_surface_free(surface) ghostty_surface_free(surface)
} }
@ -212,7 +212,7 @@ extension Ghostty {
guard self.focused != focused else { return } guard self.focused != focused else { return }
self.focused = focused self.focused = focused
ghostty_surface_set_focus(surface, focused) ghostty_surface_set_focus(surface, focused)
// On macOS 13+ we can store our continuous clock... // On macOS 13+ we can store our continuous clock...
if #available(macOS 13, iOS 16, *) { if #available(macOS 13, iOS 16, *) {
if (focused) { if (focused) {
@ -230,7 +230,7 @@ extension Ghostty {
// The size represents our final size we're going for. // The size represents our final size we're going for.
let scaledSize = self.convertToBacking(size) let scaledSize = self.convertToBacking(size)
ghostty_surface_set_size(surface, UInt32(scaledSize.width), UInt32(scaledSize.height)) ghostty_surface_set_size(surface, UInt32(scaledSize.width), UInt32(scaledSize.height))
// Frame changes do not always call mouseEntered/mouseExited, so we do some // Frame changes do not always call mouseEntered/mouseExited, so we do some
// calculations ourself to call those events. // calculations ourself to call those events.
if let window = self.window { if let window = self.window {
@ -309,7 +309,7 @@ extension Ghostty {
window.invalidateCursorRects(for: self) window.invalidateCursorRects(for: self)
} }
} }
func setCursorVisibility(_ visible: Bool) { func setCursorVisibility(_ visible: Bool) {
switch (cursorVisible) { switch (cursorVisible) {
case .visible: case .visible:
@ -317,19 +317,19 @@ extension Ghostty {
// enter the pending state. // enter the pending state.
if (visible) { return } if (visible) { return }
cursorVisible = .pendingHidden cursorVisible = .pendingHidden
case .hidden: case .hidden:
// If we want to be hidden, do nothing. If we want to be visible // If we want to be hidden, do nothing. If we want to be visible
// enter the pending state. // enter the pending state.
if (!visible) { return } if (!visible) { return }
cursorVisible = .pendingVisible cursorVisible = .pendingVisible
case .pendingVisible: case .pendingVisible:
// If we want to be visible, do nothing because we're already pending. // If we want to be visible, do nothing because we're already pending.
// If we want to be hidden, we're already hidden so reset state. // If we want to be hidden, we're already hidden so reset state.
if (visible) { return } if (visible) { return }
cursorVisible = .hidden cursorVisible = .hidden
case .pendingHidden: case .pendingHidden:
// If we want to be hidden, do nothing because we're pending that switch. // If we want to be hidden, do nothing because we're pending that switch.
// If we want to be visible, we're already visible so reset state. // If we want to be visible, we're already visible so reset state.
@ -341,30 +341,30 @@ extension Ghostty {
cursorUpdate(with: NSEvent()) cursorUpdate(with: NSEvent())
} }
} }
// MARK: - Notifications // MARK: - Notifications
@objc private func onUpdateRendererHealth(notification: SwiftUI.Notification) { @objc private func onUpdateRendererHealth(notification: SwiftUI.Notification) {
guard let healthAny = notification.userInfo?["health"] else { return } guard let healthAny = notification.userInfo?["health"] else { return }
guard let health = healthAny as? ghostty_renderer_health_e else { return } guard let health = healthAny as? ghostty_renderer_health_e else { return }
healthy = health == GHOSTTY_RENDERER_HEALTH_OK healthy = health == GHOSTTY_RENDERER_HEALTH_OK
} }
@objc private func windowDidChangeScreen(notification: SwiftUI.Notification) { @objc private func windowDidChangeScreen(notification: SwiftUI.Notification) {
guard let window = self.window else { return } guard let window = self.window else { return }
guard let object = notification.object as? NSWindow, window == object else { return } guard let object = notification.object as? NSWindow, window == object else { return }
guard let screen = window.screen else { return } guard let screen = window.screen else { return }
guard let surface = self.surface else { return } guard let surface = self.surface else { return }
// When the window changes screens, we need to update libghostty with the screen // When the window changes screens, we need to update libghostty with the screen
// ID. If vsync is enabled, this will be used with the CVDisplayLink to ensure // ID. If vsync is enabled, this will be used with the CVDisplayLink to ensure
// the proper refresh rate is going. // the proper refresh rate is going.
let id = (screen.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as! NSNumber).uint32Value let id = (screen.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as! NSNumber).uint32Value
ghostty_surface_set_display_id(surface, id) ghostty_surface_set_display_id(surface, id)
} }
// MARK: - NSView // MARK: - NSView
override func becomeFirstResponder() -> Bool { override func becomeFirstResponder() -> Bool {
let result = super.becomeFirstResponder() let result = super.becomeFirstResponder()
if (result) { focusDidChange(true) } if (result) { focusDidChange(true) }
@ -391,7 +391,7 @@ extension Ghostty {
options: [ options: [
.mouseEnteredAndExited, .mouseEnteredAndExited,
.mouseMoved, .mouseMoved,
// Only send mouse events that happen in our visible (not obscured) rect // Only send mouse events that happen in our visible (not obscured) rect
.inVisibleRect, .inVisibleRect,
@ -410,7 +410,7 @@ extension Ghostty {
override func viewDidChangeBackingProperties() { override func viewDidChangeBackingProperties() {
super.viewDidChangeBackingProperties() super.viewDidChangeBackingProperties()
// The Core Animation compositing engine uses the layer's contentsScale property // The Core Animation compositing engine uses the layer's contentsScale property
// to determine whether to scale its contents during compositing. When the window // to determine whether to scale its contents during compositing. When the window
// moves between a high DPI display and a low DPI display, or the user modifies // moves between a high DPI display and a low DPI display, or the user modifies
@ -431,7 +431,7 @@ extension Ghostty {
layer?.contentsScale = window.backingScaleFactor layer?.contentsScale = window.backingScaleFactor
CATransaction.commit() CATransaction.commit()
} }
guard let surface = self.surface else { return } guard let surface = self.surface else { return }
// Detect our X/Y scale factor so we can update our surface // Detect our X/Y scale factor so we can update our surface
@ -448,7 +448,7 @@ extension Ghostty {
guard let surface = self.surface else { return } guard let surface = self.surface else { return }
ghostty_surface_draw(surface); ghostty_surface_draw(surface);
} }
override func acceptsFirstMouse(for event: NSEvent?) -> Bool { override func acceptsFirstMouse(for event: NSEvent?) -> Bool {
// "Override this method in a subclass to allow instances to respond to // "Override this method in a subclass to allow instances to respond to
// click-through. This allows the user to click on a view in an inactive // click-through. This allows the user to click on a view in an inactive
@ -466,12 +466,12 @@ extension Ghostty {
override func mouseUp(with event: NSEvent) { override func mouseUp(with event: NSEvent) {
// Always reset our pressure when the mouse goes up // Always reset our pressure when the mouse goes up
prevPressureStage = 0 prevPressureStage = 0
// If we have an active surface, report the event // If we have an active surface, report the event
guard let surface = self.surface else { return } guard let surface = self.surface else { return }
let mods = Ghostty.ghosttyMods(event.modifierFlags) let mods = Ghostty.ghosttyMods(event.modifierFlags)
ghostty_surface_mouse_button(surface, GHOSTTY_MOUSE_RELEASE, GHOSTTY_MOUSE_LEFT, mods) ghostty_surface_mouse_button(surface, GHOSTTY_MOUSE_RELEASE, GHOSTTY_MOUSE_LEFT, mods)
// Release pressure // Release pressure
ghostty_surface_mouse_pressure(surface, 0, 0) ghostty_surface_mouse_pressure(surface, 0, 0)
} }
@ -493,7 +493,7 @@ extension Ghostty {
override func rightMouseDown(with event: NSEvent) { override func rightMouseDown(with event: NSEvent) {
guard let surface = self.surface else { return super.rightMouseDown(with: event) } guard let surface = self.surface else { return super.rightMouseDown(with: event) }
let mods = Ghostty.ghosttyMods(event.modifierFlags) let mods = Ghostty.ghosttyMods(event.modifierFlags)
if (ghostty_surface_mouse_button( if (ghostty_surface_mouse_button(
surface, surface,
@ -504,14 +504,14 @@ extension Ghostty {
// Consumed // Consumed
return return
} }
// Mouse event not consumed // Mouse event not consumed
super.rightMouseDown(with: event) super.rightMouseDown(with: event)
} }
override func rightMouseUp(with event: NSEvent) { override func rightMouseUp(with event: NSEvent) {
guard let surface = self.surface else { return super.rightMouseUp(with: event) } guard let surface = self.surface else { return super.rightMouseUp(with: event) }
let mods = Ghostty.ghosttyMods(event.modifierFlags) let mods = Ghostty.ghosttyMods(event.modifierFlags)
if (ghostty_surface_mouse_button( if (ghostty_surface_mouse_button(
surface, surface,
@ -522,14 +522,14 @@ extension Ghostty {
// Handled // Handled
return return
} }
// Mouse event not consumed // Mouse event not consumed
super.rightMouseUp(with: event) super.rightMouseUp(with: event)
} }
override func mouseMoved(with event: NSEvent) { override func mouseMoved(with event: NSEvent) {
guard let surface = self.surface else { return } guard let surface = self.surface else { return }
// Convert window position to view position. Note (0, 0) is bottom left. // Convert window position to view position. Note (0, 0) is bottom left.
let pos = self.convert(event.locationInWindow, from: nil) let pos = self.convert(event.locationInWindow, from: nil)
ghostty_surface_mouse_pos(surface, pos.x, frame.height - pos.y) ghostty_surface_mouse_pos(surface, pos.x, frame.height - pos.y)
@ -554,9 +554,9 @@ extension Ghostty {
// tab is created. In this scenario, we only want to process our // tab is created. In this scenario, we only want to process our
// callback once since this is stateful and we expect balancing. // callback once since this is stateful and we expect balancing.
if (mouseEntered) { return } if (mouseEntered) { return }
mouseEntered = true mouseEntered = true
// Update our cursor when we enter so we fully process our // Update our cursor when we enter so we fully process our
// cursorVisible state. // cursorVisible state.
cursorUpdate(with: NSEvent()) cursorUpdate(with: NSEvent())
@ -565,9 +565,9 @@ extension Ghostty {
override func mouseExited(with event: NSEvent) { override func mouseExited(with event: NSEvent) {
// See mouseEntered // See mouseEntered
if (!mouseEntered) { return } if (!mouseEntered) { return }
mouseEntered = false mouseEntered = false
// If the mouse is currently hidden, we want to show it when we exit // If the mouse is currently hidden, we want to show it when we exit
// this view. We go through the cursorVisible dance so that only // this view. We go through the cursorVisible dance so that only
// cursorUpdate manages cursor state. // cursorUpdate manages cursor state.
@ -575,7 +575,7 @@ extension Ghostty {
cursorVisible = .pendingVisible cursorVisible = .pendingVisible
cursorUpdate(with: NSEvent()) cursorUpdate(with: NSEvent())
assert(cursorVisible == .visible) assert(cursorVisible == .visible)
// We set the state to pending hidden again for the next time // We set the state to pending hidden again for the next time
// we enter. // we enter.
cursorVisible = .pendingHidden cursorVisible = .pendingHidden
@ -624,42 +624,42 @@ extension Ghostty {
ghostty_surface_mouse_scroll(surface, x, y, mods) ghostty_surface_mouse_scroll(surface, x, y, mods)
} }
override func pressureChange(with event: NSEvent) { override func pressureChange(with event: NSEvent) {
guard let surface = self.surface else { return } guard let surface = self.surface else { return }
// Notify Ghostty first. We do this because this will let Ghostty handle // Notify Ghostty first. We do this because this will let Ghostty handle
// state setup that we'll need for later pressure handling (such as // state setup that we'll need for later pressure handling (such as
// QuickLook) // QuickLook)
ghostty_surface_mouse_pressure(surface, UInt32(event.stage), Double(event.pressure)) ghostty_surface_mouse_pressure(surface, UInt32(event.stage), Double(event.pressure))
// Pressure stage 2 is force click. We only want to execute this on the // Pressure stage 2 is force click. We only want to execute this on the
// initial transition to stage 2, and not for any repeated events. // initial transition to stage 2, and not for any repeated events.
guard self.prevPressureStage < 2 else { return } guard self.prevPressureStage < 2 else { return }
prevPressureStage = event.stage prevPressureStage = event.stage
guard event.stage == 2 else { return } guard event.stage == 2 else { return }
// If the user has force click enabled then we do a quick look. There // If the user has force click enabled then we do a quick look. There
// is no public API for this as far as I can tell. // is no public API for this as far as I can tell.
guard UserDefaults.standard.bool(forKey: "com.apple.trackpad.forceClick") else { return } guard UserDefaults.standard.bool(forKey: "com.apple.trackpad.forceClick") else { return }
quickLook(with: event) quickLook(with: event)
} }
override func cursorUpdate(with event: NSEvent) { override func cursorUpdate(with event: NSEvent) {
switch (cursorVisible) { switch (cursorVisible) {
case .visible, .hidden: case .visible, .hidden:
// Do nothing, stable state // Do nothing, stable state
break break
case .pendingHidden: case .pendingHidden:
NSCursor.hide() NSCursor.hide()
cursorVisible = .hidden cursorVisible = .hidden
case .pendingVisible: case .pendingVisible:
NSCursor.unhide() NSCursor.unhide()
cursorVisible = .visible cursorVisible = .visible
} }
cursor.set() cursor.set()
} }
@ -668,7 +668,7 @@ extension Ghostty {
self.interpretKeyEvents([event]) self.interpretKeyEvents([event])
return return
} }
// We need to translate the mods (maybe) to handle configs such as option-as-alt // We need to translate the mods (maybe) to handle configs such as option-as-alt
let translationModsGhostty = Ghostty.eventModifierFlags( let translationModsGhostty = Ghostty.eventModifierFlags(
mods: ghostty_surface_key_translation_mods( mods: ghostty_surface_key_translation_mods(
@ -676,7 +676,7 @@ extension Ghostty {
Ghostty.ghosttyMods(event.modifierFlags) Ghostty.ghosttyMods(event.modifierFlags)
) )
) )
// There are hidden bits set in our event that matter for certain dead keys // There are hidden bits set in our event that matter for certain dead keys
// so we can't use translationModsGhostty directly. Instead, we just check // so we can't use translationModsGhostty directly. Instead, we just check
// for exact states and set them. // for exact states and set them.
@ -711,21 +711,21 @@ extension Ghostty {
keyCode: event.keyCode keyCode: event.keyCode
) ?? event ) ?? event
} }
let action = event.isARepeat ? GHOSTTY_ACTION_REPEAT : GHOSTTY_ACTION_PRESS let action = event.isARepeat ? GHOSTTY_ACTION_REPEAT : GHOSTTY_ACTION_PRESS
// By setting this to non-nil, we note that we're in a keyDown event. From here, // By setting this to non-nil, we note that we're in a keyDown event. From here,
// we call interpretKeyEvents so that we can handle complex input such as Korean // we call interpretKeyEvents so that we can handle complex input such as Korean
// language. // language.
keyTextAccumulator = [] keyTextAccumulator = []
defer { keyTextAccumulator = nil } defer { keyTextAccumulator = nil }
// We need to know what the length of marked text was before this event to // We need to know what the length of marked text was before this event to
// know if these events cleared it. // know if these events cleared it.
let markedTextBefore = markedText.length > 0 let markedTextBefore = markedText.length > 0
self.interpretKeyEvents([translationEvent]) self.interpretKeyEvents([translationEvent])
// If we have text, then we've composed a character, send that down. We do this // If we have text, then we've composed a character, send that down. We do this
// first because if we completed a preedit, the text will be available here // first because if we completed a preedit, the text will be available here
// AND we'll have a preedit. // AND we'll have a preedit.
@ -736,7 +736,7 @@ extension Ghostty {
keyAction(action, event: event, text: text) keyAction(action, event: event, text: text)
} }
} }
// If we have marked text, we're in a preedit state. Send that down. // If we have marked text, we're in a preedit state. Send that down.
// If we don't have marked text but we had marked text before, then the preedit // If we don't have marked text but we had marked text before, then the preedit
// was cleared so we want to send down an empty string to ensure we've cleared // was cleared so we want to send down an empty string to ensure we've cleared
@ -745,7 +745,7 @@ extension Ghostty {
handled = true handled = true
keyAction(action, event: event, preedit: markedText.string) keyAction(action, event: event, preedit: markedText.string)
} }
if (!handled) { if (!handled) {
// No text or anything, we want to handle this manually. // No text or anything, we want to handle this manually.
keyAction(action, event: event) keyAction(action, event: event)
@ -768,7 +768,7 @@ extension Ghostty {
if (event.type != .keyDown) { if (event.type != .keyDown) {
return false return false
} }
// Only process events if we're focused. Some key events like C-/ macOS // 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 // 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. // the first responder (I don't know why). This prevents us from handling it.
@ -782,7 +782,7 @@ extension Ghostty {
// Treat C-/ as C-_. We do this because C-/ makes macOS make a beep // Treat C-/ as C-_. We do this because C-/ makes macOS make a beep
// sound and we don't like the beep sound. // sound and we don't like the beep sound.
equivalent = "_" equivalent = "_"
default: default:
// Ignore other events // Ignore other events
return false return false
@ -840,18 +840,18 @@ extension Ghostty {
default: default:
sidePressed = true sidePressed = true
} }
if (sidePressed) { if (sidePressed) {
action = GHOSTTY_ACTION_PRESS action = GHOSTTY_ACTION_PRESS
} }
} }
keyAction(action, event: event) keyAction(action, event: event)
} }
private func keyAction(_ action: ghostty_input_action_e, event: NSEvent) { private func keyAction(_ action: ghostty_input_action_e, event: NSEvent) {
guard let surface = self.surface else { return } guard let surface = self.surface else { return }
var key_ev = ghostty_input_key_s() var key_ev = ghostty_input_key_s()
key_ev.action = action key_ev.action = action
key_ev.mods = Ghostty.ghosttyMods(event.modifierFlags) key_ev.mods = Ghostty.ghosttyMods(event.modifierFlags)
@ -860,7 +860,7 @@ extension Ghostty {
key_ev.composing = false key_ev.composing = false
ghostty_surface_key(surface, key_ev) ghostty_surface_key(surface, key_ev)
} }
private func keyAction(_ action: ghostty_input_action_e, event: NSEvent, preedit: String) { private func keyAction(_ action: ghostty_input_action_e, event: NSEvent, preedit: String) {
guard let surface = self.surface else { return } guard let surface = self.surface else { return }
@ -887,17 +887,17 @@ extension Ghostty {
ghostty_surface_key(surface, key_ev) ghostty_surface_key(surface, key_ev)
} }
} }
override func quickLook(with event: NSEvent) { override func quickLook(with event: NSEvent) {
guard let surface = self.surface else { return super.quickLook(with: event) } guard let surface = self.surface else { return super.quickLook(with: event) }
// Grab the text under the cursor // Grab the text under the cursor
var info: ghostty_selection_s = ghostty_selection_s(); var info: ghostty_selection_s = ghostty_selection_s();
let text = String(unsafeUninitializedCapacity: 1000000) { let text = String(unsafeUninitializedCapacity: 1000000) {
Int(ghostty_surface_quicklook_word(surface, $0.baseAddress, UInt($0.count), &info)) Int(ghostty_surface_quicklook_word(surface, $0.baseAddress, UInt($0.count), &info))
} }
guard !text.isEmpty else { return super.quickLook(with: event) } guard !text.isEmpty else { return super.quickLook(with: event) }
// If we can get a font then we use the font. This should always work // If we can get a font then we use the font. This should always work
// since we always have a primary font. The only scenario this doesn't // 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 // work is if someone is using a non-CoreText build which would be
@ -911,25 +911,25 @@ extension Ghostty {
attributes[.font] = font.takeUnretainedValue() attributes[.font] = font.takeUnretainedValue()
font.release() font.release()
} }
// Ghostty coordinate system is top-left, convert to bottom-left for AppKit // Ghostty coordinate system is top-left, convert to bottom-left for AppKit
let pt = NSMakePoint(info.tl_px_x - 2, frame.size.height - info.tl_px_y + 2) let pt = NSMakePoint(info.tl_px_x - 2, frame.size.height - info.tl_px_y + 2)
let str = NSAttributedString.init(string: text, attributes: attributes) let str = NSAttributedString.init(string: text, attributes: attributes)
self.showDefinition(for: str, at: pt); self.showDefinition(for: str, at: pt);
} }
override func menu(for event: NSEvent) -> NSMenu? { override func menu(for event: NSEvent) -> NSMenu? {
// We only support right-click menus // We only support right-click menus
switch event.type { switch event.type {
case .rightMouseDown: case .rightMouseDown:
// Good // Good
break break
case .leftMouseDown: case .leftMouseDown:
if !event.modifierFlags.contains(.control) { if !event.modifierFlags.contains(.control) {
return nil return nil
} }
// In this case, AppKit calls menu BEFORE calling any mouse events. // In this case, AppKit calls menu BEFORE calling any mouse events.
// If mouse capturing is enabled then we never show the context menu // If mouse capturing is enabled then we never show the context menu
// so that we can handle ctrl+left-click in the terminal app. // so that we can handle ctrl+left-click in the terminal app.
@ -937,7 +937,7 @@ extension Ghostty {
if ghostty_surface_mouse_captured(surface) { if ghostty_surface_mouse_captured(surface) {
return nil return nil
} }
// If we return a non-nil menu then mouse events will never be // If we return a non-nil menu then mouse events will never be
// processed by the core, so we need to manually send a right // processed by the core, so we need to manually send a right
// mouse down event. // mouse down event.
@ -951,13 +951,13 @@ extension Ghostty {
GHOSTTY_MOUSE_RIGHT, GHOSTTY_MOUSE_RIGHT,
mods mods
) )
default: default:
return nil return nil
} }
let menu = NSMenu() let menu = NSMenu()
// If we have a selection, add copy // If we have a selection, add copy
if self.selectedRange().length > 0 { if self.selectedRange().length > 0 {
menu.addItem(withTitle: "Copy", action: #selector(copy(_:)), keyEquivalent: "") menu.addItem(withTitle: "Copy", action: #selector(copy(_:)), keyEquivalent: "")
@ -974,7 +974,7 @@ extension Ghostty {
return menu return menu
} }
// MARK: Menu Handlers // MARK: Menu Handlers
@IBAction func copy(_ sender: Any?) { @IBAction func copy(_ sender: Any?) {
@ -992,7 +992,7 @@ extension Ghostty {
AppDelegate.logger.warning("action failed action=\(action)") AppDelegate.logger.warning("action failed action=\(action)")
} }
} }
@IBAction func pasteAsPlainText(_ sender: Any?) { @IBAction func pasteAsPlainText(_ sender: Any?) {
guard let surface = self.surface else { return } guard let surface = self.surface else { return }
@ -1001,7 +1001,7 @@ extension Ghostty {
AppDelegate.logger.warning("action failed action=\(action)") AppDelegate.logger.warning("action failed action=\(action)")
} }
} }
@IBAction override func selectAll(_ sender: Any?) { @IBAction override func selectAll(_ sender: Any?) {
guard let surface = self.surface else { return } guard let surface = self.surface else { return }
let action = "select_all" let action = "select_all"
@ -1063,7 +1063,7 @@ extension Ghostty.SurfaceView: NSTextInputClient {
func selectedRange() -> NSRange { func selectedRange() -> NSRange {
guard let surface = self.surface else { return NSRange() } guard let surface = self.surface else { return NSRange() }
// Get our range from the Ghostty API. There is a race condition between getting the // Get our range from the Ghostty API. There is a race condition between getting the
// range and actually using it since our selection may change but there isn't a good // range and actually using it since our selection may change but there isn't a good
// way I can think of to solve this for AppKit. // way I can think of to solve this for AppKit.
@ -1097,21 +1097,21 @@ extension Ghostty.SurfaceView: NSTextInputClient {
// Ghostty.logger.warning("pressure substring range=\(range) selectedRange=\(self.selectedRange())") // Ghostty.logger.warning("pressure substring range=\(range) selectedRange=\(self.selectedRange())")
guard let surface = self.surface else { return nil } guard let surface = self.surface else { return nil }
guard ghostty_surface_has_selection(surface) else { return nil } guard ghostty_surface_has_selection(surface) else { return nil }
// If the range is empty then we don't need to return anything // If the range is empty then we don't need to return anything
guard range.length > 0 else { return nil } guard range.length > 0 else { return nil }
// I used to do a bunch of testing here that the range requested matches the // I used to do a bunch of testing here that the range requested matches the
// selection range or contains it but a lot of macOS system behaviors request // selection range or contains it but a lot of macOS system behaviors request
// bogus ranges I truly don't understand so we just always return the // bogus ranges I truly don't understand so we just always return the
// attributed string containing our selection which is... weird but works? // attributed string containing our selection which is... weird but works?
// Get our selection. We cap it at 1MB for the purpose of this. This is // Get our selection. We cap it at 1MB for the purpose of this. This is
// arbitrary. If this is a good reason to increase it I'm happy to. // arbitrary. If this is a good reason to increase it I'm happy to.
let v = String(unsafeUninitializedCapacity: 1000000) { let v = String(unsafeUninitializedCapacity: 1000000) {
Int(ghostty_surface_selection(surface, $0.baseAddress, UInt($0.count))) Int(ghostty_surface_selection(surface, $0.baseAddress, UInt($0.count)))
} }
// If we can get a font then we use the font. This should always work // If we can get a font then we use the font. This should always work
// since we always have a primary font. The only scenario this doesn't // 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 // work is if someone is using a non-CoreText build which would be
@ -1137,11 +1137,11 @@ extension Ghostty.SurfaceView: NSTextInputClient {
guard let surface = self.surface else { guard let surface = self.surface else {
return NSMakeRect(frame.origin.x, frame.origin.y, 0, 0) return NSMakeRect(frame.origin.x, frame.origin.y, 0, 0)
} }
// Ghostty will tell us where it thinks an IME keyboard should render. // Ghostty will tell us where it thinks an IME keyboard should render.
var x: Double = 0; var x: Double = 0;
var y: Double = 0; var y: Double = 0;
// QuickLook never gives us a matching range to our selection so if we detect // QuickLook never gives us a matching range to our selection so if we detect
// this then we return the top-left selection point rather than the cursor point. // this then we return the top-left selection point rather than the cursor point.
// This is hacky but I can't think of a better way to get the right IME vs. QuickLook // This is hacky but I can't think of a better way to get the right IME vs. QuickLook
@ -1164,7 +1164,7 @@ extension Ghostty.SurfaceView: NSTextInputClient {
// Ghostty coordinates are in top-left (0, 0) so we have to convert to // Ghostty coordinates are in top-left (0, 0) so we have to convert to
// bottom-left since that is what UIKit expects // bottom-left since that is what UIKit expects
let viewRect = NSMakeRect(x, frame.size.height - y, 0, 0) let viewRect = NSMakeRect(x, frame.size.height - y, 0, 0)
// Convert the point to the window coordinates // Convert the point to the window coordinates
let winRect = self.convert(viewRect, to: nil) let winRect = self.convert(viewRect, to: nil)
@ -1188,10 +1188,10 @@ extension Ghostty.SurfaceView: NSTextInputClient {
default: default:
return return
} }
// If insertText is called, our preedit must be over. // If insertText is called, our preedit must be over.
unmarkText() unmarkText()
// If we have an accumulator we're in another key event so we just // If we have an accumulator we're in another key event so we just
// accumulate and return. // accumulate and return.
if var acc = keyTextAccumulator { if var acc = keyTextAccumulator {
@ -1199,10 +1199,10 @@ extension Ghostty.SurfaceView: NSTextInputClient {
keyTextAccumulator = acc keyTextAccumulator = acc
return return
} }
let len = chars.utf8CString.count let len = chars.utf8CString.count
if (len == 0) { return } if (len == 0) { return }
chars.withCString { ptr in chars.withCString { ptr in
// len includes the null terminator so we do len - 1 // len includes the null terminator so we do len - 1
ghostty_surface_text(surface, ptr, UInt(len - 1)) ghostty_surface_text(surface, ptr, UInt(len - 1))
@ -1227,49 +1227,49 @@ extension Ghostty.SurfaceView: NSServicesMenuRequestor {
) -> Any? { ) -> Any? {
// Types that we accept sent to us // Types that we accept sent to us
let accepted: [NSPasteboard.PasteboardType] = [.string, .init("public.utf8-plain-text")] let accepted: [NSPasteboard.PasteboardType] = [.string, .init("public.utf8-plain-text")]
// We can always receive the accepted types // We can always receive the accepted types
if (returnType == nil || accepted.contains(returnType!)) { if (returnType == nil || accepted.contains(returnType!)) {
return self return self
} }
// If we have a selection we can send the accepted types too // If we have a selection we can send the accepted types too
if ((self.surface != nil && ghostty_surface_has_selection(self.surface)) && if ((self.surface != nil && ghostty_surface_has_selection(self.surface)) &&
(sendType == nil || accepted.contains(sendType!)) (sendType == nil || accepted.contains(sendType!))
) { ) {
return self return self
} }
return super.validRequestor(forSendType: sendType, returnType: returnType) return super.validRequestor(forSendType: sendType, returnType: returnType)
} }
func writeSelection( func writeSelection(
to pboard: NSPasteboard, to pboard: NSPasteboard,
types: [NSPasteboard.PasteboardType] types: [NSPasteboard.PasteboardType]
) -> Bool { ) -> Bool {
guard let surface = self.surface else { return false } guard let surface = self.surface else { return false }
// We currently cap the maximum copy size to 1MB. iTerm2 I believe // We currently cap the maximum copy size to 1MB. iTerm2 I believe
// caps theirs at 0.1MB (configurable) so this is probably reasonable. // caps theirs at 0.1MB (configurable) so this is probably reasonable.
let v = String(unsafeUninitializedCapacity: 1000000) { let v = String(unsafeUninitializedCapacity: 1000000) {
Int(ghostty_surface_selection(surface, $0.baseAddress, UInt($0.count))) Int(ghostty_surface_selection(surface, $0.baseAddress, UInt($0.count)))
} }
pboard.declareTypes([.string], owner: nil) pboard.declareTypes([.string], owner: nil)
pboard.setString(v, forType: .string) pboard.setString(v, forType: .string)
return true return true
} }
func readSelection(from pboard: NSPasteboard) -> Bool { func readSelection(from pboard: NSPasteboard) -> Bool {
guard let str = pboard.getOpinionatedStringContents() else { return false } guard let str = pboard.getOpinionatedStringContents() else { return false }
let len = str.utf8CString.count let len = str.utf8CString.count
if (len == 0) { return true } if (len == 0) { return true }
str.withCString { ptr in str.withCString { ptr in
// len includes the null terminator so we do len - 1 // len includes the null terminator so we do len - 1
ghostty_surface_text(surface, ptr, UInt(len - 1)) ghostty_surface_text(surface, ptr, UInt(len - 1))
} }
return true return true
} }
} }

View File

@ -6,7 +6,7 @@ extension Ghostty {
class SurfaceView: UIView, ObservableObject { class SurfaceView: UIView, ObservableObject {
/// Unique ID per surface /// Unique ID per surface
let uuid: UUID let uuid: UUID
// The current title of the surface as defined by the pty. This can be // The current title of the surface as defined by the pty. This can be
// changed with escape codes. This is public because the callbacks go // changed with escape codes. This is public because the callbacks go
// to the app level and it is set from there. // to the app level and it is set from there.
@ -17,30 +17,30 @@ extension Ghostty {
// when the font size changes). This is used to allow windows to be // when the font size changes). This is used to allow windows to be
// resized in discrete steps of a single cell. // resized in discrete steps of a single cell.
@Published var cellSize: OSSize = .zero @Published var cellSize: OSSize = .zero
// The health state of the surface. This currently only reflects the // The health state of the surface. This currently only reflects the
// renderer health. In the future we may want to make this an enum. // renderer health. In the future we may want to make this an enum.
@Published var healthy: Bool = true @Published var healthy: Bool = true
// Any error while initializing the surface. // Any error while initializing the surface.
@Published var error: Error? = nil @Published var error: Error? = nil
// The hovered URL // The hovered URL
@Published var hoverUrl: String? = nil @Published var hoverUrl: String? = nil
// The time this surface last became focused. This is a ContinuousClock.Instant // The time this surface last became focused. This is a ContinuousClock.Instant
// on supported platforms. // on supported platforms.
@Published var focusInstant: Any? = nil @Published var focusInstant: Any? = nil
// Returns sizing information for the surface. This is the raw C // Returns sizing information for the surface. This is the raw C
// structure because I'm lazy. // structure because I'm lazy.
var surfaceSize: ghostty_surface_size_s? { var surfaceSize: ghostty_surface_size_s? {
guard let surface = self.surface else { return nil } guard let surface = self.surface else { return nil }
return ghostty_surface_size(surface) return ghostty_surface_size(surface)
} }
private(set) var surface: ghostty_surface_t? private(set) var surface: ghostty_surface_t?
init(_ app: ghostty_app_t, baseConfig: SurfaceConfiguration? = nil, uuid: UUID? = nil) { init(_ app: ghostty_app_t, baseConfig: SurfaceConfiguration? = nil, uuid: UUID? = nil) {
self.uuid = uuid ?? .init() self.uuid = uuid ?? .init()
@ -58,7 +58,7 @@ extension Ghostty {
} }
self.surface = surface; self.surface = surface;
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
fatalError("init(coder:) is not supported for this view") fatalError("init(coder:) is not supported for this view")
} }
@ -67,11 +67,11 @@ extension Ghostty {
guard let surface = self.surface else { return } guard let surface = self.surface else { return }
ghostty_surface_free(surface) ghostty_surface_free(surface)
} }
func focusDidChange(_ focused: Bool) { func focusDidChange(_ focused: Bool) {
guard let surface = self.surface else { return } guard let surface = self.surface else { return }
ghostty_surface_set_focus(surface, focused) ghostty_surface_set_focus(surface, focused)
// On macOS 13+ we can store our continuous clock... // On macOS 13+ we can store our continuous clock...
if #available(macOS 13, iOS 16, *) { if #available(macOS 13, iOS 16, *) {
if (focused) { if (focused) {
@ -95,15 +95,15 @@ extension Ghostty {
UInt32(size.height * scale) UInt32(size.height * scale)
) )
} }
// MARK: UIView // MARK: UIView
override class var layerClass: AnyClass { override class var layerClass: AnyClass {
get { get {
return CAMetalLayer.self return CAMetalLayer.self
} }
} }
override func didMoveToWindow() { override func didMoveToWindow() {
sizeDidChange(frame.size) sizeDidChange(frame.size)
} }

View File

@ -4,16 +4,16 @@ import Cocoa
class CodableBridge<Wrapped: Codable>: NSObject, NSSecureCoding { class CodableBridge<Wrapped: Codable>: NSObject, NSSecureCoding {
let value: Wrapped let value: Wrapped
init(_ value: Wrapped) { self.value = value } init(_ value: Wrapped) { self.value = value }
static var supportsSecureCoding: Bool { return true } static var supportsSecureCoding: Bool { return true }
required init?(coder aDecoder: NSCoder) { required init?(coder aDecoder: NSCoder) {
guard let data = aDecoder.decodeObject(of: NSData.self, forKey: "data") as? Data else { return nil } guard let data = aDecoder.decodeObject(of: NSData.self, forKey: "data") as? Data else { return nil }
guard let archiver = try? NSKeyedUnarchiver(forReadingFrom: data) else { return nil } guard let archiver = try? NSKeyedUnarchiver(forReadingFrom: data) else { return nil }
guard let value = archiver.decodeDecodable(Wrapped.self, forKey: "value") else { return nil } guard let value = archiver.decodeDecodable(Wrapped.self, forKey: "value") else { return nil }
self.value = value self.value = value
} }
func encode(with aCoder: NSCoder) { func encode(with aCoder: NSCoder) {
let archiver = NSKeyedArchiver(requiringSecureCoding: true) let archiver = NSKeyedArchiver(requiringSecureCoding: true)
try? archiver.encodeEncodable(value, forKey: "value") try? archiver.encodeEncodable(value, forKey: "value")

View File

@ -1,18 +1,18 @@
import SwiftUI import SwiftUI
import GhosttyKit import GhosttyKit
class FullScreenHandler { class FullScreenHandler {
var previousTabGroup: NSWindowTabGroup? var previousTabGroup: NSWindowTabGroup?
var previousTabGroupIndex: Int? var previousTabGroupIndex: Int?
var previousContentFrame: NSRect? var previousContentFrame: NSRect?
var previousStyleMask: NSWindow.StyleMask? = nil var previousStyleMask: NSWindow.StyleMask? = nil
// We keep track of whether we entered non-native fullscreen in case // We keep track of whether we entered non-native fullscreen in case
// a user goes to fullscreen, changes the config to disable non-native fullscreen // a user goes to fullscreen, changes the config to disable non-native fullscreen
// and then wants to toggle it off // and then wants to toggle it off
var isInNonNativeFullscreen: Bool = false var isInNonNativeFullscreen: Bool = false
var isInFullscreen: Bool = false var isInFullscreen: Bool = false
func toggleFullscreen(window: NSWindow, nonNativeFullscreen: ghostty_non_native_fullscreen_e) { func toggleFullscreen(window: NSWindow, nonNativeFullscreen: ghostty_non_native_fullscreen_e) {
let useNonNativeFullscreen = nonNativeFullscreen != GHOSTTY_NON_NATIVE_FULLSCREEN_FALSE let useNonNativeFullscreen = nonNativeFullscreen != GHOSTTY_NON_NATIVE_FULLSCREEN_FALSE
if isInFullscreen { if isInFullscreen {
@ -40,17 +40,17 @@ class FullScreenHandler {
isInFullscreen = true isInFullscreen = true
} }
} }
func enterFullscreen(window: NSWindow, hideMenu: Bool) { func enterFullscreen(window: NSWindow, hideMenu: Bool) {
guard let screen = window.screen else { return } guard let screen = window.screen else { return }
guard let contentView = window.contentView else { return } guard let contentView = window.contentView else { return }
previousTabGroup = window.tabGroup previousTabGroup = window.tabGroup
previousTabGroupIndex = window.tabGroup?.windows.firstIndex(of: window) previousTabGroupIndex = window.tabGroup?.windows.firstIndex(of: window)
// Save previous contentViewFrame and screen // Save previous contentViewFrame and screen
previousContentFrame = window.convertToScreen(contentView.frame) previousContentFrame = window.convertToScreen(contentView.frame)
// Change presentation style to hide menu bar and dock if needed // Change presentation style to hide menu bar and dock if needed
// It's important to do this in two calls, because setting them in a single call guarantees // It's important to do this in two calls, because setting them in a single call guarantees
// that the menu bar will also be hidden on any additional displays (why? nobody knows!) // that the menu bar will also be hidden on any additional displays (why? nobody knows!)
@ -61,7 +61,7 @@ class FullScreenHandler {
// has not yet been hidden, so the order matters here! // has not yet been hidden, so the order matters here!
if (shouldHideDock(screen: screen)) { if (shouldHideDock(screen: screen)) {
self.hideDock() self.hideDock()
// Ensure that we always hide the dock bar for this window, but not for non fullscreen ones // Ensure that we always hide the dock bar for this window, but not for non fullscreen ones
NotificationCenter.default.addObserver( NotificationCenter.default.addObserver(
self, self,
@ -76,7 +76,7 @@ class FullScreenHandler {
} }
if (hideMenu) { if (hideMenu) {
self.hideMenu() self.hideMenu()
// Ensure that we always hide the menu bar for this window, but not for non fullscreen ones // Ensure that we always hide the menu bar for this window, but not for non fullscreen ones
// This is not the best way to do this, not least because it causes the menu to stay visible // This is not the best way to do this, not least because it causes the menu to stay visible
// for a brief moment before being hidden in some cases (e.g. when switching spaces). // for a brief moment before being hidden in some cases (e.g. when switching spaces).
@ -93,28 +93,28 @@ class FullScreenHandler {
name: NSWindow.didResignMainNotification, name: NSWindow.didResignMainNotification,
object: window) object: window)
} }
// This is important: it gives us the full screen, including the // This is important: it gives us the full screen, including the
// notch area on MacBooks. // notch area on MacBooks.
self.previousStyleMask = window.styleMask self.previousStyleMask = window.styleMask
window.styleMask.remove(.titled) window.styleMask.remove(.titled)
// Set frame to screen size, accounting for the menu bar if needed // Set frame to screen size, accounting for the menu bar if needed
let frame = calculateFullscreenFrame(screen: screen, subtractMenu: !hideMenu) let frame = calculateFullscreenFrame(screen: screen, subtractMenu: !hideMenu)
window.setFrame(frame, display: true) window.setFrame(frame, display: true)
// Focus window // Focus window
window.makeKeyAndOrderFront(nil) window.makeKeyAndOrderFront(nil)
} }
@objc func hideMenu() { @objc func hideMenu() {
NSApp.presentationOptions.insert(.autoHideMenuBar) NSApp.presentationOptions.insert(.autoHideMenuBar)
} }
@objc func onDidResignMain(_ notification: Notification) { @objc func onDidResignMain(_ notification: Notification) {
guard let resigningWindow = notification.object as? NSWindow else { return } guard let resigningWindow = notification.object as? NSWindow else { return }
guard let mainWindow = NSApplication.shared.mainWindow else { return } guard let mainWindow = NSApplication.shared.mainWindow else { return }
// We're only unhiding the menu bar, if the focus shifted within our application. // We're only unhiding the menu bar, if the focus shifted within our application.
// In that case, `mainWindow` is the window of our application the focus shifted // In that case, `mainWindow` is the window of our application the focus shifted
// to. // to.
@ -122,20 +122,20 @@ class FullScreenHandler {
NSApp.presentationOptions.remove(.autoHideMenuBar) NSApp.presentationOptions.remove(.autoHideMenuBar)
} }
} }
@objc func hideDock() { @objc func hideDock() {
NSApp.presentationOptions.insert(.autoHideDock) NSApp.presentationOptions.insert(.autoHideDock)
} }
@objc func unHideDock() { @objc func unHideDock() {
NSApp.presentationOptions.remove(.autoHideDock) NSApp.presentationOptions.remove(.autoHideDock)
} }
func calculateFullscreenFrame(screen: NSScreen, subtractMenu: Bool)->NSRect { func calculateFullscreenFrame(screen: NSScreen, subtractMenu: Bool)->NSRect {
if (subtractMenu) { if (subtractMenu) {
if let menuHeight = NSApp.mainMenu?.menuBarHeight { if let menuHeight = NSApp.mainMenu?.menuBarHeight {
var padding: CGFloat = 0 var padding: CGFloat = 0
// Detect the notch. If there is a safe area on top it includes the // Detect the notch. If there is a safe area on top it includes the
// menu height as a safe area so we also subtract that from it. // menu height as a safe area so we also subtract that from it.
if (screen.safeAreaInsets.top > 0) { if (screen.safeAreaInsets.top > 0) {
@ -152,34 +152,34 @@ class FullScreenHandler {
} }
return screen.frame return screen.frame
} }
func leaveFullscreen(window: NSWindow) { func leaveFullscreen(window: NSWindow) {
guard let previousFrame = previousContentFrame else { return } guard let previousFrame = previousContentFrame else { return }
// Restore the style mask // Restore the style mask
window.styleMask = self.previousStyleMask! window.styleMask = self.previousStyleMask!
// Restore previous presentation options // Restore previous presentation options
NSApp.presentationOptions = [] NSApp.presentationOptions = []
// Stop handling any window focus notifications // Stop handling any window focus notifications
// that we use to manage menu bar visibility // that we use to manage menu bar visibility
NotificationCenter.default.removeObserver(self, name: NSWindow.didBecomeMainNotification, object: window) NotificationCenter.default.removeObserver(self, name: NSWindow.didBecomeMainNotification, object: window)
NotificationCenter.default.removeObserver(self, name: NSWindow.didResignMainNotification, object: window) NotificationCenter.default.removeObserver(self, name: NSWindow.didResignMainNotification, object: window)
// Restore frame // Restore frame
window.setFrame(window.frameRect(forContentRect: previousFrame), display: true) window.setFrame(window.frameRect(forContentRect: previousFrame), display: true)
// Have titlebar tabs set itself up again, since removing the titlebar when fullscreen breaks its constraints. // Have titlebar tabs set itself up again, since removing the titlebar when fullscreen breaks its constraints.
if let window = window as? TerminalWindow, window.titlebarTabs { if let window = window as? TerminalWindow, window.titlebarTabs {
window.titlebarTabs = true window.titlebarTabs = true
} }
// If the window was previously in a tab group that isn't empty now, we re-add it // If the window was previously in a tab group that isn't empty now, we re-add it
if let group = previousTabGroup, let tabIndex = previousTabGroupIndex, !group.windows.isEmpty { if let group = previousTabGroup, let tabIndex = previousTabGroupIndex, !group.windows.isEmpty {
var tabWindow: NSWindow? var tabWindow: NSWindow?
var order: NSWindow.OrderingMode = .below var order: NSWindow.OrderingMode = .below
// Index of the window before `window` // Index of the window before `window`
let tabIndexBefore = tabIndex-1 let tabIndexBefore = tabIndex-1
if tabIndexBefore < 0 { if tabIndexBefore < 0 {
@ -194,15 +194,15 @@ class FullScreenHandler {
// If index is after group, add it after last window // If index is after group, add it after last window
tabWindow = group.windows.last tabWindow = group.windows.last
} }
// Add the window // Add the window
tabWindow?.addTabbedWindow(window, ordered: order) tabWindow?.addTabbedWindow(window, ordered: order)
} }
// Focus window // Focus window
window.makeKeyAndOrderFront(nil) window.makeKeyAndOrderFront(nil)
} }
// We only want to hide the dock if it's not already going to be hidden automatically, and if // We only want to hide the dock if it's not already going to be hidden automatically, and if
// it's on the same display as the ghostty window that we want to make fullscreen. // it's on the same display as the ghostty window that we want to make fullscreen.
func shouldHideDock(screen: NSScreen) -> Bool { func shouldHideDock(screen: NSScreen) -> Bool {

View File

@ -16,7 +16,7 @@ fileprivate struct MetalViewRepresentable<V: MTKView>: NSViewRepresentable {
func makeNSView(context: Context) -> some NSView { func makeNSView(context: Context) -> some NSView {
metalView metalView
} }
func updateNSView(_ view: NSViewType, context: Context) { func updateNSView(_ view: NSViewType, context: Context) {
updateMetalView() updateMetalView()
} }

View File

@ -4,13 +4,13 @@ extension OSColor {
var isLightColor: Bool { var isLightColor: Bool {
return self.luminance > 0.5 return self.luminance > 0.5
} }
var luminance: Double { var luminance: Double {
var r: CGFloat = 0 var r: CGFloat = 0
var g: CGFloat = 0 var g: CGFloat = 0
var b: CGFloat = 0 var b: CGFloat = 0
var a: CGFloat = 0 var a: CGFloat = 0
// getRed:green:blue:alpha requires sRGB space // getRed:green:blue:alpha requires sRGB space
#if canImport(AppKit) #if canImport(AppKit)
guard let rgb = self.usingColorSpace(.sRGB) else { return 0 } guard let rgb = self.usingColorSpace(.sRGB) else { return 0 }

View File

@ -7,7 +7,7 @@ extension SplitView {
let visibleSize: CGFloat let visibleSize: CGFloat
let invisibleSize: CGFloat let invisibleSize: CGFloat
let color: Color let color: Color
private var visibleWidth: CGFloat? { private var visibleWidth: CGFloat? {
switch (direction) { switch (direction) {
case .horizontal: case .horizontal:
@ -16,7 +16,7 @@ extension SplitView {
return nil return nil
} }
} }
private var visibleHeight: CGFloat? { private var visibleHeight: CGFloat? {
switch (direction) { switch (direction) {
case .horizontal: case .horizontal:
@ -25,7 +25,7 @@ extension SplitView {
return visibleSize return visibleSize
} }
} }
private var invisibleWidth: CGFloat? { private var invisibleWidth: CGFloat? {
switch (direction) { switch (direction) {
case .horizontal: case .horizontal:
@ -34,7 +34,7 @@ extension SplitView {
return nil return nil
} }
} }
private var invisibleHeight: CGFloat? { private var invisibleHeight: CGFloat? {
switch (direction) { switch (direction) {
case .horizontal: case .horizontal:

View File

@ -9,17 +9,17 @@ import Combine
struct SplitView<L: View, R: View>: View { struct SplitView<L: View, R: View>: View {
/// Direction of the split /// Direction of the split
let direction: SplitViewDirection let direction: SplitViewDirection
/// Divider color /// Divider color
let dividerColor: Color let dividerColor: Color
/// If set, the split view supports programmatic resizing via events sent via the publisher. /// If set, the split view supports programmatic resizing via events sent via the publisher.
/// Minimum increment (in points) that this split can be resized by, in /// Minimum increment (in points) that this split can be resized by, in
/// each direction. Both `height` and `width` should be whole numbers /// each direction. Both `height` and `width` should be whole numbers
/// greater than or equal to 1.0 /// greater than or equal to 1.0
let resizeIncrements: NSSize let resizeIncrements: NSSize
let resizePublisher: PassthroughSubject<Double, Never> let resizePublisher: PassthroughSubject<Double, Never>
/// The left and right views to render. /// The left and right views to render.
let left: L let left: L
let right: R let right: R
@ -34,13 +34,13 @@ struct SplitView<L: View, R: View>: View {
/// be used for getting a resize handle. The total width/height of the splitter is the sum of both. /// be used for getting a resize handle. The total width/height of the splitter is the sum of both.
private let splitterVisibleSize: CGFloat = 1 private let splitterVisibleSize: CGFloat = 1
private let splitterInvisibleSize: CGFloat = 6 private let splitterInvisibleSize: CGFloat = 6
var body: some View { var body: some View {
GeometryReader { geo in GeometryReader { geo in
let leftRect = self.leftRect(for: geo.size) let leftRect = self.leftRect(for: geo.size)
let rightRect = self.rightRect(for: geo.size, leftRect: leftRect) let rightRect = self.rightRect(for: geo.size, leftRect: leftRect)
let splitterPoint = self.splitterPoint(for: geo.size, leftRect: leftRect) let splitterPoint = self.splitterPoint(for: geo.size, leftRect: leftRect)
ZStack(alignment: .topLeading) { ZStack(alignment: .topLeading) {
left left
.frame(width: leftRect.size.width, height: leftRect.size.height) .frame(width: leftRect.size.width, height: leftRect.size.height)
@ -48,7 +48,7 @@ struct SplitView<L: View, R: View>: View {
right right
.frame(width: rightRect.size.width, height: rightRect.size.height) .frame(width: rightRect.size.width, height: rightRect.size.height)
.offset(x: rightRect.origin.x, y: rightRect.origin.y) .offset(x: rightRect.origin.x, y: rightRect.origin.y)
Divider(direction: direction, Divider(direction: direction,
visibleSize: splitterVisibleSize, visibleSize: splitterVisibleSize,
invisibleSize: splitterInvisibleSize, invisibleSize: splitterInvisibleSize,
color: dividerColor) color: dividerColor)
@ -60,11 +60,11 @@ struct SplitView<L: View, R: View>: View {
} }
} }
} }
/// Initialize a split view. This view isn't programmatically resizable; it can only be resized /// Initialize a split view. This view isn't programmatically resizable; it can only be resized
/// by manually dragging the divider. /// by manually dragging the divider.
init(_ direction: SplitViewDirection, init(_ direction: SplitViewDirection,
_ split: Binding<CGFloat>, _ split: Binding<CGFloat>,
dividerColor: Color, dividerColor: Color,
@ViewBuilder left: (() -> L), @ViewBuilder left: (() -> L),
@ViewBuilder right: (() -> R)) { @ViewBuilder right: (() -> R)) {
@ -78,7 +78,7 @@ struct SplitView<L: View, R: View>: View {
right: right right: right
) )
} }
/// Initialize a split view that supports programmatic resizing. /// Initialize a split view that supports programmatic resizing.
init( init(
_ direction: SplitViewDirection, _ direction: SplitViewDirection,
@ -97,7 +97,7 @@ struct SplitView<L: View, R: View>: View {
self.left = left() self.left = left()
self.right = right() self.right = right()
} }
private func resize(for size: CGSize, amount: Double) { private func resize(for size: CGSize, amount: Double) {
let dim: CGFloat let dim: CGFloat
switch (direction) { switch (direction) {
@ -119,14 +119,14 @@ struct SplitView<L: View, R: View>: View {
case .horizontal: case .horizontal:
let new = min(max(minSize, gesture.location.x), size.width - minSize) let new = min(max(minSize, gesture.location.x), size.width - minSize)
split = new / size.width split = new / size.width
case .vertical: case .vertical:
let new = min(max(minSize, gesture.location.y), size.height - minSize) let new = min(max(minSize, gesture.location.y), size.height - minSize)
split = new / size.height split = new / size.height
} }
} }
} }
/// Calculates the bounding rect for the left view. /// Calculates the bounding rect for the left view.
private func leftRect(for size: CGSize) -> CGRect { private func leftRect(for size: CGSize) -> CGRect {
// Initially the rect is the full size // Initially the rect is the full size
@ -136,16 +136,16 @@ struct SplitView<L: View, R: View>: View {
result.size.width = result.size.width * split result.size.width = result.size.width * split
result.size.width -= splitterVisibleSize / 2 result.size.width -= splitterVisibleSize / 2
result.size.width -= result.size.width.truncatingRemainder(dividingBy: self.resizeIncrements.width) result.size.width -= result.size.width.truncatingRemainder(dividingBy: self.resizeIncrements.width)
case .vertical: case .vertical:
result.size.height = result.size.height * split result.size.height = result.size.height * split
result.size.height -= splitterVisibleSize / 2 result.size.height -= splitterVisibleSize / 2
result.size.height -= result.size.height.truncatingRemainder(dividingBy: self.resizeIncrements.height) result.size.height -= result.size.height.truncatingRemainder(dividingBy: self.resizeIncrements.height)
} }
return result return result
} }
/// Calculates the bounding rect for the right view. /// Calculates the bounding rect for the right view.
private func rightRect(for size: CGSize, leftRect: CGRect) -> CGRect { private func rightRect(for size: CGSize, leftRect: CGRect) -> CGRect {
// Initially the rect is the full size // Initially the rect is the full size
@ -157,22 +157,22 @@ struct SplitView<L: View, R: View>: View {
result.origin.x += leftRect.size.width result.origin.x += leftRect.size.width
result.origin.x += splitterVisibleSize / 2 result.origin.x += splitterVisibleSize / 2
result.size.width -= result.origin.x result.size.width -= result.origin.x
case .vertical: case .vertical:
result.origin.y += leftRect.size.height result.origin.y += leftRect.size.height
result.origin.y += splitterVisibleSize / 2 result.origin.y += splitterVisibleSize / 2
result.size.height -= result.origin.y result.size.height -= result.origin.y
} }
return result return result
} }
/// Calculates the point at which the splitter should be rendered. /// Calculates the point at which the splitter should be rendered.
private func splitterPoint(for size: CGSize, leftRect: CGRect) -> CGPoint { private func splitterPoint(for size: CGSize, leftRect: CGRect) -> CGPoint {
switch (direction) { switch (direction) {
case .horizontal: case .horizontal:
return CGPoint(x: leftRect.size.width, y: size.height / 2) return CGPoint(x: leftRect.size.width, y: size.height / 2)
case .vertical: case .vertical:
return CGPoint(x: size.width / 2, y: leftRect.size.height) return CGPoint(x: size.width / 2, y: leftRect.size.height)
} }

View File

@ -6,7 +6,7 @@ extension String {
} }
return self.prefix(maxLength) + trailing return self.prefix(maxLength) + trailing
} }
#if canImport(AppKit) #if canImport(AppKit)
func temporaryFile(_ filename: String = "temp") -> URL { func temporaryFile(_ filename: String = "temp") -> URL {
let url = FileManager.default.temporaryDirectory let url = FileManager.default.temporaryDirectory