import AppKit import Combine /// A snapshot of one running application shown in the top bar. struct RunningApp: Identifiable, Equatable { let id: pid_t let name: String let bundleID: String? let icon: NSImage let isHidden: Bool let isActive: Bool static func == (lhs: RunningApp, rhs: RunningApp) -> Bool { lhs.id == rhs.id && lhs.name == rhs.name && lhs.isHidden == rhs.isHidden && lhs.isActive == rhs.isActive } } /// Tracks running applications and splits them into the two top bar sections: /// regular apps (with a Dock icon) and background apps (accessory/agent apps). @MainActor final class AppListModel: ObservableObject { static let shared = AppListModel() /// Number of background apps shown before the rest are hidden behind the /// "Show more" affordance. nonisolated static let backgroundPageSize = 5 @Published private(set) var regularApps: [RunningApp] = [] @Published private(set) var backgroundApps: [RunningApp] = [] /// Whether the background-app section is expanded. Collapsed means the /// section renders as a single dropdown header. @Published var backgroundExpanded = false /// How many background apps are currently revealed after expanding. @Published private(set) var backgroundVisibleCount = AppListModel.backgroundPageSize private var observers: [NSObjectProtocol] = [] private init() {} func start() { refresh() let center = NSWorkspace.shared.notificationCenter let names: [Notification.Name] = [ NSWorkspace.didLaunchApplicationNotification, NSWorkspace.didTerminateApplicationNotification, NSWorkspace.didActivateApplicationNotification, NSWorkspace.didHideApplicationNotification, NSWorkspace.didUnhideApplicationNotification, ] for name in names { observers.append(center.addObserver(forName: name, object: nil, queue: .main) { [weak self] _ in Task { @MainActor in self?.refresh() } }) } } func refresh() { let ownBundleID = Bundle.main.bundleIdentifier var regular: [RunningApp] = [] var background: [RunningApp] = [] for app in NSWorkspace.shared.runningApplications { // Never list ourselves. if let ownBundleID, app.bundleIdentifier == ownBundleID { continue } let info = RunningApp( id: app.processIdentifier, name: app.localizedName ?? app.bundleIdentifier ?? "Unknown", bundleID: app.bundleIdentifier, icon: app.icon ?? NSImage(named: NSImage.applicationIconName) ?? NSImage(), isHidden: app.isHidden, isActive: app.isActive ) if app.activationPolicy == .regular { regular.append(info) } else { background.append(info) } } regularApps = Self.sorted(regular) backgroundApps = Self.sorted(background) // Keep the revealed count valid as apps come and go. backgroundVisibleCount = Self.clampedVisibleCount( current: backgroundVisibleCount, total: backgroundApps.count ) } nonisolated static func sorted(_ apps: [RunningApp]) -> [RunningApp] { apps.sorted { a, b in let result = a.name.localizedCaseInsensitiveCompare(b.name) if result == .orderedSame { return a.id < b.id } return result == .orderedAscending } } /// Background apps that the expanded section should render right now. var visibleBackgroundApps: [RunningApp] { guard backgroundExpanded else { return [] } return Array(backgroundApps.prefix(backgroundVisibleCount)) } /// How many background apps are currently hidden behind "Show more". var hiddenBackgroundCount: Int { max(0, backgroundApps.count - backgroundVisibleCount) } var canShowMoreBackgroundApps: Bool { hiddenBackgroundCount > 0 } func toggleBackgroundExpanded() { backgroundExpanded.toggle() if !backgroundExpanded { backgroundVisibleCount = Self.backgroundPageSize } } func showMoreBackgroundApps() { backgroundVisibleCount = Self.nextVisibleCount( current: backgroundVisibleCount, total: backgroundApps.count ) } // MARK: - Pure helpers (exercised by --self-test) nonisolated static func clampedVisibleCount(current: Int, total: Int) -> Int { max(0, min(current, total)) } nonisolated static func nextVisibleCount(current: Int, total: Int) -> Int { min(total, current + backgroundPageSize) } @discardableResult func activate(_ app: RunningApp) -> Bool { guard let running = NSRunningApplication(processIdentifier: app.id) else { return false } return running.activate(options: [.activateIgnoringOtherApps]) } @discardableResult func toggleHidden(_ app: RunningApp) -> Bool { guard let running = NSRunningApplication(processIdentifier: app.id) else { return false } if running.isHidden { return running.unhide() } return running.hide() } @discardableResult func quit(_ app: RunningApp) -> Bool { guard let running = NSRunningApplication(processIdentifier: app.id) else { return false } return running.terminate() } }