Created
May 22, 2026 13:00
-
-
Save alin23/69c0b52b03ab68e3b238ff4ef0f6fde7 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // | |
| // KeylumeIntegration.swift | |
| // | |
| // Drop-in helper for integrating Keylume (https://lowtechguys.com/keylume) | |
| // into a macOS Swift app. Handles a headless install/uninstall/reinstall | |
| // flow, install + running-state detection, and posting the show / hide | |
| // distributed notifications to drive a temporary keyboard layer. | |
| // | |
| // Single-file: no external dependencies beyond SwiftUI + AppKit, so this | |
| // file can be copied into any macOS app target as-is. | |
| // | |
| // Usage: | |
| // | |
| // // Show a temporary keyboard with two keys bound to apps: | |
| // KeylumeIntegration.shared.showKeyboard(bindings: [ | |
| // "q": .app(bundleID: "com.apple.Safari"), | |
| // "w": .symbolText(symbol: "magnifyingglass", text: "Search"), | |
| // ]) | |
| // | |
| // // Hide it later: | |
| // KeylumeIntegration.shared.hideKeyboard() | |
| // | |
| // // Render a Settings pane: | |
| // var body: some View { | |
| // KeylumeSettingsForm() | |
| // } | |
| // | |
| import AppKit | |
| import Combine | |
| import SwiftUI | |
| // MARK: - Constants | |
| public enum Keylume { | |
| public static let bundleID = "com.lowtechguys.Keylume" | |
| public static let appPath = "/Applications/Keylume.app" | |
| public static let downloadURL = URL(string: "https://files.lowtechguys.com/releases/Keylume.zip")! | |
| /// `UserDefaults` keys used by Keylume itself. Set on the | |
| /// `com.lowtechguys.Keylume` suite before first launch to opt the | |
| /// companion into a fully background, no-UI install. | |
| enum HeadlessKey { | |
| static let menuBarIconVisible = "menuBarIconVisible" | |
| static let layersEnabled = "layersEnabled" | |
| static let adaptToKeyboardLayout = "adaptToKeyboardLayout" | |
| static let showOnScreenRecording = "showOnScreenRecording" | |
| static let enableGlobalHotkey = "enableGlobalHotkey" | |
| static let launchAtLogin = "launchAtLogin" | |
| static let passedProFeaturesOnboarding = "passedProFeaturesOnboarding" | |
| static let onboardingVersion = "onboardingVersion" | |
| static let sparkleEnableAutomaticChecks = "SUEnableAutomaticChecks" | |
| static let sparkleAutomaticallyUpdate = "SUAutomaticallyUpdate" | |
| static let sparkleHasLaunchedBefore = "SUHasLaunchedBefore" | |
| static let sparkleScheduledCheckInterval = "SUScheduledCheckInterval" | |
| } | |
| } | |
| public extension Notification.Name { | |
| static let keylumeShow = Notification.Name("com.lowtechguys.Keylume.show") | |
| static let keylumeHide = Notification.Name("com.lowtechguys.Keylume.hide") | |
| } | |
| // MARK: - Public types | |
| public enum KeylumeInstallState: Equatable { | |
| case notInstalled | |
| case downloading(progress: Double) | |
| case extracting | |
| case configuring | |
| case installed | |
| case failed(String) | |
| } | |
| public enum KeylumeFillMode: String, CaseIterable, Codable { | |
| case visible | |
| case dim | |
| case blank | |
| public var label: String { | |
| switch self { | |
| case .visible: "Visible" | |
| case .dim: "Dim" | |
| case .blank: "Blank" | |
| } | |
| } | |
| } | |
| public enum KeylumePosition: String, CaseIterable { | |
| case bottom, top, left, right, center | |
| case topLeft, topRight, bottomLeft, bottomRight | |
| } | |
| /// Value bound to a Keylume keyboard key. Encodes to the value format the | |
| /// `keylume://show` URL scheme accepts. See Keylume's REMOTE_CONTROL.md. | |
| public enum KeylumeBinding { | |
| case app(bundleID: String) | |
| case symbol(name: String) | |
| case text(String) | |
| case symbolText(symbol: String, text: String) | |
| case raw(String) | |
| var encoded: String { | |
| switch self { | |
| case let .app(bundleID): "a:\(bundleID)" | |
| case let .symbol(name): "s:\(name)" | |
| case let .text(text): text | |
| case let .symbolText(symbol, text): "symtext:\(symbol)|\(text)" | |
| case let .raw(value): value | |
| } | |
| } | |
| } | |
| // MARK: - Manager | |
| @MainActor | |
| public final class KeylumeIntegration: ObservableObject { | |
| private init() { | |
| installState = Self.isInstalledOnDisk() ? .installed : .notInstalled | |
| running = !NSRunningApplication.runningApplications(withBundleIdentifier: Keylume.bundleID).isEmpty | |
| fillMode = UserDefaults.standard.string(forKey: "keylume.fillMode") | |
| .flatMap(KeylumeFillMode.init(rawValue:)) ?? .dim | |
| observeWorkspace() | |
| } | |
| deinit { | |
| let observers = workspaceObservers | |
| let nc = NSWorkspace.shared.notificationCenter | |
| for o in observers { | |
| nc.removeObserver(o) | |
| } | |
| } | |
| public static let shared = KeylumeIntegration() | |
| @Published public private(set) var installState: KeylumeInstallState = .notInstalled | |
| @Published public private(set) var running = false | |
| /// Persisted user preference for how Keylume renders keys without a | |
| /// binding. The settings Form reads/writes this; pass it through to | |
| /// `showKeyboard(fillMode:)` if you want the setting to apply. | |
| @Published public var fillMode: KeylumeFillMode { | |
| didSet { | |
| UserDefaults.standard.set(fillMode.rawValue, forKey: "keylume.fillMode") | |
| } | |
| } | |
| // MARK: Detection | |
| public static func isInstalledOnDisk() -> Bool { | |
| FileManager.default.fileExists(atPath: Keylume.appPath) | |
| } | |
| public func isInstalled() -> Bool { Self.isInstalledOnDisk() } | |
| public func isRunning() -> Bool { | |
| !NSRunningApplication.runningApplications(withBundleIdentifier: Keylume.bundleID).isEmpty | |
| } | |
| // MARK: Show / hide | |
| /// Show a temporary Keylume keyboard with the given key bindings. | |
| /// Bindings clear automatically when the keyboard hides. | |
| public func showKeyboard( | |
| bindings: [String: KeylumeBinding], | |
| position: KeylumePosition = .bottom, | |
| offset: CGPoint = CGPoint(x: 0, y: 100), | |
| width: Double = 1000, | |
| fillMode: KeylumeFillMode? = nil | |
| ) { | |
| guard isInstalled() else { return } | |
| var userInfo: [String: String] = [:] | |
| for (key, value) in bindings { | |
| userInfo[key] = value.encoded | |
| } | |
| userInfo["position"] = position.rawValue | |
| userInfo["offset"] = "\(Int(offset.x)):\(Int(offset.y))" | |
| userInfo["width"] = String(Int(width)) | |
| userInfo["fill"] = (fillMode ?? self.fillMode).rawValue | |
| // Apply the fill effect to modifier keys (shift, ctrl, option, cmd, | |
| // fn) too, so an `app-launcher`-style layer doesn't leave a noisy | |
| // row of full-opacity modifier glyphs around the dimmed letters. | |
| userInfo["dimBlankModifierKeys"] = "1" | |
| DistributedNotificationCenter.default().postNotificationName( | |
| .keylumeShow, object: nil, userInfo: userInfo, deliverImmediately: true | |
| ) | |
| } | |
| public func hideKeyboard() { | |
| DistributedNotificationCenter.default().postNotificationName( | |
| .keylumeHide, object: nil, userInfo: nil, deliverImmediately: true | |
| ) | |
| } | |
| // MARK: Install | |
| /// Download Keylume.zip, extract to /Applications, write headless | |
| /// preferences so the companion runs invisibly, and launch it. Safe to | |
| /// call when Keylume is already installed and/or running, quits and | |
| /// replaces in place. | |
| public func installHeadless() { | |
| guard !isCurrentlyInstalling else { return } | |
| installState = .downloading(progress: 0) | |
| let dest = URL(fileURLWithPath: Keylume.appPath) | |
| let tmpDir = URL(fileURLWithPath: NSTemporaryDirectory()) | |
| .appendingPathComponent("keylume-install-\(UUID().uuidString)") | |
| try? FileManager.default.createDirectory(at: tmpDir, withIntermediateDirectories: true) | |
| let zipPath = tmpDir.appendingPathComponent("Keylume.zip") | |
| let task = URLSession.shared.downloadTask(with: Keylume.downloadURL) { [weak self] url, _, error in | |
| guard let self else { return } | |
| if let error { | |
| Task { @MainActor in self.installState = .failed("Download failed: \(error.localizedDescription)") } | |
| return | |
| } | |
| guard let url else { | |
| Task { @MainActor in self.installState = .failed("Download failed: no file") } | |
| return | |
| } | |
| do { | |
| if FileManager.default.fileExists(atPath: zipPath.path) { | |
| try FileManager.default.removeItem(at: zipPath) | |
| } | |
| try FileManager.default.moveItem(at: url, to: zipPath) | |
| } catch { | |
| Task { @MainActor in self.installState = .failed("Move failed: \(error.localizedDescription)") } | |
| return | |
| } | |
| Task { @MainActor in | |
| self.installState = .extracting | |
| self.unzipAndInstall(zipPath: zipPath, destination: dest, scratchDir: tmpDir) | |
| } | |
| } | |
| progressObservation = task.progress.observe(\.fractionCompleted) { [weak self] progress, _ in | |
| Task { @MainActor in | |
| guard let self else { return } | |
| if case .downloading = self.installState { | |
| self.installState = .downloading(progress: progress.fractionCompleted) | |
| } | |
| } | |
| } | |
| task.resume() | |
| } | |
| /// Quit any running Keylume, delete the bundle, and clear the running | |
| /// state. Leaves the user's Keylume preferences in place so a | |
| /// subsequent install keeps their themes and layers. | |
| public func uninstall() { | |
| quitRunningKeylume() | |
| try? FileManager.default.removeItem(atPath: Keylume.appPath) | |
| installState = .notInstalled | |
| running = false | |
| } | |
| private var progressObservation: NSKeyValueObservation? | |
| private var workspaceObservers: [NSObjectProtocol] = [] | |
| private var isCurrentlyInstalling: Bool { | |
| switch installState { | |
| case .downloading, .extracting, .configuring: true | |
| default: false | |
| } | |
| } | |
| private func observeWorkspace() { | |
| let center = NSWorkspace.shared.notificationCenter | |
| let launch = center.addObserver(forName: NSWorkspace.didLaunchApplicationNotification, object: nil, queue: .main) { [weak self] note in | |
| guard let app = note.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication, | |
| app.bundleIdentifier == Keylume.bundleID else { return } | |
| Task { @MainActor in self?.running = true } | |
| } | |
| let terminate = center.addObserver(forName: NSWorkspace.didTerminateApplicationNotification, object: nil, queue: .main) { [weak self] note in | |
| guard let app = note.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication, | |
| app.bundleIdentifier == Keylume.bundleID else { return } | |
| Task { @MainActor in self?.running = self?.isRunning() ?? false } | |
| } | |
| workspaceObservers = [launch, terminate] | |
| } | |
| private func unzipAndInstall(zipPath: URL, destination: URL, scratchDir: URL) { | |
| // `ditto -xk` preserves Apple metadata + code signatures so | |
| // NSWorkspace.openApplication can launch the freshly extracted .app | |
| // without Gatekeeper rejecting it. | |
| let unzip = Process() | |
| unzip.executableURL = URL(fileURLWithPath: "/usr/bin/ditto") | |
| unzip.arguments = ["-xk", zipPath.path, scratchDir.path] | |
| do { | |
| try unzip.run() | |
| unzip.waitUntilExit() | |
| } catch { | |
| installState = .failed("Unzip failed: \(error.localizedDescription)") | |
| return | |
| } | |
| guard unzip.terminationStatus == 0 else { | |
| installState = .failed("Unzip failed (exit \(unzip.terminationStatus))") | |
| return | |
| } | |
| let extractedApp = scratchDir.appendingPathComponent("Keylume.app") | |
| guard FileManager.default.fileExists(atPath: extractedApp.path) else { | |
| installState = .failed("Keylume.app not found in archive") | |
| return | |
| } | |
| // Replace in place: a running Keylume holds open file handles inside | |
| // the bundle, which silently corrupts the replace if we don't quit | |
| // first. | |
| quitRunningKeylume() | |
| if FileManager.default.fileExists(atPath: destination.path) { | |
| try? FileManager.default.removeItem(at: destination) | |
| } | |
| do { | |
| try FileManager.default.moveItem(at: extractedApp, to: destination) | |
| } catch { | |
| installState = .failed("Install to /Applications failed: \(error.localizedDescription)") | |
| return | |
| } | |
| try? FileManager.default.removeItem(at: scratchDir) | |
| installState = .configuring | |
| writeHeadlessPreferences() | |
| launchKeylume() | |
| // Give Keylume a moment to come up before the running observer fires. | |
| DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { [weak self] in | |
| guard let self else { return } | |
| installState = .installed | |
| } | |
| } | |
| private func writeHeadlessPreferences() { | |
| guard let defaults = UserDefaults(suiteName: Keylume.bundleID) else { return } | |
| defaults.set(false, forKey: Keylume.HeadlessKey.menuBarIconVisible) | |
| defaults.set(false, forKey: Keylume.HeadlessKey.layersEnabled) | |
| defaults.set(false, forKey: Keylume.HeadlessKey.adaptToKeyboardLayout) | |
| defaults.set(false, forKey: Keylume.HeadlessKey.showOnScreenRecording) | |
| defaults.set(false, forKey: Keylume.HeadlessKey.enableGlobalHotkey) | |
| defaults.set(true, forKey: Keylume.HeadlessKey.launchAtLogin) | |
| // Skip onboarding (current Keylume version is 2; 100 future-proofs). | |
| defaults.set(true, forKey: Keylume.HeadlessKey.passedProFeaturesOnboarding) | |
| defaults.set(100, forKey: Keylume.HeadlessKey.onboardingVersion) | |
| // Sparkle: automatic + silent updates. | |
| defaults.set(true, forKey: Keylume.HeadlessKey.sparkleEnableAutomaticChecks) | |
| defaults.set(true, forKey: Keylume.HeadlessKey.sparkleAutomaticallyUpdate) | |
| defaults.set(true, forKey: Keylume.HeadlessKey.sparkleHasLaunchedBefore) | |
| defaults.set(3600, forKey: Keylume.HeadlessKey.sparkleScheduledCheckInterval) | |
| defaults.synchronize() | |
| } | |
| private func launchKeylume() { | |
| let url = URL(fileURLWithPath: Keylume.appPath) | |
| let config = NSWorkspace.OpenConfiguration() | |
| config.activates = false | |
| config.addsToRecentItems = false | |
| NSWorkspace.shared.openApplication(at: url, configuration: config) { _, _ in } | |
| } | |
| private func quitRunningKeylume() { | |
| let running = NSRunningApplication.runningApplications(withBundleIdentifier: Keylume.bundleID) | |
| guard !running.isEmpty else { return } | |
| for app in running { | |
| app.terminate() | |
| } | |
| let deadline = Date().addingTimeInterval(2.0) | |
| while Date() < deadline, | |
| !NSRunningApplication.runningApplications(withBundleIdentifier: Keylume.bundleID).isEmpty | |
| { | |
| Thread.sleep(forTimeInterval: 0.1) | |
| } | |
| for app in NSRunningApplication.runningApplications(withBundleIdentifier: Keylume.bundleID) { | |
| app.forceTerminate() | |
| } | |
| } | |
| } | |
| // MARK: - SwiftUI Settings Form | |
| /// A modestly-styled grouped `Form` with the install row, fill-mode picker, | |
| /// and (optionally) any additional content the host app supplies. Drop in | |
| /// as the body of a Settings scene tab. | |
| public struct KeylumeSettingsForm<Extra: View>: View { | |
| public init( | |
| title: String = "Keylume integration", | |
| subtitle: String = "On-screen keyboard companion for app launchers and shortcut overlays.", | |
| @ViewBuilder extra: @escaping () -> Extra = { EmptyView() } | |
| ) { | |
| self.title = title | |
| self.subtitle = subtitle | |
| extraContent = extra | |
| } | |
| public var body: some View { | |
| Form { | |
| Section { | |
| installRow | |
| } header: { | |
| VStack(alignment: .leading, spacing: 4) { | |
| Text(title).font(.system(size: 16, weight: .bold)) | |
| Text(subtitle) | |
| .font(.system(size: 11)) | |
| .foregroundColor(.secondary) | |
| .fixedSize(horizontal: false, vertical: true) | |
| } | |
| .padding(.bottom, 12) | |
| } | |
| Section { | |
| HStack(spacing: 10) { | |
| VStack(alignment: .leading, spacing: 2) { | |
| Text("Unassigned keys").font(.system(size: 13, weight: .medium)) | |
| Text("How keys without a binding appear on the Keylume keyboard.") | |
| .font(.system(size: 10)) | |
| .foregroundColor(.secondary) | |
| .fixedSize(horizontal: false, vertical: true) | |
| } | |
| .frame(maxWidth: .infinity, alignment: .leading) | |
| Picker("", selection: $integration.fillMode) { | |
| ForEach(KeylumeFillMode.allCases, id: \.self) { mode in | |
| Text(mode.label).tag(mode) | |
| } | |
| } | |
| .labelsHidden() | |
| .pickerStyle(.segmented) | |
| .fixedSize() | |
| } | |
| .disabled(!isInstalled) | |
| .opacity(isInstalled ? 1 : 0.5) | |
| extraContent() | |
| } header: { | |
| Text("Behavior") | |
| } | |
| } | |
| .formStyle(.grouped) | |
| .scrollContentBackground(.hidden) | |
| } | |
| @ObservedObject private var integration = KeylumeIntegration.shared | |
| private let title: String | |
| private let subtitle: String | |
| private let extraContent: () -> Extra | |
| private var isInstalled: Bool { | |
| if case .installed = integration.installState { return true } | |
| return false | |
| } | |
| private var statusDotColor: Color { | |
| if !isInstalled { return .red } | |
| return integration.running ? .green : .orange | |
| } | |
| private var statusDotTooltip: String { | |
| if !isInstalled { return "Keylume not installed" } | |
| return integration.running ? "Keylume installed and running" : "Keylume installed but not running" | |
| } | |
| private var installTitle: String { | |
| switch integration.installState { | |
| case .notInstalled: "Install Keylume" | |
| case .downloading: "Downloading Keylume…" | |
| case .extracting: "Extracting…" | |
| case .configuring: "Configuring headless mode…" | |
| case .installed: integration.running ? "Keylume running" : "Keylume installed" | |
| case .failed: "Installation failed" | |
| } | |
| } | |
| private var installSubtitle: String { | |
| switch integration.installState { | |
| case .notInstalled: | |
| "Downloads Keylume.app to /Applications and configures it for silent background use." | |
| case let .downloading(progress): | |
| String(format: "%.0f%% downloaded", progress * 100) | |
| case .extracting: | |
| "Unpacking the archive into /Applications." | |
| case .configuring: | |
| "Writing headless preferences and launching the companion." | |
| case .installed: | |
| integration.running | |
| ? "Installed at \(Keylume.appPath)." | |
| : "Installed at \(Keylume.appPath) but not running." | |
| case let .failed(reason): | |
| reason | |
| } | |
| } | |
| @ViewBuilder | |
| private var installRow: some View { | |
| HStack(alignment: .center, spacing: 12) { | |
| Circle() | |
| .fill(statusDotColor) | |
| .frame(width: 10, height: 10) | |
| .help(statusDotTooltip) | |
| VStack(alignment: .leading, spacing: 2) { | |
| Text(installTitle).font(.system(size: 13, weight: .medium)) | |
| Text(installSubtitle) | |
| .font(.system(size: 10)) | |
| .foregroundColor(.secondary) | |
| .fixedSize(horizontal: false, vertical: true) | |
| } | |
| Spacer() | |
| installButton | |
| } | |
| } | |
| @ViewBuilder | |
| private var installButton: some View { | |
| switch integration.installState { | |
| case .notInstalled, .failed: | |
| Button("Install") { integration.installHeadless() } | |
| case .downloading, .extracting, .configuring: | |
| ProgressView().controlSize(.small) | |
| case .installed: | |
| HStack(spacing: 8) { | |
| Button("Reinstall") { integration.installHeadless() } | |
| .buttonStyle(.bordered) | |
| Button("Uninstall") { integration.uninstall() } | |
| .buttonStyle(.bordered) | |
| .tint(.red) | |
| } | |
| } | |
| } | |
| } | |
| #if DEBUG | |
| #Preview("Keylume Settings Form") { | |
| KeylumeSettingsForm { | |
| Text("Sample additional settings content") | |
| .font(.system(size: 12)) | |
| .foregroundColor(.secondary) | |
| } | |
| .frame(width: 620) | |
| .padding() | |
| } | |
| #endif |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment