152 lines
5.2 KiB
Swift
152 lines
5.2 KiB
Swift
import AppKit
|
|
|
|
/// A terminal title is authored by the remote host. Keep it on one printable
|
|
/// line before allowing it into AppKit's global menu structure.
|
|
nonisolated enum RemoteWindowTitle {
|
|
static func sanitized(_ proposed: String) -> String? {
|
|
enum EscapeState { case text, escape, controlSequence }
|
|
|
|
var state = EscapeState.text
|
|
var result = ""
|
|
var insertsSeparator = false
|
|
|
|
for scalar in proposed.unicodeScalars {
|
|
switch state {
|
|
case .escape:
|
|
state = scalar.value == 0x5B ? .controlSequence : .text
|
|
case .controlSequence:
|
|
// ECMA-48 CSI sequences end at the first final byte.
|
|
if (0x40...0x7E).contains(scalar.value) { state = .text }
|
|
case .text:
|
|
if scalar.value == 0x1B {
|
|
state = .escape
|
|
} else if scalar.value == 0x9B {
|
|
state = .controlSequence
|
|
} else if CharacterSet.controlCharacters.contains(scalar)
|
|
|| CharacterSet.newlines.contains(scalar)
|
|
{
|
|
insertsSeparator = !result.isEmpty
|
|
} else {
|
|
if insertsSeparator, !CharacterSet.whitespaces.contains(scalar) {
|
|
result.append(" ")
|
|
}
|
|
insertsSeparator = false
|
|
result.unicodeScalars.append(scalar)
|
|
}
|
|
}
|
|
}
|
|
|
|
let cleaned = result.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
return cleaned.isEmpty ? nil : cleaned
|
|
}
|
|
}
|
|
|
|
/// Mirrors Kitty's macOS title item: an empty top-level item whose empty
|
|
/// submenu carries ` :: <active terminal title>`. Appending it to `mainMenu`
|
|
/// places it after the ordinary Help menu without turning the title into an
|
|
/// application command.
|
|
@MainActor
|
|
final class RemoteWindowTitleMenuCoordinator {
|
|
static let shared = RemoteWindowTitleMenuCoordinator()
|
|
|
|
private final class Report {
|
|
weak var window: NSWindow?
|
|
var title: String?
|
|
var revision: UInt64
|
|
|
|
init(window: NSWindow?, title: String?, revision: UInt64) {
|
|
self.window = window
|
|
self.title = title
|
|
self.revision = revision
|
|
}
|
|
}
|
|
|
|
private static let itemIdentifier = NSUserInterfaceItemIdentifier(
|
|
"com.akinokaede.termind.remote-window-title"
|
|
)
|
|
|
|
private let mainMenuProvider: () -> NSMenu?
|
|
private let keyWindowProvider: () -> NSWindow?
|
|
private var reports: [UUID: Report] = [:]
|
|
private var revision: UInt64 = 0
|
|
private var keyWindowObservers: [any NSObjectProtocol] = []
|
|
private var titleMenuItem: NSMenuItem?
|
|
|
|
init(
|
|
mainMenuProvider: @escaping () -> NSMenu? = { NSApp.mainMenu },
|
|
keyWindowProvider: @escaping () -> NSWindow? = { NSApp.keyWindow },
|
|
observesKeyWindowChanges: Bool = true
|
|
) {
|
|
self.mainMenuProvider = mainMenuProvider
|
|
self.keyWindowProvider = keyWindowProvider
|
|
if observesKeyWindowChanges { observeKeyWindowChanges() }
|
|
}
|
|
|
|
func report(title: String?, for window: NSWindow?, token: UUID) {
|
|
revision &+= 1
|
|
reports[token] = Report(window: window, title: title, revision: revision)
|
|
refresh()
|
|
}
|
|
|
|
func withdraw(token: UUID) {
|
|
reports[token] = nil
|
|
refresh()
|
|
}
|
|
|
|
func refresh() {
|
|
reports = reports.filter { $0.value.window != nil }
|
|
guard let keyWindow = keyWindowProvider() else {
|
|
removeTitleMenuItem()
|
|
return
|
|
}
|
|
let title = reports.values
|
|
.filter { $0.window === keyWindow }
|
|
.max { $0.revision < $1.revision }?
|
|
.title
|
|
guard let title = title.flatMap(RemoteWindowTitle.sanitized) else {
|
|
removeTitleMenuItem()
|
|
return
|
|
}
|
|
installOrUpdateTitleMenuItem(title)
|
|
}
|
|
|
|
private func observeKeyWindowChanges() {
|
|
let center = NotificationCenter.default
|
|
for name in [NSWindow.didBecomeKeyNotification, NSWindow.didResignKeyNotification] {
|
|
keyWindowObservers.append(
|
|
center.addObserver(
|
|
forName: name, object: nil, queue: .main
|
|
) { [weak self] _ in
|
|
MainActor.assumeIsolated { self?.refresh() }
|
|
})
|
|
}
|
|
}
|
|
|
|
private func installOrUpdateTitleMenuItem(_ title: String) {
|
|
guard let mainMenu = mainMenuProvider() else { return }
|
|
let item: NSMenuItem
|
|
if let installed = titleMenuItem, installed.menu === mainMenu {
|
|
item = installed
|
|
} else if let existing = mainMenu.items.first(where: {
|
|
$0.identifier == Self.itemIdentifier
|
|
}) {
|
|
titleMenuItem = existing
|
|
item = existing
|
|
} else {
|
|
let created = NSMenuItem(title: "", action: nil, keyEquivalent: "")
|
|
created.identifier = Self.itemIdentifier
|
|
created.submenu = NSMenu()
|
|
mainMenu.addItem(created)
|
|
titleMenuItem = created
|
|
item = created
|
|
}
|
|
item.submenu?.title = " :: \(title)"
|
|
}
|
|
|
|
private func removeTitleMenuItem() {
|
|
guard let item = titleMenuItem else { return }
|
|
item.menu?.removeItem(item)
|
|
titleMenuItem = nil
|
|
}
|
|
}
|