Files
downterm/Downterm/CommandNotch/Managers/PopoutWindowController.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)
}
}
}