macos: working on the new terminalmanager

This commit is contained in:
Mitchell Hashimoto
2023-10-29 22:06:32 -07:00
parent 3018377389
commit 704c303cd1
6 changed files with 130 additions and 38 deletions

View File

@ -30,6 +30,7 @@
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 */; };
A59630A02AEF6AEB00D64628 /* TerminalManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A596309F2AEF6AEB00D64628 /* TerminalManager.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 */; };
@ -68,6 +69,7 @@
A59630992AEE1C6400D64628 /* Terminal.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = Terminal.xib; sourceTree = "<group>"; };
A596309B2AEE1C9E00D64628 /* TerminalController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalController.swift; sourceTree = "<group>"; };
A596309D2AEE1D6C00D64628 /* TerminalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalView.swift; sourceTree = "<group>"; };
A596309F2AEF6AEB00D64628 /* TerminalManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalManager.swift; sourceTree = "<group>"; };
A59FB5CE2AE0DB50009128F3 /* InspectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InspectorView.swift; sourceTree = "<group>"; };
A59FB5D02AE0DEA7009128F3 /* MetalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetalView.swift; sourceTree = "<group>"; };
A5A1F8842A489D6800D1E8BC /* terminfo */ = {isa = PBXFileReference; lastKnownFileType = folder; name = terminfo; path = "../zig-out/share/terminfo"; sourceTree = "<group>"; };
@ -184,6 +186,7 @@
isa = PBXGroup;
children = (
A59630992AEE1C6400D64628 /* Terminal.xib */,
A596309F2AEF6AEB00D64628 /* TerminalManager.swift */,
A596309B2AEE1C9E00D64628 /* TerminalController.swift */,
A596309D2AEE1D6C00D64628 /* TerminalView.swift */,
);
@ -319,6 +322,7 @@
A56D58892ACDE6CA00508D2C /* ServiceProvider.swift in Sources */,
A5278A9B2AA05B2600CD3039 /* Ghostty.Input.swift in Sources */,
A59630972AEE163600D64628 /* HostingWindow.swift in Sources */,
A59630A02AEF6AEB00D64628 /* TerminalManager.swift in Sources */,
A53426352A7DA53D00EBB7A2 /* AppDelegate.swift in Sources */,
A5CDF1952AAFA19600513312 /* ConfigurationErrorsView.swift in Sources */,
A55B7BBC29B6FC330055DE60 /* SurfaceView.swift in Sources */,

View File

