187 lines
7.5 KiB
Swift

import SwiftUI
import GhosttyKit
struct PrimaryView: View {
@ObservedObject var ghostty: Ghostty.AppState
// We need access to our app delegate to know if we're quitting or not.
// Make sure to use `@ObservedObject` so we can keep track of `appDelegate.confirmQuit`.
@ObservedObject var appDelegate: AppDelegate
// We need this to report back up the app controller which surface in this view is focused.
let focusedSurfaceWrapper: FocusedSurfaceWrapper
// If this is set, this is the base configuration that we build our surface out of.
let baseConfig: Ghostty.SurfaceConfiguration?
// We need access to our window to know if we're the key window to determine
// if we show the quit confirmation or not.
@State private var window: NSWindow?
// This handles non-native fullscreen
@State private var fullScreen = FullScreenHandler()
// 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
// 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: self.baseConfig)
.ghosttyApp(ghostty.app!)
.ghosttyConfig(ghostty.config!)
.background(WindowAccessor(window: $window))
.onReceive(gotoTab) { onGotoTab(notification: $0) }
.onReceive(toggleFullscreen) { onToggleFullscreen(notification: $0) }
.focused($focused)
.onAppear { self.focused = true }
.onChange(of: focusedSurface) { newValue in
self.focusedSurfaceWrapper.surface = newValue?.surface
}
.onChange(of: title) { newValue in
// We need to handle this manually because we are using AppKit lifecycle
// so navigationTitle no longer works.
guard let window = self.window else { return }
window.title = newValue
}
}
}
}
static func closeWindow() {
guard let currentWindow = NSApp.keyWindow else { return }
currentWindow.close()
}
private func onGotoTab(notification: SwiftUI.Notification) {
// Notification center indiscriminately sends to every subscriber (makes sense)
// but we only want to process this once. In order to process it once lets only
// handle it if we're the focused window.
guard let window = self.window else { return }
guard window.isKeyWindow else { return }
// Get the tab index from the notification
guard let tabIndexAny = notification.userInfo?[Ghostty.Notification.GotoTabKey] else { return }
guard let tabIndex = tabIndexAny as? Int32 else { return }
guard let windowController = window.windowController else { return }
guard let tabGroup = windowController.window?.tabGroup else { return }
let tabbedWindows = tabGroup.windows
// This will be the index we want to actual go to
let finalIndex: Int
// An index that is invalid is used to signal some special values.
if (tabIndex <= 0) {
guard let selectedWindow = tabGroup.selectedWindow else { return }
guard let selectedIndex = tabbedWindows.firstIndex(where: { $0 == selectedWindow }) else { return }
if (tabIndex == GHOSTTY_TAB_PREVIOUS.rawValue) {
finalIndex = selectedIndex - 1
} else if (tabIndex == GHOSTTY_TAB_NEXT.rawValue) {
finalIndex = selectedIndex + 1
} else {
return
}
} else {
// Tabs are 0-indexed here, so we subtract one from the key the user hit.
finalIndex = Int(tabIndex - 1)
}
guard finalIndex >= 0 && finalIndex < tabbedWindows.count else { return }
let targetWindow = tabbedWindows[finalIndex]
targetWindow.makeKeyAndOrderFront(nil)
}
private func onToggleFullscreen(notification: SwiftUI.Notification) {
// Just like in `onGotoTab`, we might receive this multiple times. But
// it's fine, because `toggleFullscreen` should only apply to the
// currently focused window.
guard let window = self.window else { return }
guard window.isKeyWindow else { return }
// Check whether we use non-native fullscreen
guard let useNonNativeFullscreenAny = notification.userInfo?[Ghostty.Notification.NonNativeFullscreenKey] else { return }
guard let useNonNativeFullscreen = useNonNativeFullscreenAny as? ghostty_non_native_fullscreen_e else { return }
self.fullScreen.toggleFullscreen(window: window, nonNativeFullscreen: useNonNativeFullscreen)
// After toggling fullscreen we need to focus the terminal again.
self.focused = true
// For some reason focus always gets moved to the first split when
// toggling fullscreen, so we set it back to the correct one.
if let focusedSurface {
Ghostty.moveFocus(to: focusedSurface)
}
}
}
struct DebugBuildWarningView: View {
@State private var isPopover = false
var body: some View {
HStack {
Spacer()
Image(systemName: "exclamationmark.triangle.fill")
.foregroundColor(.yellow)
Text("You're running a debug build of Ghostty! Performance will be degraded.")
.padding(.all, 8)
.popover(isPresented: $isPopover, arrowEdge: .bottom) {
Text("""
Debug builds of Ghostty are very slow and you may experience
performance problems. Debug builds are only recommended during
development.
""")
.padding(.all)
}
Spacer()
}
.background(Color(.windowBackgroundColor))
.frame(maxWidth: .infinity)
.onTapGesture {
isPopover = true
}
}
}