mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-05-14 12:18:36 +03:00
165 lines
5.3 KiB
Swift
165 lines
5.3 KiB
Swift
import Foundation
|
|
import Cocoa
|
|
import SwiftUI
|
|
import GhosttyKit
|
|
|
|
/// Controller for the slide-style terminal.
|
|
class SlideTerminalController: NSWindowController, NSWindowDelegate, TerminalViewDelegate, TerminalViewModel {
|
|
override var windowNibName: NSNib.Name? { "SlideTerminal" }
|
|
|
|
/// The app instance that this terminal view will represent.
|
|
let ghostty: Ghostty.App
|
|
|
|
/// The position for the slide terminal.
|
|
let position: SlideTerminalPosition
|
|
|
|
/// The surface tree for this window.
|
|
@Published var surfaceTree: Ghostty.SplitNode? = nil
|
|
|
|
init(_ ghostty: Ghostty.App,
|
|
position: SlideTerminalPosition = .top,
|
|
baseConfig base: Ghostty.SurfaceConfiguration? = nil,
|
|
surfaceTree tree: Ghostty.SplitNode? = nil
|
|
) {
|
|
self.ghostty = ghostty
|
|
self.position = position
|
|
|
|
super.init(window: nil)
|
|
|
|
// Initialize our initial surface.
|
|
guard let ghostty_app = ghostty.app else { preconditionFailure("app must be loaded") }
|
|
self.surfaceTree = tree ?? .leaf(.init(ghostty_app, baseConfig: base))
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) is not supported for this view")
|
|
}
|
|
|
|
// MARK: NSWindowController
|
|
|
|
override func windowDidLoad() {
|
|
guard let window = self.window else { return }
|
|
|
|
// The controller is the window delegate so we can detect events such as
|
|
// window close so we can animate out.
|
|
window.delegate = self
|
|
|
|
// The slide window is not restorable (yet!). "Yet" because in theory we can
|
|
// make this restorable, but it isn't currently implemented.
|
|
window.isRestorable = false
|
|
|
|
// Setup our content
|
|
window.contentView = NSHostingView(rootView: TerminalView(
|
|
ghostty: self.ghostty,
|
|
viewModel: self,
|
|
delegate: self
|
|
))
|
|
|
|
// Animate the window in
|
|
slideIn()
|
|
}
|
|
|
|
// MARK: NSWindowDelegate
|
|
|
|
func windowDidResignKey(_ notification: Notification) {
|
|
slideOut()
|
|
}
|
|
|
|
func windowWillResize(_ sender: NSWindow, to frameSize: NSSize) -> NSSize {
|
|
guard let screen = NSScreen.main else { return frameSize }
|
|
return position.restrictFrameSize(frameSize, on: screen)
|
|
}
|
|
|
|
//MARK: TerminalViewDelegate
|
|
|
|
func cellSizeDidChange(to: NSSize) {
|
|
guard ghostty.config.windowStepResize else { return }
|
|
self.window?.contentResizeIncrements = to
|
|
}
|
|
|
|
func surfaceTreeDidChange() {
|
|
if (surfaceTree == nil) {
|
|
self.window?.close()
|
|
}
|
|
}
|
|
|
|
// MARK: Slide Methods
|
|
|
|
func slideToggle() {
|
|
guard let window = self.window else { return }
|
|
if (window.alphaValue > 0) {
|
|
slideOut()
|
|
} else {
|
|
slideIn()
|
|
}
|
|
}
|
|
|
|
func slideIn() {
|
|
guard let window = self.window else { return }
|
|
slideWindowIn(window: window, from: position)
|
|
}
|
|
|
|
func slideOut() {
|
|
guard let window = self.window else { return }
|
|
slideWindowOut(window: window, to: position)
|
|
}
|
|
|
|
private func slideWindowIn(window: NSWindow, from position: SlideTerminalPosition) {
|
|
guard let screen = NSScreen.main else { return }
|
|
|
|
// Move our window off screen to the top
|
|
position.setInitial(in: window, on: screen)
|
|
|
|
// Move it to the visible position since animation requires this
|
|
window.makeKeyAndOrderFront(nil)
|
|
|
|
// Run the animation that moves our window into the proper place and makes
|
|
// it visible.
|
|
NSAnimationContext.runAnimationGroup { context in
|
|
context.duration = 0.3
|
|
context.timingFunction = .init(name: .easeIn)
|
|
position.setFinal(in: window.animator(), on: screen)
|
|
}
|
|
}
|
|
|
|
private func slideWindowOut(window: NSWindow, to position: SlideTerminalPosition) {
|
|
guard let screen = NSScreen.main else { return }
|
|
|
|
// Keep track of if we were the key window. If we were the key window then we
|
|
// want to move focus to the next window so that focus is preserved somewhere
|
|
// in the app.
|
|
let wasKey = window.isKeyWindow
|
|
|
|
NSAnimationContext.runAnimationGroup({ context in
|
|
context.duration = 0.3
|
|
context.timingFunction = .init(name: .easeIn)
|
|
position.setInitial(in: window.animator(), on: screen)
|
|
}, completionHandler: {
|
|
guard wasKey else { return }
|
|
self.focusNextWindow()
|
|
})
|
|
}
|
|
|
|
private func focusNextWindow() {
|
|
// We only want to consider windows that are visible
|
|
let windows = NSApp.windows.filter { $0.isVisible }
|
|
|
|
// If we have no windows there is nothing to focus.
|
|
guard !windows.isEmpty else { return }
|
|
|
|
// Find the current key window (the window that is currently focused)
|
|
if let keyWindow = NSApp.keyWindow,
|
|
let currentIndex = windows.firstIndex(of: keyWindow) {
|
|
// Calculate the index of the next window (cycle through the list)
|
|
let nextIndex = (currentIndex + 1) % windows.count
|
|
let nextWindow = windows[nextIndex]
|
|
|
|
// Make the next window key and bring it to the front
|
|
nextWindow.makeKeyAndOrderFront(nil)
|
|
} else {
|
|
// If there's no key window, focus the first available window
|
|
windows.first?.makeKeyAndOrderFront(nil)
|
|
}
|
|
}
|
|
}
|