@ -42,16 +42,16 @@ class AppDelegate: NSObject, ObservableObject, NSApplicationDelegate, GhosttyApp
private var dockMenu: NSMenu = NSMenu()
/// The ghostty global state. Only one per process.
private var ghostty: Ghostty.AppState = Ghostty.AppState()
private let ghostty: Ghostty.AppState = Ghostty.AppState()
/// Manages windows and tabs, ensuring they're allocated/deallocated correctly
var windowManager: PrimaryWindowManager!
/// Manages our terminal windows.
let terminalManager: TerminalManager
override init() {
self.terminalManager = TerminalManager(ghostty)
super.init()
ghostty.delegate = self
windowManager = PrimaryWindowManager(ghostty: self.ghostty)
}
//MARK: - NSApplicationDelegate
@ -73,15 +73,11 @@ 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.
// TODO: remove when TerminalController is done
// windowManager.addInitialWindow()
terminalManager.newWindow()
// 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()
@ -151,7 +147,7 @@ class AppDelegate: NSObject, ObservableObject, NSApplicationDelegate, GhosttyApp
guard !flag else { return true }
// No visible windows, open a new one.
windowManager.newWindow()
terminalManager.newWindow()
return false
}
@ -174,16 +170,9 @@ class AppDelegate: NSObject, ObservableObject, NSApplicationDelegate, GhosttyApp
// Build our config
var config = Ghostty.SurfaceConfiguration()
config.workingDirectory = filename
// If we don't have a window open through the window manager, we launch
// a new window.
guard let mainWindow = windowManager.mainWindow else {
windowManager.addNewWindow(withBaseConfig: config)
return true
}
// Add a new tab
windowManager.addNewTab(to: mainWindow, withBaseConfig: config)
// Add a new tab or create a new window
terminalManager.newTab(withBaseConfig: config)
return true
}
@ -258,7 +247,7 @@ class AppDelegate: NSObject, ObservableObject, NSApplicationDelegate, GhosttyApp
func configDidReload(_ state: Ghostty.AppState) {
// Config could change keybindings, so update everything that depends on that
syncMenuShortcuts()
windowManager.relabelTabs()
//windowManager.relabelTabs()
// Config could change window appearance
syncAppearance()
@ -308,7 +297,7 @@ class AppDelegate: NSObject, ObservableObject, NSApplicationDelegate, GhosttyApp
}
@IBAction func newWindow(_ sender: Any?) {
windowManager.newWindow()
terminalManager.newWindow()
// We also activate our app so that it becomes front. This may be
// necessary for the dock menu.
@ -316,7 +305,7 @@ class AppDelegate: NSObject, ObservableObject, NSApplicationDelegate, GhosttyApp
}
@IBAction func newTab(_ sender: Any?) {
windowManager.newTab()
terminalManager.newTab()
// We also activate our app so that it becomes front. This may be
// necessary for the dock menu.

View File

@ -39,7 +39,7 @@ class ServiceProvider: NSObject {
private func openTerminal(_ path: String, target: OpenTarget) {
guard let delegateRaw = NSApp.delegate else { return }
guard let delegate = delegateRaw as? AppDelegate else { return }
guard let windowManager = delegate.windowManager else { return }
let terminalManager = delegate.terminalManager
// We only open in directories.
var isDirectory = ObjCBool(true)
@ -49,20 +49,13 @@ class ServiceProvider: NSObject {
// Build our config
var config = Ghostty.SurfaceConfiguration()
config.workingDirectory = path
// If we don't have a window open through the window manager, we launch
// a new window even if they requested a tab.
guard let mainWindow = windowManager.mainWindow else {
windowManager.addNewWindow(withBaseConfig: config)
return
}
switch (target) {
case .window:
windowManager.addNewWindow(withBaseConfig: config)
terminalManager.newWindow(withBaseConfig: config)
case .tab:
windowManager.addNewTab(to: mainWindow, withBaseConfig: config)
terminalManager.newTab(withBaseConfig: config)
}
}
}

View File

@ -3,7 +3,7 @@ import Cocoa
import SwiftUI
import Combine
class TerminalController: NSWindowController, NSWindowDelegate {
class TerminalController: NSWindowController, NSWindowDelegate, TerminalViewDelegate {
override var windowNibName: NSNib.Name? { "Terminal" }
/// The app instance that this terminal view will represent.
@ -12,6 +12,11 @@ class TerminalController: NSWindowController, NSWindowDelegate {
init(_ ghostty: Ghostty.AppState) {
self.ghostty = ghostty
super.init(window: nil)
// Register as observer for window-level manipulations that are best handled
// here at the controller layer rather than in the SwiftUI stack.
let center = NotificationCenter.default
}
required init?(coder: NSCoder) {
@ -39,7 +44,8 @@ class TerminalController: NSWindowController, NSWindowDelegate {
// Initialize our content view to the SwiftUI root
window.contentView = NSHostingView(rootView: TerminalView(
ghostty: self.ghostty
ghostty: self.ghostty,
delegate: self
))
}
@ -47,4 +53,15 @@ class TerminalController: NSWindowController, NSWindowDelegate {
func windowWillClose(_ notification: Notification) {
}
//MARK: - TerminalViewDelegate
func titleDidChange(to: String) {
self.window?.title = to
}
func cellSizeDidChange(to: NSSize) {
guard ghostty.windowStepResize else { return }
self.window?.contentResizeIncrements = to
}
}

View File

@ -0,0 +1,62 @@
import Cocoa
/// Manages a set of terminal windows.
class TerminalManager {
struct Window {
let controller: TerminalController
}
let ghostty: Ghostty.AppState
/// The set of windows we currently have.
private var windows: [Window] = []
/// Returns the main window of the managed window stack. If there is no window
/// then an arbitrary window will be chosen.
private var mainWindow: Window? {
for window in windows {
if (window.controller.window?.isMainWindow ?? false) {
return window
}
}
// If we have no main window, just use the first window.
return windows.first
}
init(_ ghostty: Ghostty.AppState) {
self.ghostty = ghostty
}
/// Create a new terminal window.
func newWindow(withBaseConfig base: Ghostty.SurfaceConfiguration? = nil) {
let c = createWindow(withBaseConfig: base)
c.showWindow(self)
}
/// Creates a new tab in the current main window. If there are no windows, a window
/// is created.
func newTab(withBaseConfig base: Ghostty.SurfaceConfiguration? = nil) {
// If there is no main window, just create a new window
guard let parent = mainWindow?.controller.window else {
newWindow(withBaseConfig: base)
return
}
// Create a new window and add it to the parent
let window = createWindow(withBaseConfig: base).window!
parent.addTabbedWindow(window, ordered: .above)
window.makeKeyAndOrderFront(self)
}
/// Creates a window controller, adds it to our managed list, and returns it.
func createWindow(withBaseConfig: Ghostty.SurfaceConfiguration?) -> TerminalController {
// Initialize our controller to load the window
let c = TerminalController(ghostty)
// Keep track of every window we manage
windows.append(Window(controller: c))
return c
}
}

View File

@ -1,12 +1,33 @@
import SwiftUI
import GhosttyKit
protocol TerminalViewDelegate: AnyObject {
/// Called when the currently focused surface changed. This can be nil.
func focusedSurfaceDidChange(to: Ghostty.SurfaceView?)
/// The title of the terminal should change.
func titleDidChange(to: String)
/// The cell size changed.
func cellSizeDidChange(to: NSSize)
}
extension TerminalViewDelegate {
func focusedSurfaceDidChange(to: Ghostty.SurfaceView?) {}
func titleDidChange(to: String) {}
func cellSizeDidChange(to: NSSize) {}
}
struct TerminalView: View {
@ObservedObject var ghostty: Ghostty.AppState
// An optional delegate to receive information about terminal changes.
weak var delegate: TerminalViewDelegate? = nil
// This seems like a crutch after switching from SwiftUI to AppKit lifecycle.
@FocusState private var focused: Bool
// Various state values sent back up from the currently focused terminals.
@FocusedValue(\.ghosttySurfaceView) private var focusedSurface
@FocusedValue(\.ghosttySurfaceTitle) private var surfaceTitle
@FocusedValue(\.ghosttySurfaceZoomed) private var zoomedSplit
@ -38,10 +59,6 @@ struct TerminalView: View {
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.
@ -54,6 +71,16 @@ struct TerminalView: View {
.ghosttyConfig(ghostty.config!)
.focused($focused)
.onAppear { self.focused = true }
.onChange(of: focusedSurface) { newValue in
self.delegate?.focusedSurfaceDidChange(to: newValue)
}
.onChange(of: title) { newValue in
self.delegate?.titleDidChange(to: newValue)
}
.onChange(of: cellSize) { newValue in
guard let size = newValue else { return }
self.delegate?.cellSizeDidChange(to: size)
}
}
}
}