style(macos): cleanup trailing spaces
parent
140d1dde5a
commit
7ff9af1520
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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, " +
|
||||||
|
|
|
||||||
|
|
@ -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.")
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue