macos: render a terminal in the slide window

This commit is contained in:
Mitchell Hashimoto
2024-09-22 20:52:04 -07:00
parent 93b2fe60f8
commit bdd0070ffd
6 changed files with 108 additions and 37 deletions

View File

@ -65,6 +65,7 @@
A5CBD05C2CA0C5C70017A1AE /* SlideTerminal.xib in Resources */ = {isa = PBXBuildFile; fileRef = A5CBD05B2CA0C5C70017A1AE /* SlideTerminal.xib */; }; A5CBD05C2CA0C5C70017A1AE /* SlideTerminal.xib in Resources */ = {isa = PBXBuildFile; fileRef = A5CBD05B2CA0C5C70017A1AE /* SlideTerminal.xib */; };
A5CBD05E2CA0C5EC0017A1AE /* SlideTerminalController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CBD05D2CA0C5E70017A1AE /* SlideTerminalController.swift */; }; A5CBD05E2CA0C5EC0017A1AE /* SlideTerminalController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CBD05D2CA0C5E70017A1AE /* SlideTerminalController.swift */; };
A5CBD0602CA0C90A0017A1AE /* SlideTerminalWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CBD05F2CA0C9080017A1AE /* SlideTerminalWindow.swift */; }; A5CBD0602CA0C90A0017A1AE /* SlideTerminalWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CBD05F2CA0C9080017A1AE /* SlideTerminalWindow.swift */; };
A5CBD0642CA122E70017A1AE /* SlideTerminalPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CBD0632CA122E70017A1AE /* SlideTerminalPosition.swift */; };
A5CC36132C9CD72D004D6760 /* SecureInputOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CC36122C9CD729004D6760 /* SecureInputOverlay.swift */; }; A5CC36132C9CD72D004D6760 /* SecureInputOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CC36122C9CD729004D6760 /* SecureInputOverlay.swift */; };
A5CC36152C9CDA06004D6760 /* View+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CC36142C9CDA03004D6760 /* View+Extension.swift */; }; A5CC36152C9CDA06004D6760 /* View+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CC36142C9CDA03004D6760 /* View+Extension.swift */; };
A5CDF1912AAF9A5800513312 /* ConfigurationErrors.xib in Resources */ = {isa = PBXBuildFile; fileRef = A5CDF1902AAF9A5800513312 /* ConfigurationErrors.xib */; }; A5CDF1912AAF9A5800513312 /* ConfigurationErrors.xib in Resources */ = {isa = PBXBuildFile; fileRef = A5CDF1902AAF9A5800513312 /* ConfigurationErrors.xib */; };
@ -139,6 +140,7 @@
A5CBD05B2CA0C5C70017A1AE /* SlideTerminal.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SlideTerminal.xib; sourceTree = "<group>"; }; A5CBD05B2CA0C5C70017A1AE /* SlideTerminal.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SlideTerminal.xib; sourceTree = "<group>"; };
A5CBD05D2CA0C5E70017A1AE /* SlideTerminalController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlideTerminalController.swift; sourceTree = "<group>"; }; A5CBD05D2CA0C5E70017A1AE /* SlideTerminalController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlideTerminalController.swift; sourceTree = "<group>"; };
A5CBD05F2CA0C9080017A1AE /* SlideTerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlideTerminalWindow.swift; sourceTree = "<group>"; }; A5CBD05F2CA0C9080017A1AE /* SlideTerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlideTerminalWindow.swift; sourceTree = "<group>"; };
A5CBD0632CA122E70017A1AE /* SlideTerminalPosition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlideTerminalPosition.swift; sourceTree = "<group>"; };
A5CC36122C9CD729004D6760 /* SecureInputOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureInputOverlay.swift; sourceTree = "<group>"; }; A5CC36122C9CD729004D6760 /* SecureInputOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureInputOverlay.swift; sourceTree = "<group>"; };
A5CC36142C9CDA03004D6760 /* View+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Extension.swift"; sourceTree = "<group>"; }; A5CC36142C9CDA03004D6760 /* View+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Extension.swift"; sourceTree = "<group>"; };
A5CDF1902AAF9A5800513312 /* ConfigurationErrors.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ConfigurationErrors.xib; sourceTree = "<group>"; }; A5CDF1902AAF9A5800513312 /* ConfigurationErrors.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ConfigurationErrors.xib; sourceTree = "<group>"; };
@ -393,6 +395,7 @@
children = ( children = (
A5CBD05B2CA0C5C70017A1AE /* SlideTerminal.xib */, A5CBD05B2CA0C5C70017A1AE /* SlideTerminal.xib */,
A5CBD05D2CA0C5E70017A1AE /* SlideTerminalController.swift */, A5CBD05D2CA0C5E70017A1AE /* SlideTerminalController.swift */,
A5CBD0632CA122E70017A1AE /* SlideTerminalPosition.swift */,
A5CBD05F2CA0C9080017A1AE /* SlideTerminalWindow.swift */, A5CBD05F2CA0C9080017A1AE /* SlideTerminalWindow.swift */,
); );
path = SlideTerminal; path = SlideTerminal;
@ -548,6 +551,7 @@
A5D0AF3D2B37804400D21823 /* CodableBridge.swift in Sources */, A5D0AF3D2B37804400D21823 /* CodableBridge.swift in Sources */,
A5D0AF3B2B36A1DE00D21823 /* TerminalRestorable.swift in Sources */, A5D0AF3B2B36A1DE00D21823 /* TerminalRestorable.swift in Sources */,
C1F26EA72B738B9900404083 /* NSView+Extension.swift in Sources */, C1F26EA72B738B9900404083 /* NSView+Extension.swift in Sources */,
A5CBD0642CA122E70017A1AE /* SlideTerminalPosition.swift in Sources */,
A596309C2AEE1C9E00D64628 /* TerminalController.swift in Sources */, A596309C2AEE1C9E00D64628 /* TerminalController.swift in Sources */,
A5CC36152C9CDA06004D6760 /* View+Extension.swift in Sources */, A5CC36152C9CDA06004D6760 /* View+Extension.swift in Sources */,
A56D58892ACDE6CA00508D2C /* ServiceProvider.swift in Sources */, A56D58892ACDE6CA00508D2C /* ServiceProvider.swift in Sources */,

View File

@ -170,7 +170,7 @@ class AppDelegate: NSObject,
//terminalManager.newWindow() //terminalManager.newWindow()
} }
foo = SlideTerminalController(window: nil) foo = SlideTerminalController(ghostty, baseConfig: nil)
foo?.showWindow(self) foo?.showWindow(self)
} }

View File

@ -4,39 +4,77 @@ import SwiftUI
import GhosttyKit import GhosttyKit
/// Controller for the slide-style terminal. /// Controller for the slide-style terminal.
class SlideTerminalController: NSWindowController { class SlideTerminalController: NSWindowController, TerminalViewDelegate, TerminalViewModel {
override var windowNibName: NSNib.Name? { "SlideTerminal" } override var windowNibName: NSNib.Name? { "SlideTerminal" }
/// The app instance that this terminal view will represent.
let ghostty: Ghostty.App
/// The position fo 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() { override func windowDidLoad() {
guard let window = self.window else { return } guard let window = self.window else { return }
// Make the window full width // The slide window is not restorable (yet!). "Yet" because in theory we can
let screenFrame = NSScreen.main?.frame ?? .zero // make this restorable, but it isn't currently implemented.
window.setFrame(NSRect( window.isRestorable = false
x: 0,
y: 0,
width: screenFrame.size.width,
height: window.frame.size.height
), display: false)
slideWindowIn(window: window) // Setup our content
window.contentView = NSHostingView(rootView: TerminalView(
ghostty: self.ghostty,
viewModel: self,
delegate: self
))
// Animate the window in
slideWindowIn(window: window, from: position)
} }
private func slideWindowIn(window: NSWindow) { //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 Logic
private func slideWindowIn(window: NSWindow, from position: SlideTerminalPosition) {
guard let screen = NSScreen.main else { return } guard let screen = NSScreen.main else { return }
// Determine our final position. Our final position is exactly
// pinned against the top menu bar.
let windowFrame = window.frame
let finalY = screen.visibleFrame.maxY - windowFrame.height
// Move our window off screen to the top // Move our window off screen to the top
window.setFrameOrigin(.init( position.setInitial(in: window, on: screen)
x: windowFrame.origin.x,
y: screen.frame.maxY))
// Set the window invisible
window.alphaValue = 0
// Move it to the visible position since animation requires this // Move it to the visible position since animation requires this
window.makeKeyAndOrderFront(nil) window.makeKeyAndOrderFront(nil)
@ -46,17 +84,7 @@ class SlideTerminalController: NSWindowController {
NSAnimationContext.runAnimationGroup { context in NSAnimationContext.runAnimationGroup { context in
context.duration = 0.3 context.duration = 0.3
context.timingFunction = .init(name: .easeIn) context.timingFunction = .init(name: .easeIn)
position.setFinal(in: window.animator(), on: screen)
let animator = window.animator()
animator.setFrame(.init(
origin: .init(x: windowFrame.origin.x, y: finalY),
size: windowFrame.size
), display: true)
animator.alphaValue = 1
} }
} }
} }
enum SlideTerminalLocation {
case top
}

View File

@ -0,0 +1,41 @@
import Cocoa
enum SlideTerminalPosition {
case top
/// Set the initial state for a window for animating out of this position.
func setInitial(in window: NSWindow, on screen: NSScreen) {
// We always start invisible
window.alphaValue = 0
// Position depends
switch (self) {
case .top:
window.setFrame(.init(
origin: .init(
x: 0,
y: screen.frame.maxY),
size: .init(
width: screen.frame.width,
height: window.frame.height)
), display: false)
}
}
/// Set the final state for a window in this position.
func setFinal(in window: NSWindow, on screen: NSScreen) {
// We always end visible
window.alphaValue = 1
// Position depends
switch (self) {
case .top:
window.setFrame(.init(
origin: .init(
x: window.frame.origin.x,
y: screen.visibleFrame.maxY - window.frame.height),
size: window.frame.size
), display: true)
}
}
}

View File

@ -23,9 +23,6 @@ class SlideTerminalWindow: NSWindow {
// and lets us render off screen. // and lets us render off screen.
self.level = .popUpMenu self.level = .popUpMenu
self.isMovableByWindowBackground = true
self.isMovable = true
self.collectionBehavior = [ self.collectionBehavior = [
// We want this to be part of every space because it is a singleton. // We want this to be part of every space because it is a singleton.
.canJoinAllSpaces, .canJoinAllSpaces,

View File

@ -18,6 +18,7 @@ protocol TerminalViewDelegate: AnyObject {
/// not called initially. /// not called initially.
func surfaceTreeDidChange() func surfaceTreeDidChange()
/// This is called when a split is zoomed.
func zoomStateDidChange(to: Bool) func zoomStateDidChange(to: Bool)
} }