import AppKit import ConjureKit @MainActor final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSearchFieldDelegate, NSTableViewDataSource, NSTableViewDelegate { private let searchField = NSSearchField() private let tableView = ApplicationTableView() private let statusLabel = NSTextField(labelWithString: "Finding applications…") private let emptyLabel = NSTextField(labelWithString: "Finding applications…") private let openButton = NSButton(title: "Open Application", target: nil, action: nil) private var applications: [Application] = [] private var results: [Application] = [] private var discoveryTask: Task? private var isLaunching = false init() { let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 640, height: 480), styleMask: [.titled, .closable, .miniaturizable, .resizable], backing: .buffered, defer: false ) window.title = "Conjure" window.contentMinSize = NSSize(width: 420, height: 300) window.isReleasedWhenClosed = false window.center() super.init(window: window) configureContent(in: window) window.delegate = self } required init?(coder: NSCoder) { fatalError("Conjure creates its window programmatically") } func present() { searchField.stringValue = "" updateResults() window?.deminiaturize(nil) showWindow(nil) NSApp.activate() window?.makeKeyAndOrderFront(nil) window?.makeFirstResponder(searchField) refreshApplications() } func windowDidResignKey(_ notification: Notification) { guard !isLaunching, window?.attachedSheet == nil else { return } window?.orderOut(nil) } private func configureContent(in window: NSWindow) { guard let content = window.contentView else { return } searchField.placeholderString = "Search applications" searchField.controlSize = .large searchField.font = .systemFont(ofSize: NSFont.systemFontSize(for: .large)) searchField.sendsSearchStringImmediately = true searchField.sendsWholeSearchString = false searchField.delegate = self searchField.setAccessibilityLabel("Search applications") let column = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("application")) column.resizingMask = .autoresizingMask tableView.addTableColumn(column) tableView.headerView = nil tableView.style = .inset tableView.rowHeight = 44 tableView.columnAutoresizingStyle = .uniformColumnAutoresizingStyle tableView.allowsMultipleSelection = false tableView.allowsEmptySelection = false tableView.dataSource = self tableView.delegate = self tableView.target = self tableView.doubleAction = #selector(openSelection) tableView.openSelection = { [weak self] in self?.openSelection() } tableView.dismiss = { [weak self] in self?.window?.orderOut(nil) } tableView.editSearch = { [weak self] event in guard let self, let window = self.window else { return } window.makeFirstResponder(self.searchField) self.searchField.currentEditor()?.keyDown(with: event) } tableView.setAccessibilityLabel("Applications") let scrollView = NSScrollView() scrollView.documentView = tableView scrollView.hasVerticalScroller = true scrollView.autohidesScrollers = true scrollView.borderType = .noBorder statusLabel.font = .systemFont(ofSize: NSFont.smallSystemFontSize) statusLabel.textColor = .secondaryLabelColor statusLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) emptyLabel.textColor = .secondaryLabelColor emptyLabel.alignment = .center openButton.bezelStyle = .rounded openButton.keyEquivalent = "\r" openButton.target = self openButton.action = #selector(openSelection) openButton.isEnabled = false openButton.toolTip = "Open the selected application (Return). Use ↑ and ↓ to select." let footer = NSStackView(views: [statusLabel, NSView(), openButton]) footer.orientation = .horizontal footer.spacing = 12 for view in [searchField, scrollView, emptyLabel, footer] { view.translatesAutoresizingMaskIntoConstraints = false content.addSubview(view) } NSLayoutConstraint.activate([ searchField.topAnchor.constraint(equalTo: content.topAnchor, constant: 16), searchField.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 16), searchField.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -16), scrollView.topAnchor.constraint(equalTo: searchField.bottomAnchor, constant: 12), scrollView.leadingAnchor.constraint(equalTo: content.leadingAnchor), scrollView.trailingAnchor.constraint(equalTo: content.trailingAnchor), scrollView.bottomAnchor.constraint(equalTo: footer.topAnchor, constant: -12), emptyLabel.centerXAnchor.constraint(equalTo: scrollView.centerXAnchor), emptyLabel.centerYAnchor.constraint(equalTo: scrollView.centerYAnchor), emptyLabel.leadingAnchor.constraint(greaterThanOrEqualTo: content.leadingAnchor, constant: 16), emptyLabel.trailingAnchor.constraint(lessThanOrEqualTo: content.trailingAnchor, constant: -16), footer.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 16), footer.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -16), footer.bottomAnchor.constraint(equalTo: content.bottomAnchor, constant: -12), ]) window.initialFirstResponder = searchField } private func refreshApplications() { guard discoveryTask == nil else { return } statusLabel.stringValue = "Finding applications…" discoveryTask = Task { [weak self] in let applications = await Task.detached(priority: .userInitiated) { ApplicationCatalog.discover() }.value guard let self else { return } self.applications = applications self.discoveryTask = nil self.updateResults() } } private func updateResults() { results = ApplicationSearch.results(for: searchField.stringValue, in: applications) tableView.reloadData() if !results.isEmpty { tableView.selectRowIndexes(IndexSet(integer: 0), byExtendingSelection: false) tableView.scrollRowToVisible(0) } emptyLabel.isHidden = !results.isEmpty if discoveryTask != nil { emptyLabel.stringValue = "Finding applications…" statusLabel.stringValue = "Finding applications…" } else { emptyLabel.stringValue = applications.isEmpty ? "No applications found" : "No matching applications" statusLabel.stringValue = results.count == 1 ? "1 application" : "\(results.count) applications" } updateOpenButton() } func controlTextDidChange(_ notification: Notification) { updateResults() } func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool { switch commandSelector { case #selector(NSResponder.moveDown(_:)): moveSelection(by: 1) case #selector(NSResponder.moveUp(_:)): moveSelection(by: -1) case #selector(NSResponder.insertNewline(_:)): openSelection() case #selector(NSResponder.cancelOperation(_:)): window?.orderOut(nil) default: return false } return true } private func moveSelection(by offset: Int) { guard !results.isEmpty else { return } let row = min(max(tableView.selectedRow + offset, 0), results.count - 1) tableView.selectRowIndexes(IndexSet(integer: row), byExtendingSelection: false) tableView.scrollRowToVisible(row) } private func updateOpenButton() { openButton.isEnabled = !isLaunching && results.indices.contains(tableView.selectedRow) } func tableViewSelectionDidChange(_ notification: Notification) { updateOpenButton() } func numberOfRows(in tableView: NSTableView) -> Int { results.count } func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { let identifier = NSUserInterfaceItemIdentifier("applicationCell") let cell = (tableView.makeView(withIdentifier: identifier, owner: self) as? NSTableCellView) ?? makeCell(identifier: identifier) let application = results[row] cell.textField?.stringValue = application.name cell.imageView?.image = NSWorkspace.shared.icon(forFile: application.url.path) cell.toolTip = application.url.path return cell } private func makeCell(identifier: NSUserInterfaceItemIdentifier) -> NSTableCellView { let cell = NSTableCellView() cell.identifier = identifier let icon = NSImageView() icon.imageScaling = .scaleProportionallyDown icon.setAccessibilityElement(false) let label = NSTextField(labelWithString: "") label.lineBreakMode = .byTruncatingTail cell.imageView = icon cell.textField = label for view in [icon, label] { view.translatesAutoresizingMaskIntoConstraints = false cell.addSubview(view) } NSLayoutConstraint.activate([ icon.leadingAnchor.constraint(equalTo: cell.leadingAnchor, constant: 8), icon.centerYAnchor.constraint(equalTo: cell.centerYAnchor), icon.widthAnchor.constraint(equalToConstant: 28), icon.heightAnchor.constraint(equalToConstant: 28), label.leadingAnchor.constraint(equalTo: icon.trailingAnchor, constant: 12), label.centerYAnchor.constraint(equalTo: cell.centerYAnchor), label.trailingAnchor.constraint(equalTo: cell.trailingAnchor, constant: -8), ]) return cell } @objc private func openSelection() { guard !isLaunching, results.indices.contains(tableView.selectedRow) else { return } let application = results[tableView.selectedRow] isLaunching = true updateOpenButton() Task { do { try await AppLauncher.launch(application.url) window?.orderOut(nil) } catch { let alert = NSAlert(error: error) alert.messageText = "Couldn’t open \(application.name)" if let window { await alert.beginSheetModal(for: window) } } isLaunching = false updateOpenButton() } } } @MainActor private final class ApplicationTableView: NSTableView { var openSelection: (() -> Void)? var dismiss: (() -> Void)? var editSearch: ((NSEvent) -> Void)? override func keyDown(with event: NSEvent) { switch event.keyCode { case 36, 76: openSelection?() case 53: dismiss?() default: let isEditing = event.keyCode == 51 || event.keyCode == 117 || (event.characters?.unicodeScalars.contains { !CharacterSet.controlCharacters.contains($0) } ?? false) if isEditing && event.modifierFlags.intersection([.command, .control]).isEmpty { editSearch?(event) } else { super.keyDown(with: event) } } } }