diff --git a/src/AppDelegate.swift b/src/AppDelegate.swift index 00b9536..83d1440 100644 --- a/src/AppDelegate.swift +++ b/src/AppDelegate.swift @@ -124,11 +124,8 @@ public final class AppDelegate: NSObject, NSApplicationDelegate { // 1. Check for Executive Voice Slash Commands if SonioxSettings.shared.voiceCommands && VoiceCommandExecutor.shared.isSlashCommand(text: trimmed) { - let handled = VoiceCommandExecutor.shared.execute(rawText: trimmed) - if handled { - print("AppDelegate: Successfully executed slash command for: \(trimmed)") - return // Handled as executive command — do not paste text! - } + VoiceCommandExecutor.shared.execute(rawText: trimmed) + return // STRICT: If speech starts with slash, NEVER paste into any active input box! } // 2. Normal dictation processing & paste diff --git a/src/VoiceCommandExecutor.swift b/src/VoiceCommandExecutor.swift index 378c46c..9d1a23f 100644 --- a/src/VoiceCommandExecutor.swift +++ b/src/VoiceCommandExecutor.swift @@ -1,73 +1,182 @@ import Cocoa import Carbon +public struct InstalledAppInfo { + public let name: String + public let normalizedName: String + public let url: URL +} + public final class VoiceCommandExecutor { public static let shared = VoiceCommandExecutor() - private init() {} + private var cachedInstalledApps: [InstalledAppInfo] = [] + private var lastAppsScanTime: TimeInterval = 0 + + // Comprehensive Persian to Canonical English Application Names + private let persianAppDictionary: [String: String] = [ + // System & Apple Apps + "ستینگ": "System Settings", + "ستینگز": "System Settings", + "تنظیمات": "System Settings", + "تنظیمات سیستم": "System Settings", + "سیستم ستینگ": "System Settings", + "فایندر": "Finder", + "ترمینال": "Terminal", + "سافاری": "Safari", + "ماشین حساب": "Calculator", + "کلکولیتور": "Calculator", + "تقویم": "Calendar", + "کلندر": "Calendar", + "یادداشت": "Notes", + "یادداشت‌ها": "Notes", + "نوت": "Notes", + "نوتس": "Notes", + "اپ استور": "App Store", + "فوتوز": "Photos", + "عکس": "Photos", + "موزیک": "Music", + "آهنگ": "Music", + "پادکست": "Podcasts", + "مخاطبین": "Contacts", + "پیام‌ها": "Messages", + "مسج": "Messages", + "ایمیل": "Mail", + "میل": "Mail", + "شورتکات": "Shortcuts", + "میانبرها": "Shortcuts", + "فری فرم": "Freeform", + "پریویو": "Preview", + "اکتیویتی مانیتور": "Activity Monitor", + "دیسک یوتیلیتی": "Disk Utility", + "پسورد": "Passwords", + + // Developer & Custom Apps on this Mac + "آنتی گرویتی آیدی ای": "Antigravity IDE", + "آنتی گرویتی آی دی ای": "Antigravity IDE", + "آنتی گرویتی ادیتور": "Antigravity IDE", + "آنتی گرویتی کد": "Antigravity IDE", + "آنتیگرویتی آیدی ای": "Antigravity IDE", + "انتی گرویتی ایدی ای": "Antigravity IDE", + + "آنتی گرویتی": "Antigravity", + "انتی گرویتی": "Antigravity", + "آنتیگرویتی": "Antigravity", + + "فایرفاکس": "Firefox", + "کروم": "Google Chrome", + "گوگل کروم": "Google Chrome", + "تلگرام": "Telegram", + "تیم بی": "TeamBy", + "تیم بای": "TeamBy", + "تیم‌بای": "TeamBy", + "تیمبی": "TeamBy", + "فیگما": "Figma", + "وی تو باکس": "V2Box", + "وی۲باکس": "V2Box", + "وی اس کد": "Visual Studio Code", + "کد": "Visual Studio Code", + "ویژوال استودیو": "Visual Studio Code", + "سانی اوکس": "Soniox Voice", + + // Utilities installed on this Mac + "روزگار": "Roozegar", + "رزگار": "Roozegar", + "شمسی کال": "ShamsiCal", + "چند": "Chand", + "میدل کلیک": "MiddleClick" + ] + + private init() { + refreshInstalledApps() + } public func isSlashCommand(text: String) -> Bool { let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() - return trimmed.hasPrefix("اسلش") || - trimmed.hasPrefix("slash") || - trimmed.hasPrefix("/") || - trimmed.hasPrefix("\\") + if trimmed.hasPrefix("اسلش") || + trimmed.hasPrefix("اصلش") || + trimmed.hasPrefix("slash") || + trimmed.hasPrefix("slsh") || + trimmed.hasPrefix("/") || + trimmed.hasPrefix("\\") { + return true + } + + let firstWord = trimmed.components(separatedBy: .whitespacesAndNewlines).first ?? "" + let cleanFirst = firstWord.trimmingCharacters(in: CharacterSet(charactersIn: ":,،.؛-_/\\")) + return cleanFirst == "اسلش" || cleanFirst == "اصلش" || cleanFirst == "slash" || cleanFirst == "slsh" } - @discardableResult - public func execute(rawText: String) -> Bool { - let trimmed = rawText.trimmingCharacters(in: .whitespacesAndNewlines) - guard isSlashCommand(text: trimmed) else { return false } + public func extractCommandBody(rawText: String) -> String { + var body = rawText.trimmingCharacters(in: .whitespacesAndNewlines) + let lower = body.lowercased() - // Strip trigger prefix - var body = trimmed - if body.lowercased().hasPrefix("اسلش") { + if lower.hasPrefix("اسلش") || lower.hasPrefix("اصلش") { body = String(body.dropFirst(4)).trimmingCharacters(in: .whitespacesAndNewlines) - } else if body.lowercased().hasPrefix("slash") { + } else if lower.hasPrefix("slash") { body = String(body.dropFirst(5)).trimmingCharacters(in: .whitespacesAndNewlines) + } else if lower.hasPrefix("slsh") { + body = String(body.dropFirst(4)).trimmingCharacters(in: .whitespacesAndNewlines) } else if body.hasPrefix("/") || body.hasPrefix("\\") { body = String(body.dropFirst(1)).trimmingCharacters(in: .whitespacesAndNewlines) + if body.hasPrefix("/") { + body = String(body.dropFirst(1)).trimmingCharacters(in: .whitespacesAndNewlines) + } } - body = body.trimmingCharacters(in: CharacterSet(charactersIn: ":,،.؛ -_")) + return body.trimmingCharacters(in: CharacterSet(charactersIn: ":,،.؛ -_")) + } + + @discardableResult + public func execute(rawText: String) -> Bool { + let trimmed = rawText.trimmingCharacters(in: .whitespacesAndNewlines) + guard isSlashCommand(text: trimmed) else { return false } + + let body = extractCommandBody(rawText: trimmed) let lower = body.lowercased() print("VoiceCommandExecutor: executing command for body: '\(body)'") - // 1. App with Tab combinations (e.g. "فایرفاکس تب ۱", "کروم تب ۲") + // 1. App + Tab combinations (e.g. "فایرفاکس تب ۱", "کروم تب ۲", "firefox tab 1", "chrome tab 3") if handleAppWithTab(lower: lower, original: body) { playConfirmationSound() return true } - // 2. Standalone Tab commands (e.g. "تب ۱", "تب جدید", "بستن تب") + // 2. Standalone Tab & Navigation commands (e.g. "تب ۱", "تب جدید", "بستن تب", "tab 1", "new tab") if handleStandaloneTab(lower: lower) { playConfirmationSound() return true } - // 3. System controls (volume, lock, screenshot, dark mode, clipboard) + // 3. System controls (volume, mute, lock, screenshot, dark mode, clipboard) if handleSystemControls(lower: lower) { playConfirmationSound() return true } - // 4. Named applications (Antigravity IDE, Antigravity, Settings, Firefox, etc.) - if handleNamedApp(lower: lower, original: body) { + // 4. Match App from Persian dictionary or exact names + if handleKnownApp(lower: lower, original: body) { playConfirmationSound() return true } - // 5. Generic application launch fallback - if handleGenericApp(original: body) { + // 5. Universal fuzzy resolver across all installed apps in /Applications and /System/Applications + if handleUniversalInstalledApp(query: body) { playConfirmationSound() return true } + // Command was prefixed with slash but body was not recognized + print("VoiceCommandExecutor: slash command body unrecognized: '\(body)'") + HistoryManager.shared.addEntry("⚡️ [Command] Unrecognized: \(body)") + playWarningSound() return false } private func handleAppWithTab(lower: String, original: String) -> Bool { + guard lower.contains("تب") || lower.contains("tab") else { return false } + var targetApp: String? = nil if lower.contains("فایرفاکس") || lower.contains("firefox") { targetApp = "Firefox" @@ -75,6 +184,10 @@ public final class VoiceCommandExecutor { targetApp = "Google Chrome" } else if lower.contains("سافاری") || lower.contains("safari") { targetApp = "Safari" + } else if lower.contains("آنتی گرویتی آیدی ای") || lower.contains("antigravity ide") { + targetApp = "Antigravity IDE" + } else if lower.contains("آنتی گرویتی") || lower.contains("antigravity") { + targetApp = "Antigravity" } guard let appName = targetApp else { return false } @@ -85,13 +198,13 @@ public final class VoiceCommandExecutor { } HistoryManager.shared.addEntry("⚡️ [Command] \(appName): Tab \(tabNum)") return true - } else if lower.contains("تب جدید") || lower.contains("new tab") { + } else if lower.contains("تب جدید") || lower.contains("نیو تب") || lower.contains("new tab") { activateApp(named: appName) { self.pressHotkey(keyCode: 17, modifiers: .maskCommand) // Cmd+T } HistoryManager.shared.addEntry("⚡️ [Command] \(appName): New Tab") return true - } else if lower.contains("بستن تب") || lower.contains("close tab") { + } else if lower.contains("بستن تب") || lower.contains("کلوز تب") || lower.contains("close tab") { activateApp(named: appName) { self.pressHotkey(keyCode: 13, modifiers: .maskCommand) // Cmd+W } @@ -104,31 +217,49 @@ public final class VoiceCommandExecutor { private func extractTabNumber(from text: String) -> Int? { let t = text.lowercased() - if t.contains("تب ۱") || t.contains("تب 1") || t.contains("تب یک") || t.contains("تب اول") || t.contains("tab 1") || t.contains("tab one") { + // Tab 1 + if t.contains("تب ۱") || t.contains("تب 1") || t.contains("تب یک") || t.contains("تب اول") || + t.contains("tab 1") || t.contains("tab one") || t.contains("tab first") { return 1 } - if t.contains("تب ۲") || t.contains("تب 2") || t.contains("تب دو") || t.contains("تب دوم") || t.contains("tab 2") || t.contains("tab two") { + // Tab 2 + if t.contains("تب ۲") || t.contains("تب 2") || t.contains("تب دو") || t.contains("تب دوم") || + t.contains("tab 2") || t.contains("tab two") || t.contains("tab second") { return 2 } - if t.contains("تب ۳") || t.contains("تب 3") || t.contains("تب سه") || t.contains("تب سوم") || t.contains("tab 3") || t.contains("tab three") { + // Tab 3 + if t.contains("تب ۳") || t.contains("تب 3") || t.contains("تب سه") || t.contains("تب سوم") || + t.contains("tab 3") || t.contains("tab three") || t.contains("tab third") { return 3 } - if t.contains("تب ۴") || t.contains("تب 4") || t.contains("تب چهار") || t.contains("تب چهارم") || t.contains("tab 4") || t.contains("tab four") { + // Tab 4 + if t.contains("تب ۴") || t.contains("تب 4") || t.contains("تب چهار") || t.contains("تب چهارم") || + t.contains("tab 4") || t.contains("tab four") { return 4 } - if t.contains("تب ۵") || t.contains("تب 5") || t.contains("تب پنج") || t.contains("تب پنجم") || t.contains("tab 5") || t.contains("tab five") { + // Tab 5 + if t.contains("تب ۵") || t.contains("تب 5") || t.contains("تب پنج") || t.contains("تب پنجم") || + t.contains("tab 5") || t.contains("tab five") { return 5 } - if t.contains("تب ۶") || t.contains("تب 6") || t.contains("تب شش") || t.contains("تب ششم") || t.contains("tab 6") || t.contains("tab six") { + // Tab 6 + if t.contains("تب ۶") || t.contains("تب 6") || t.contains("تب شش") || t.contains("تب ششم") || + t.contains("tab 6") || t.contains("tab six") { return 6 } - if t.contains("تب ۷") || t.contains("تب 7") || t.contains("تب هفت") || t.contains("تب هفتم") || t.contains("tab 7") || t.contains("tab seven") { + // Tab 7 + if t.contains("تب ۷") || t.contains("تب 7") || t.contains("تب هفت") || t.contains("تب هفتم") || + t.contains("tab 7") || t.contains("tab seven") { return 7 } - if t.contains("تب ۸") || t.contains("تب 8") || t.contains("تب هشت") || t.contains("تب هشتم") || t.contains("tab 8") || t.contains("tab eight") { + // Tab 8 + if t.contains("تب ۸") || t.contains("تب 8") || t.contains("تب هشت") || t.contains("تب هشتم") || + t.contains("tab 8") || t.contains("tab eight") { return 8 } - if t.contains("تب ۹") || t.contains("تب 9") || t.contains("تب نه") || t.contains("تب آخر") || t.contains("tab 9") || t.contains("tab last") { + // Tab 9 / Last Tab + if t.contains("تب ۹") || t.contains("تب 9") || t.contains("تب نه") || t.contains("تب نهم") || t.contains("تب آخر") || + t.contains("tab 9") || t.contains("tab nine") || t.contains("tab last") { return 9 } return nil @@ -168,115 +299,6 @@ public final class VoiceCommandExecutor { return false } - private func handleNamedApp(lower: String, original: String) -> Bool { - // System Settings - if lower.contains("ستینگ") || lower.contains("settings") || lower.contains("تنظیمات") || lower.contains("سیستم ستینگ") { - launchOrActivateApp(named: "System Settings") - HistoryManager.shared.addEntry("⚡️ [Command] Open System Settings") - return true - } - - // Antigravity IDE - if lower.contains("آنتی گرویتی آیدی ای") || lower.contains("آنتی گرویتی آی دی ای") || lower.contains("antigravity ide") || lower.contains("آنتیگرویتی آیدی ای") { - launchOrActivateApp(named: "Antigravity IDE") - HistoryManager.shared.addEntry("⚡️ [Command] Open Antigravity IDE") - return true - } - - // Antigravity - if lower.contains("آنتی گرویتی") || lower.contains("انتی گرویتی") || lower.contains("آنتیگرویتی") || lower.contains("antigravity") { - launchOrActivateApp(named: "Antigravity") - HistoryManager.shared.addEntry("⚡️ [Command] Open Antigravity") - return true - } - - // Firefox - if lower.contains("فایرفاکس") || lower.contains("firefox") { - launchOrActivateApp(named: "Firefox") - HistoryManager.shared.addEntry("⚡️ [Command] Open Firefox") - return true - } - - // Google Chrome - if lower.contains("گوگل کروم") || lower.contains("کروم") || lower.contains("chrome") { - launchOrActivateApp(named: "Google Chrome") - HistoryManager.shared.addEntry("⚡️ [Command] Open Google Chrome") - return true - } - - // Telegram - if lower.contains("تلگرام") || lower.contains("telegram") { - launchOrActivateApp(named: "Telegram") - HistoryManager.shared.addEntry("⚡️ [Command] Open Telegram") - return true - } - - // TeamBy - if lower.contains("تیم بای") || lower.contains("تیمبی") || lower.contains("teamby") { - launchOrActivateApp(named: "TeamBy") - HistoryManager.shared.addEntry("⚡️ [Command] Open TeamBy") - return true - } - - // Terminal - if lower.contains("ترمینال") || lower.contains("terminal") { - launchOrActivateApp(named: "Terminal") - HistoryManager.shared.addEntry("⚡️ [Command] Open Terminal") - return true - } - - // Finder - if lower.contains("فایندر") || lower.contains("finder") { - launchOrActivateApp(named: "Finder") - HistoryManager.shared.addEntry("⚡️ [Command] Open Finder") - return true - } - - // VS Code - if lower.contains("وی اس کد") || lower.contains("vscode") || lower == "کد" || lower == "code" { - launchOrActivateApp(named: "Visual Studio Code") - HistoryManager.shared.addEntry("⚡️ [Command] Open Visual Studio Code") - return true - } - - // Figma - if lower.contains("فیگما") || lower.contains("figma") { - launchOrActivateApp(named: "Figma") - HistoryManager.shared.addEntry("⚡️ [Command] Open Figma") - return true - } - - // Safari - if lower.contains("سافاری") || lower.contains("safari") { - launchOrActivateApp(named: "Safari") - HistoryManager.shared.addEntry("⚡️ [Command] Open Safari") - return true - } - - // Calculator - if lower.contains("ماشین حساب") || lower.contains("calculator") { - launchOrActivateApp(named: "Calculator") - HistoryManager.shared.addEntry("⚡️ [Command] Open Calculator") - return true - } - - // Calendar - if lower.contains("تقویم") || lower.contains("calendar") { - launchOrActivateApp(named: "Calendar") - HistoryManager.shared.addEntry("⚡️ [Command] Open Calendar") - return true - } - - // Notes - if lower.contains("یادداشت") || lower.contains("نوت") || lower.contains("notes") { - launchOrActivateApp(named: "Notes") - HistoryManager.shared.addEntry("⚡️ [Command] Open Notes") - return true - } - - return false - } - private func handleSystemControls(lower: String) -> Bool { // Volume controls if lower.contains("صدا زیاد") || lower.contains("افزایش صدا") || lower.contains("volume up") { @@ -296,7 +318,7 @@ public final class VoiceCommandExecutor { } // Lock screen - if lower.contains("قفل") || lower.contains("لاک") || lower.contains("lock screen") || lower.contains("lock") { + if lower.contains("قفل") || lower.contains("لاک") || lower.contains("lock screen") || lower == "lock" { let task = Process() task.executableURL = URL(fileURLWithPath: "/usr/bin/pmset") task.arguments = ["displaysleepnow"] @@ -316,63 +338,147 @@ public final class VoiceCommandExecutor { } // Dark Mode toggle - if lower.contains("دارک مود") || lower.contains("لایت مود") || lower.contains("حالت شب") || lower.contains("dark mode") { + if lower.contains("دارک مود") || lower.contains("لایت مود") || lower.contains("حالت شب") || lower.contains("حالت روز") || lower.contains("dark mode") { runAppleScript("tell application \"System Events\" to tell appearance preferences to set dark mode to not dark mode") HistoryManager.shared.addEntry("⚡️ [Command] Toggle Dark Mode") return true } - // Editing controls + // Clipboard & Editing hotkeys if lower == "کپی" || lower == "copy" { pressHotkey(keyCode: 8, modifiers: .maskCommand) // Cmd+C + HistoryManager.shared.addEntry("⚡️ [Command] Copy") return true } if lower == "پیست" || lower == "paste" { pressHotkey(keyCode: 9, modifiers: .maskCommand) // Cmd+V + HistoryManager.shared.addEntry("⚡️ [Command] Paste") return true } if lower == "کات" || lower == "cut" { pressHotkey(keyCode: 7, modifiers: .maskCommand) // Cmd+X + HistoryManager.shared.addEntry("⚡️ [Command] Cut") return true } if lower.contains("انتخاب همه") || lower.contains("سلکت آل") || lower == "select all" { pressHotkey(keyCode: 0, modifiers: .maskCommand) // Cmd+A + HistoryManager.shared.addEntry("⚡️ [Command] Select All") return true } if lower == "آندو" || lower == "برگرد" || lower == "undo" { pressHotkey(keyCode: 6, modifiers: .maskCommand) // Cmd+Z + HistoryManager.shared.addEntry("⚡️ [Command] Undo") return true } if lower == "اینتر" || lower == "enter" { - pressHotkey(keyCode: 36, modifiers: []) // Return + pressHotkey(keyCode: 36, modifiers: []) // Return key + HistoryManager.shared.addEntry("⚡️ [Command] Enter") return true } return false } - private func handleGenericApp(original: String) -> Bool { - var appCandidate = original - if appCandidate.hasPrefix("باز کن ") || appCandidate.hasPrefix("بازکن ") { - appCandidate = String(appCandidate.dropFirst(7)).trimmingCharacters(in: .whitespacesAndNewlines) - } else if appCandidate.lowercased().hasPrefix("open ") { - appCandidate = String(appCandidate.dropFirst(5)).trimmingCharacters(in: .whitespacesAndNewlines) + private func handleKnownApp(lower: String, original: String) -> Bool { + // Strip common prefix like "باز کن" or "open" + var query = lower + for prefix in ["باز کن ", "بازکن ", "برو به ", "برو تو ", "اجرا کن ", "open ", "launch ", "goto "] { + if query.hasPrefix(prefix) { + query = String(query.dropFirst(prefix.count)).trimmingCharacters(in: .whitespacesAndNewlines) + break + } } - guard !appCandidate.isEmpty else { return false } + // Check exact or partial match in Persian App Dictionary + for (persianKey, englishAppName) in persianAppDictionary { + if query == persianKey || query.contains(persianKey) { + launchOrActivateApp(named: englishAppName) + HistoryManager.shared.addEntry("⚡️ [Command] Open \(englishAppName)") + return true + } + } - let ws = NSWorkspace.shared - let appURL = ws.urlForApplication(withBundleIdentifier: appCandidate) ?? - URL(fileURLWithPath: "/Applications/\(appCandidate).app") - if FileManager.default.fileExists(atPath: appURL.path) || ws.urlForApplication(withBundleIdentifier: appCandidate) != nil { - launchOrActivateApp(named: appCandidate) - HistoryManager.shared.addEntry("⚡️ [Command] Open \(appCandidate)") + return false + } + + private func handleUniversalInstalledApp(query: String) -> Bool { + var cleanQuery = query.trimmingCharacters(in: .whitespacesAndNewlines) + for prefix in ["باز کن ", "بازکن ", "open ", "launch "] { + if cleanQuery.lowercased().hasPrefix(prefix) { + cleanQuery = String(cleanQuery.dropFirst(prefix.count)).trimmingCharacters(in: .whitespacesAndNewlines) + break + } + } + guard !cleanQuery.isEmpty else { return false } + + let normalizedQuery = normalizeForMatching(cleanQuery) + refreshInstalledApps() + + // 1. Check exact normalized match + if let match = cachedInstalledApps.first(where: { $0.normalizedName == normalizedQuery }) { + launchApp(at: match.url, name: match.name) + return true + } + + // 2. Check prefix match + if let match = cachedInstalledApps.first(where: { $0.normalizedName.hasPrefix(normalizedQuery) }) { + launchApp(at: match.url, name: match.name) + return true + } + + // 3. Check contains match + if let match = cachedInstalledApps.first(where: { $0.normalizedName.contains(normalizedQuery) || normalizedQuery.contains($0.normalizedName) }) { + launchApp(at: match.url, name: match.name) return true } return false } + private func launchApp(at url: URL, name: String) { + let ws = NSWorkspace.shared + if let running = ws.runningApplications.first(where: { $0.bundleURL == url }) { + running.activate(options: [.activateIgnoringOtherApps]) + } else { + let conf = NSWorkspace.OpenConfiguration() + conf.activates = true + ws.openApplication(at: url, configuration: conf, completionHandler: nil) + } + HistoryManager.shared.addEntry("⚡️ [Command] Open \(name)") + } + + private func normalizeForMatching(_ text: String) -> String { + return text.lowercased() + .components(separatedBy: CharacterSet.alphanumerics.inverted) + .joined() + } + + private func refreshInstalledApps() { + let now = Date().timeIntervalSince1970 + guard (now - lastAppsScanTime) > 60 || cachedInstalledApps.isEmpty else { return } + lastAppsScanTime = now + + var apps: [InstalledAppInfo] = [] + let searchDirs = [ + URL(fileURLWithPath: "/Applications"), + URL(fileURLWithPath: "/Applications/Utilities"), + URL(fileURLWithPath: "/System/Applications"), + URL(fileURLWithPath: "/System/Applications/Utilities"), + FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("Applications") + ] + + let fm = FileManager.default + for dir in searchDirs { + guard let contents = try? fm.contentsOfDirectory(at: dir, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles]) else { continue } + for item in contents where item.pathExtension == "app" { + let name = item.deletingPathExtension().lastPathComponent + let normalized = normalizeForMatching(name) + apps.append(InstalledAppInfo(name: name, normalizedName: normalized, url: item)) + } + } + cachedInstalledApps = apps + } + public func switchToTab(number: Int) { let keyCodes: [Int: CGKeyCode] = [ 1: 18, 2: 19, 3: 20, 4: 21, 5: 23, @@ -432,4 +538,12 @@ public final class VoiceCommandExecutor { } } } + + private func playWarningSound() { + if SonioxSettings.shared.playSounds { + if !(NSSound(named: NSSound.Name("Basso"))?.play() ?? false) { + NSSound(named: NSSound.Name("Pop"))?.play() + } + } + } }