|
|
@ -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("اسلش") || |
|
|
|
|
|
|
|
|
if trimmed.hasPrefix("اسلش") || |
|
|
|
|
|
trimmed.hasPrefix("اصلش") || |
|
|
trimmed.hasPrefix("slash") || |
|
|
trimmed.hasPrefix("slash") || |
|
|
|
|
|
trimmed.hasPrefix("slsh") || |
|
|
trimmed.hasPrefix("/") || |
|
|
trimmed.hasPrefix("/") || |
|
|
trimmed.hasPrefix("\\") |
|
|
|
|
|
|
|
|
trimmed.hasPrefix("\\") { |
|
|
|
|
|
return true |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@discardableResult |
|
|
|
|
|
public func execute(rawText: String) -> Bool { |
|
|
|
|
|
let trimmed = rawText.trimmingCharacters(in: .whitespacesAndNewlines) |
|
|
|
|
|
guard isSlashCommand(text: trimmed) else { return false } |
|
|
|
|
|
|
|
|
let firstWord = trimmed.components(separatedBy: .whitespacesAndNewlines).first ?? "" |
|
|
|
|
|
let cleanFirst = firstWord.trimmingCharacters(in: CharacterSet(charactersIn: ":,،.؛-_/\\")) |
|
|
|
|
|
return cleanFirst == "اسلش" || cleanFirst == "اصلش" || cleanFirst == "slash" || cleanFirst == "slsh" |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
// Strip trigger prefix |
|
|
|
|
|
var body = trimmed |
|
|
|
|
|
if body.lowercased().hasPrefix("اسلش") { |
|
|
|
|
|
|
|
|
public func extractCommandBody(rawText: String) -> String { |
|
|
|
|
|
var body = rawText.trimmingCharacters(in: .whitespacesAndNewlines) |
|
|
|
|
|
let lower = body.lowercased() |
|
|
|
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return body.trimmingCharacters(in: CharacterSet(charactersIn: ":,،.؛ -_")) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
body = 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 |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 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 |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
guard !appCandidate.isEmpty else { return false } |
|
|
|
|
|
|
|
|
return false |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
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)") |
|
|
|
|
|
|
|
|
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() |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
} |
|
|
} |