diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index e20b635aa..a525abd94 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -27,6 +27,9 @@ A571AB1D2A206FCF00248498 /* GhosttyKit.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A5D495A1299BEC7E00DD1313 /* GhosttyKit.xcframework */; }; A59444F729A2ED5200725BBA /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59444F629A2ED5200725BBA /* SettingsView.swift */; }; A59630972AEE163600D64628 /* HostingWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59630962AEE163600D64628 /* HostingWindow.swift */; }; + A596309A2AEE1C6400D64628 /* Terminal.xib in Resources */ = {isa = PBXBuildFile; fileRef = A59630992AEE1C6400D64628 /* Terminal.xib */; }; + A596309C2AEE1C9E00D64628 /* TerminalController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A596309B2AEE1C9E00D64628 /* TerminalController.swift */; }; + A596309E2AEE1D6C00D64628 /* TerminalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A596309D2AEE1D6C00D64628 /* TerminalView.swift */; }; A59FB5CF2AE0DB50009128F3 /* InspectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59FB5CE2AE0DB50009128F3 /* InspectorView.swift */; }; A59FB5D12AE0DEA7009128F3 /* MetalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59FB5D02AE0DEA7009128F3 /* MetalView.swift */; }; A5A1F8852A489D6800D1E8BC /* terminfo in Resources */ = {isa = PBXBuildFile; fileRef = A5A1F8842A489D6800D1E8BC /* terminfo */; }; @@ -62,6 +65,9 @@ A571AB1C2A206FC600248498 /* Ghostty-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = "Ghostty-Info.plist"; sourceTree = ""; }; A59444F629A2ED5200725BBA /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; A59630962AEE163600D64628 /* HostingWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostingWindow.swift; sourceTree = ""; }; + A59630992AEE1C6400D64628 /* Terminal.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = Terminal.xib; sourceTree = ""; }; + A596309B2AEE1C9E00D64628 /* TerminalController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalController.swift; sourceTree = ""; }; + A596309D2AEE1D6C00D64628 /* TerminalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalView.swift; sourceTree = ""; }; A59FB5CE2AE0DB50009128F3 /* InspectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InspectorView.swift; sourceTree = ""; }; A59FB5D02AE0DEA7009128F3 /* MetalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetalView.swift; sourceTree = ""; }; A5A1F8842A489D6800D1E8BC /* terminfo */ = {isa = PBXFileReference; lastKnownFileType = folder; name = terminfo; path = "../zig-out/share/terminfo"; sourceTree = ""; }; @@ -97,6 +103,7 @@ children = ( A56D58872ACDE6BE00508D2C /* Services */, A53426372A7DC53A00EBB7A2 /* Primary Window */, + A59630982AEE1C4400D64628 /* Terminal */, A534263E2A7DCC5800EBB7A2 /* Settings */, ); path = Features; @@ -173,6 +180,16 @@ path = Services; sourceTree = ""; }; + A59630982AEE1C4400D64628 /* Terminal */ = { + isa = PBXGroup; + children = ( + A59630992AEE1C6400D64628 /* Terminal.xib */, + A596309B2AEE1C9E00D64628 /* TerminalController.swift */, + A596309D2AEE1D6C00D64628 /* TerminalView.swift */, + ); + path = Terminal; + sourceTree = ""; + }; A5A1F8862A489D7400D1E8BC /* Resources */ = { isa = PBXGroup; children = ( @@ -280,6 +297,7 @@ buildActionMask = 2147483647; files = ( A545D1A22A5772CE006E0AE4 /* shell-integration in Resources */, + A596309A2AEE1C6400D64628 /* Terminal.xib in Resources */, A5A1F8852A489D6800D1E8BC /* terminfo in Resources */, A5CDF1912AAF9A5800513312 /* ConfigurationErrors.xib in Resources */, A5B30539299BEAAB0047F10C /* Assets.xcassets in Resources */, @@ -295,6 +313,7 @@ buildActionMask = 2147483647; files = ( A59FB5CF2AE0DB50009128F3 /* InspectorView.swift in Sources */, + A596309C2AEE1C9E00D64628 /* TerminalController.swift in Sources */, A53426392A7DC55C00EBB7A2 /* PrimaryWindowManager.swift in Sources */, 85DE1C922A6A3DCA00493853 /* PrimaryWindow.swift in Sources */, A56D58892ACDE6CA00508D2C /* ServiceProvider.swift in Sources */, @@ -318,6 +337,7 @@ 85102A1C2A6E32890084AB3E /* PrimaryWindowController.swift in Sources */, A5CEAFFF29C2410700646FDA /* Backport.swift in Sources */, 8503D7C72A549C66006CFF3D /* FullScreenHandler.swift in Sources */, + A596309E2AEE1D6C00D64628 /* TerminalView.swift in Sources */, A5CEAFDE29B8058B00646FDA /* SplitView.Divider.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/macos/Sources/AppDelegate.swift b/macos/Sources/AppDelegate.swift index cf9b53427..9c3afac13 100644 --- a/macos/Sources/AppDelegate.swift +++ b/macos/Sources/AppDelegate.swift @@ -73,11 +73,15 @@ class AppDelegate: NSObject, ObservableObject, NSApplicationDelegate, GhosttyApp // Let's launch our first window. // TODO: we should detect if we restored windows and if so not launch a new window. - windowManager.addInitialWindow() + // TODO: remove when TerminalController is done + // windowManager.addInitialWindow() // Initial config loading configDidReload(ghostty) + let c = TerminalController(ghostty) + c.showWindow(self) + // Register our service provider. This must happen after everything // else is initialized. NSApp.servicesProvider = ServiceProvider() diff --git a/macos/Sources/Features/Terminal/Terminal.xib b/macos/Sources/Features/Terminal/Terminal.xib new file mode 100644 index 000000000..f0f0937db --- /dev/null +++ b/macos/Sources/Features/Terminal/Terminal.xib @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift new file mode 100644 index 000000000..6fb3c6af6 --- /dev/null +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -0,0 +1,50 @@ +import Foundation +import Cocoa +import SwiftUI +import Combine + +class TerminalController: NSWindowController, NSWindowDelegate { + override var windowNibName: NSNib.Name? { "Terminal" } + + /// The app instance that this terminal view will represent. + let ghostty: Ghostty.AppState + + init(_ ghostty: Ghostty.AppState) { + self.ghostty = ghostty + super.init(window: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) is not supported for this view") + } + + //MARK: - NSWindowController + + override func windowWillLoad() { + // We want every new terminal window to cascade so they don't directly overlap. + shouldCascadeWindows = true + } + + override func windowDidLoad() { + guard let window = window else { return } + + // Terminals typically operate in sRGB color space and macOS defaults + // to "native" which is typically P3. There is a lot more resources + // covered in thie GitHub issue: https://github.com/mitchellh/ghostty/pull/376 + window.colorSpace = NSColorSpace.sRGB + + // Center the window to start, we'll move the window frame automatically + // when cascading. + window.center() + + // Initialize our content view to the SwiftUI root + window.contentView = NSHostingView(rootView: TerminalView( + ghostty: self.ghostty + )) + } + + //MARK: - NSWindowDelegate + + func windowWillClose(_ notification: Notification) { + } +} diff --git a/macos/Sources/Features/Terminal/TerminalView.swift b/macos/Sources/Features/Terminal/TerminalView.swift new file mode 100644 index 000000000..1edbb3ef9 --- /dev/null +++ b/macos/Sources/Features/Terminal/TerminalView.swift @@ -0,0 +1,65 @@ +import SwiftUI +import GhosttyKit + +struct TerminalView: View { + @ObservedObject var ghostty: Ghostty.AppState + + // This seems like a crutch after switching from SwiftUI to AppKit lifecycle. + @FocusState private var focused: Bool + + @FocusedValue(\.ghosttySurfaceView) private var focusedSurface + @FocusedValue(\.ghosttySurfaceTitle) private var surfaceTitle + @FocusedValue(\.ghosttySurfaceZoomed) private var zoomedSplit + @FocusedValue(\.ghosttySurfaceCellSize) private var cellSize + + // The title for our window + private var title: String { + var title = "👻" + + if let surfaceTitle = surfaceTitle { + if (surfaceTitle.count > 0) { + title = surfaceTitle + } + } + + if let zoomedSplit = zoomedSplit { + if zoomedSplit { + title = "🔍 " + title + } + } + + return title + } + + var body: some View { + switch ghostty.readiness { + case .loading: + Text("Loading") + case .error: + ErrorView() + case .ready: + let center = NotificationCenter.default + let gotoTab = center.publisher(for: Ghostty.Notification.ghosttyGotoTab) + let toggleFullscreen = center.publisher(for: Ghostty.Notification.ghosttyToggleFullscreen) + + VStack(spacing: 0) { + // If we're running in debug mode we show a warning so that users + // know that performance will be degraded. + if (ghostty.info.mode == GHOSTTY_BUILD_MODE_DEBUG) { + DebugBuildWarningView() + } + + Ghostty.TerminalSplit(onClose: Self.closeWindow, baseConfig: nil) + .ghosttyApp(ghostty.app!) + .ghosttyConfig(ghostty.config!) + .focused($focused) + .onAppear { self.focused = true } + } + } + } + + static func closeWindow() { + guard let currentWindow = NSApp.keyWindow else { return } + currentWindow.close() + } +}