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) } } }