88 lines
3.0 KiB
Swift
88 lines
3.0 KiB
Swift
import AppKit
|
|
import SwiftUI
|
|
import Combine
|
|
|
|
/// Manages standalone pop-out terminal windows for detached tabs.
|
|
/// Each detached tab gets its own resizable window with the terminal view.
|
|
@MainActor
|
|
class PopoutWindowController: NSObject, NSWindowDelegate {
|
|
|
|
static let shared = PopoutWindowController()
|
|
|
|
/// Tracks open pop-out windows so they aren't released prematurely.
|
|
private var windows: [UUID: NSWindow] = [:]
|
|
private var sessions: [UUID: TerminalSession] = [:]
|
|
private var titleObservers: [UUID: AnyCancellable] = [:]
|
|
|
|
private override init() {
|
|
super.init()
|
|
}
|
|
|
|
/// Creates a new standalone window for the given terminal session.
|
|
func popout(session: TerminalSession) {
|
|
let windowID = session.id
|
|
|
|
if let existingWindow = windows[windowID] {
|
|
existingWindow.makeKeyAndOrderFront(nil)
|
|
NSApp.activate(ignoringOtherApps: true)
|
|
return
|
|
}
|
|
|
|
let win = NSWindow(
|
|
contentRect: NSRect(x: 0, y: 0, width: 720, height: 480),
|
|
styleMask: [.titled, .closable, .resizable, .miniaturizable],
|
|
backing: .buffered,
|
|
defer: false
|
|
)
|
|
win.title = session.title
|
|
win.appearance = NSAppearance(named: .darkAqua)
|
|
win.backgroundColor = .black
|
|
win.delegate = self
|
|
win.isReleasedWhenClosed = false
|
|
|
|
let hostingView = NSHostingView(
|
|
rootView: SwiftTermView(session: session)
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
.background(Color.black)
|
|
.preferredColorScheme(.dark)
|
|
)
|
|
hostingView.frame = NSRect(origin: .zero, size: win.contentRect(forFrameRect: win.frame).size)
|
|
win.contentView = hostingView
|
|
|
|
win.center()
|
|
win.makeKeyAndOrderFront(nil)
|
|
NSApp.activate(ignoringOtherApps: true)
|
|
|
|
windows[windowID] = win
|
|
sessions[windowID] = session
|
|
|
|
// Update window title when the terminal title changes
|
|
titleObservers[windowID] = session.$title
|
|
.receive(on: RunLoop.main)
|
|
.sink { [weak win] title in win?.title = title }
|
|
}
|
|
|
|
// MARK: - NSWindowDelegate
|
|
|
|
func windowDidBecomeKey(_ notification: Notification) {
|
|
guard let window = notification.object as? NSWindow,
|
|
let entry = windows.first(where: { $0.value === window }),
|
|
let terminalView = sessions[entry.key]?.terminalView,
|
|
terminalView.window === window else { return }
|
|
|
|
window.makeFirstResponder(terminalView)
|
|
}
|
|
|
|
func windowWillClose(_ notification: Notification) {
|
|
guard let closingWindow = notification.object as? NSWindow else { return }
|
|
|
|
// Find which session this window belongs to and clean up
|
|
if let entry = windows.first(where: { $0.value === closingWindow }) {
|
|
sessions[entry.key]?.terminate()
|
|
sessions.removeValue(forKey: entry.key)
|
|
windows.removeValue(forKey: entry.key)
|
|
titleObservers.removeValue(forKey: entry.key)
|
|
}
|
|
}
|
|
}
|