mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
macos: working on the new terminalmanager
This commit is contained in:
@ -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 */,
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
62
macos/Sources/Features/Terminal/TerminalManager.swift
Normal file
62
macos/Sources/Features/Terminal/TerminalManager.swift
Normal 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
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user