Browse Source

fix: strictly suppress input-box typing on slash commands and add universal bilingual app matching

main
Ali Alavi 15 hours ago
parent
commit
73987c9aaf
  1. 7
      src/AppDelegate.swift
  2. 430
      src/VoiceCommandExecutor.swift

7
src/AppDelegate.swift

@ -124,11 +124,8 @@ public final class AppDelegate: NSObject, NSApplicationDelegate {
// 1. Check for Executive Voice Slash Commands // 1. Check for Executive Voice Slash Commands
if SonioxSettings.shared.voiceCommands && VoiceCommandExecutor.shared.isSlashCommand(text: trimmed) { 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 // 2. Normal dictation processing & paste

430
src/VoiceCommandExecutor.swift

@ -1,73 +1,182 @@
import Cocoa import Cocoa
import Carbon import Carbon
public struct InstalledAppInfo {
public let name: String
public let normalizedName: String
public let url: URL
}
public final class VoiceCommandExecutor { public final class VoiceCommandExecutor {
public static let shared = 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 { public func isSlashCommand(text: String) -> Bool {
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() 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) 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) 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("\\") { } else if body.hasPrefix("/") || body.hasPrefix("\\") {
body = String(body.dropFirst(1)).trimmingCharacters(in: .whitespacesAndNewlines) 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() let lower = body.lowercased()
print("VoiceCommandExecutor: executing command for body: '\(body)'") 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) { if handleAppWithTab(lower: lower, original: body) {
playConfirmationSound() playConfirmationSound()
return true return true
} }
// 2. Standalone Tab commands (e.g. "تب ۱", "تب جدید", "بستن تب")
// 2. Standalone Tab & Navigation commands (e.g. "تب ۱", "تب جدید", "بستن تب", "tab 1", "new tab")
if handleStandaloneTab(lower: lower) { if handleStandaloneTab(lower: lower) {
playConfirmationSound() playConfirmationSound()
return true 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) { if handleSystemControls(lower: lower) {
playConfirmationSound() playConfirmationSound()
return true 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() playConfirmationSound()
return true 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() playConfirmationSound()
return true 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 return false
} }
private func handleAppWithTab(lower: String, original: String) -> Bool { private func handleAppWithTab(lower: String, original: String) -> Bool {
guard lower.contains("تب") || lower.contains("tab") else { return false }
var targetApp: String? = nil var targetApp: String? = nil
if lower.contains("فایرفاکس") || lower.contains("firefox") { if lower.contains("فایرفاکس") || lower.contains("firefox") {
targetApp = "Firefox" targetApp = "Firefox"
@ -75,6 +184,10 @@ public final class VoiceCommandExecutor {
targetApp = "Google Chrome" targetApp = "Google Chrome"
} else if lower.contains("سافاری") || lower.contains("safari") { } else if lower.contains("سافاری") || lower.contains("safari") {
targetApp = "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 } guard let appName = targetApp else { return false }
@ -85,13 +198,13 @@ public final class VoiceCommandExecutor {
} }
HistoryManager.shared.addEntry("⚡️ [Command] \(appName): Tab \(tabNum)") HistoryManager.shared.addEntry("⚡️ [Command] \(appName): Tab \(tabNum)")
return true return true
} else if lower.contains("تب جدید") || lower.contains("new tab") {
} else if lower.contains("تب جدید") || lower.contains("نیو تب") || lower.contains("new tab") {
activateApp(named: appName) { activateApp(named: appName) {
self.pressHotkey(keyCode: 17, modifiers: .maskCommand) // Cmd+T self.pressHotkey(keyCode: 17, modifiers: .maskCommand) // Cmd+T
} }
HistoryManager.shared.addEntry("⚡️ [Command] \(appName): New Tab") HistoryManager.shared.addEntry("⚡️ [Command] \(appName): New Tab")
return true return true
} else if lower.contains("بستن تب") || lower.contains("close tab") {
} else if lower.contains("بستن تب") || lower.contains("کلوز تب") || lower.contains("close tab") {
activateApp(named: appName) { activateApp(named: appName) {
self.pressHotkey(keyCode: 13, modifiers: .maskCommand) // Cmd+W self.pressHotkey(keyCode: 13, modifiers: .maskCommand) // Cmd+W
} }
@ -104,31 +217,49 @@ public final class VoiceCommandExecutor {
private func extractTabNumber(from text: String) -> Int? { private func extractTabNumber(from text: String) -> Int? {
let t = text.lowercased() 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 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 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 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 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 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 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 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 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 9
} }
return nil return nil
@ -168,115 +299,6 @@ public final class VoiceCommandExecutor {
return false 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 { private func handleSystemControls(lower: String) -> Bool {
// Volume controls // Volume controls
if lower.contains("صدا زیاد") || lower.contains("افزایش صدا") || lower.contains("volume up") { if lower.contains("صدا زیاد") || lower.contains("افزایش صدا") || lower.contains("volume up") {
@ -296,7 +318,7 @@ public final class VoiceCommandExecutor {
} }
// Lock screen // 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() let task = Process()
task.executableURL = URL(fileURLWithPath: "/usr/bin/pmset") task.executableURL = URL(fileURLWithPath: "/usr/bin/pmset")
task.arguments = ["displaysleepnow"] task.arguments = ["displaysleepnow"]
@ -316,63 +338,147 @@ public final class VoiceCommandExecutor {
} }
// Dark Mode toggle // 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") runAppleScript("tell application \"System Events\" to tell appearance preferences to set dark mode to not dark mode")
HistoryManager.shared.addEntry("⚡️ [Command] Toggle Dark Mode") HistoryManager.shared.addEntry("⚡️ [Command] Toggle Dark Mode")
return true return true
} }
// Editing controls
// Clipboard & Editing hotkeys
if lower == "کپی" || lower == "copy" { if lower == "کپی" || lower == "copy" {
pressHotkey(keyCode: 8, modifiers: .maskCommand) // Cmd+C pressHotkey(keyCode: 8, modifiers: .maskCommand) // Cmd+C
HistoryManager.shared.addEntry("⚡️ [Command] Copy")
return true return true
} }
if lower == "پیست" || lower == "paste" { if lower == "پیست" || lower == "paste" {
pressHotkey(keyCode: 9, modifiers: .maskCommand) // Cmd+V pressHotkey(keyCode: 9, modifiers: .maskCommand) // Cmd+V
HistoryManager.shared.addEntry("⚡️ [Command] Paste")
return true return true
} }
if lower == "کات" || lower == "cut" { if lower == "کات" || lower == "cut" {
pressHotkey(keyCode: 7, modifiers: .maskCommand) // Cmd+X pressHotkey(keyCode: 7, modifiers: .maskCommand) // Cmd+X
HistoryManager.shared.addEntry("⚡️ [Command] Cut")
return true return true
} }
if lower.contains("انتخاب همه") || lower.contains("سلکت آل") || lower == "select all" { if lower.contains("انتخاب همه") || lower.contains("سلکت آل") || lower == "select all" {
pressHotkey(keyCode: 0, modifiers: .maskCommand) // Cmd+A pressHotkey(keyCode: 0, modifiers: .maskCommand) // Cmd+A
HistoryManager.shared.addEntry("⚡️ [Command] Select All")
return true return true
} }
if lower == "آندو" || lower == "برگرد" || lower == "undo" { if lower == "آندو" || lower == "برگرد" || lower == "undo" {
pressHotkey(keyCode: 6, modifiers: .maskCommand) // Cmd+Z pressHotkey(keyCode: 6, modifiers: .maskCommand) // Cmd+Z
HistoryManager.shared.addEntry("⚡️ [Command] Undo")
return true return true
} }
if lower == "اینتر" || lower == "enter" { if lower == "اینتر" || lower == "enter" {
pressHotkey(keyCode: 36, modifiers: []) // Return
pressHotkey(keyCode: 36, modifiers: []) // Return key
HistoryManager.shared.addEntry("⚡️ [Command] Enter")
return true return true
} }
return false 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 true
} }
return false 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) { public func switchToTab(number: Int) {
let keyCodes: [Int: CGKeyCode] = [ let keyCodes: [Int: CGKeyCode] = [
1: 18, 2: 19, 3: 20, 4: 21, 5: 23, 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()
}
}
}
} }
Loading…
Cancel
Save