|
|
@ -284,23 +284,7 @@ public final class VoiceCommandExecutor { |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// 2. Specialized Antigravity Matcher (Instant detection of any variation) |
|
|
|
|
|
if let antiMatch = matchAntigravity(normalized: normalized) { |
|
|
|
|
|
launchOrActivateApp(named: antiMatch) |
|
|
|
|
|
playConfirmationSound() |
|
|
|
|
|
HistoryManager.shared.addEntry("⚡️ [App] Open \(antiMatch)") |
|
|
|
|
|
VoiceCommandLogger.shared.logCommand( |
|
|
|
|
|
rawSpeech: rawText, |
|
|
|
|
|
body: body, |
|
|
|
|
|
normalized: normalized, |
|
|
|
|
|
actionCategory: "Application Launch", |
|
|
|
|
|
actionDetail: "Launched / Focused: \(antiMatch)", |
|
|
|
|
|
status: "SUCCESS" |
|
|
|
|
|
) |
|
|
|
|
|
return true |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 3. Web Search & Direct Navigation ("سرچ X", "search X", "یوتیوب X", "youtube X", "برو به X") |
|
|
|
|
|
|
|
|
// 2. Web Search & Direct Navigation ("سرچ X", "search X", "یوتیوب X", "youtube X", "برو به X") |
|
|
if let searchDetail = handleWebSearchAndNavigation(normalized: normalized, original: body) { |
|
|
if let searchDetail = handleWebSearchAndNavigation(normalized: normalized, original: body) { |
|
|
playConfirmationSound() |
|
|
playConfirmationSound() |
|
|
VoiceCommandLogger.shared.logCommand( |
|
|
VoiceCommandLogger.shared.logCommand( |
|
|
@ -314,7 +298,7 @@ public final class VoiceCommandExecutor { |
|
|
return true |
|
|
return true |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// 4. Window Tiling & Management (چپ, راست, بالا, پایین, تمام صفحه, وسط, مینیمایز, هاید, بستن) |
|
|
|
|
|
|
|
|
// 3. Window Tiling & Management (چپ, راست, بالا, پایین, تمام صفحه, وسط, مینیمایز, هاید, بستن) |
|
|
if let tileDetail = handleWindowTiling(normalized: normalized) { |
|
|
if let tileDetail = handleWindowTiling(normalized: normalized) { |
|
|
playConfirmationSound() |
|
|
playConfirmationSound() |
|
|
VoiceCommandLogger.shared.logCommand( |
|
|
VoiceCommandLogger.shared.logCommand( |
|
|
@ -328,21 +312,21 @@ public final class VoiceCommandExecutor { |
|
|
return true |
|
|
return true |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// 5. App + Tab combinations (e.g. "فایرفاکس تب ۱", "کروم تب ۲", "firefox tab 1") |
|
|
|
|
|
|
|
|
// 4. App + Tab/Account combinations (e.g. "تلگرام ۱", "فایرفاکس ۲", "کروم ۳", "سافاری ۱", "telegram 1", "firefox 2") |
|
|
if let appTabDetail = handleAppWithTab(normalized: normalized) { |
|
|
if let appTabDetail = handleAppWithTab(normalized: normalized) { |
|
|
playConfirmationSound() |
|
|
playConfirmationSound() |
|
|
VoiceCommandLogger.shared.logCommand( |
|
|
VoiceCommandLogger.shared.logCommand( |
|
|
rawSpeech: rawText, |
|
|
rawSpeech: rawText, |
|
|
body: body, |
|
|
body: body, |
|
|
normalized: normalized, |
|
|
normalized: normalized, |
|
|
actionCategory: "Browser App Tab", |
|
|
|
|
|
|
|
|
actionCategory: "App Account / Tab Switch", |
|
|
actionDetail: appTabDetail, |
|
|
actionDetail: appTabDetail, |
|
|
status: "SUCCESS" |
|
|
status: "SUCCESS" |
|
|
) |
|
|
) |
|
|
return true |
|
|
return true |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// 6. Standalone Tab & Navigation commands (e.g. "تب ۱", "تب جدید", "بستن تب", "tab 1", "new tab") |
|
|
|
|
|
|
|
|
// 5. Standalone Tab & Navigation commands (e.g. "تب ۱", "تب جدید", "بستن تب", "tab 1", "new tab") |
|
|
if let tabDetail = handleStandaloneTab(normalized: normalized) { |
|
|
if let tabDetail = handleStandaloneTab(normalized: normalized) { |
|
|
playConfirmationSound() |
|
|
playConfirmationSound() |
|
|
VoiceCommandLogger.shared.logCommand( |
|
|
VoiceCommandLogger.shared.logCommand( |
|
|
@ -356,7 +340,7 @@ public final class VoiceCommandExecutor { |
|
|
return true |
|
|
return true |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// 7. System controls (volume, mute, lock, screenshot, dark mode, editing hotkeys) |
|
|
|
|
|
|
|
|
// 6. System controls (volume, mute, lock, screenshot, dark mode, editing hotkeys) |
|
|
if let sysDetail = handleSystemControls(normalized: normalized) { |
|
|
if let sysDetail = handleSystemControls(normalized: normalized) { |
|
|
playConfirmationSound() |
|
|
playConfirmationSound() |
|
|
VoiceCommandLogger.shared.logCommand( |
|
|
VoiceCommandLogger.shared.logCommand( |
|
|
@ -370,6 +354,22 @@ public final class VoiceCommandExecutor { |
|
|
return true |
|
|
return true |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 7. Specialized Antigravity Matcher (Instant detection of standalone variation) |
|
|
|
|
|
if let antiMatch = matchAntigravity(normalized: normalized) { |
|
|
|
|
|
launchOrActivateApp(named: antiMatch) |
|
|
|
|
|
playConfirmationSound() |
|
|
|
|
|
HistoryManager.shared.addEntry("⚡️ [App] Open \(antiMatch)") |
|
|
|
|
|
VoiceCommandLogger.shared.logCommand( |
|
|
|
|
|
rawSpeech: rawText, |
|
|
|
|
|
body: body, |
|
|
|
|
|
normalized: normalized, |
|
|
|
|
|
actionCategory: "Application Launch", |
|
|
|
|
|
actionDetail: "Launched / Focused: \(antiMatch)", |
|
|
|
|
|
status: "SUCCESS" |
|
|
|
|
|
) |
|
|
|
|
|
return true |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
// 8. Match App from Persian dictionary or exact names |
|
|
// 8. Match App from Persian dictionary or exact names |
|
|
if let appName = matchKnownApp(normalized: normalized) { |
|
|
if let appName = matchKnownApp(normalized: normalized) { |
|
|
launchOrActivateApp(named: appName) |
|
|
launchOrActivateApp(named: appName) |
|
|
@ -613,10 +613,101 @@ public final class VoiceCommandExecutor { |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// MARK: - App + Tab Combinations |
|
|
|
|
|
|
|
|
// MARK: - App + Tab/Account Combinations |
|
|
|
|
|
private func extractTrailingOrEmbeddedNumber(from text: String) -> Int? { |
|
|
|
|
|
let t = text.lowercased() |
|
|
|
|
|
let words = t.components(separatedBy: .whitespacesAndNewlines) |
|
|
|
|
|
|
|
|
|
|
|
// 1. Direct digits (English and Persian) |
|
|
|
|
|
for i in 1...9 { |
|
|
|
|
|
let enDig = "\(i)" |
|
|
|
|
|
let faDigs = ["۱", "۲", "۳", "۴", "۵", "۶", "۷", "۸", "۹"] |
|
|
|
|
|
let faDig = faDigs[i - 1] |
|
|
|
|
|
|
|
|
|
|
|
for w in words { |
|
|
|
|
|
let cleanW = w.trimmingCharacters(in: CharacterSet(charactersIn: ".,،؛:!؟?-_")) |
|
|
|
|
|
if cleanW == enDig || cleanW == faDig { |
|
|
|
|
|
return i |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 2. Word numbers (Persian & English cardinal and ordinal) |
|
|
|
|
|
let numberWords: [(names: [String], num: Int)] = [ |
|
|
|
|
|
(["یک", "اول", "نخست", "one", "first"], 1), |
|
|
|
|
|
(["دو", "دوم", "two", "second"], 2), |
|
|
|
|
|
(["سه", "سوم", "three", "third"], 3), |
|
|
|
|
|
(["چهار", "چهارم", "four", "fourth"], 4), |
|
|
|
|
|
(["پنج", "پنجم", "five", "fifth"], 5), |
|
|
|
|
|
(["شش", "ششم", "six", "sixth"], 6), |
|
|
|
|
|
(["هفت", "هفتم", "seven", "seventh"], 7), |
|
|
|
|
|
(["هشت", "هشتم", "eight", "eighth"], 8), |
|
|
|
|
|
(["نه", "نهم", "اخر", "آخر", "nine", "ninth", "last"], 9) |
|
|
|
|
|
] |
|
|
|
|
|
|
|
|
|
|
|
for w in words { |
|
|
|
|
|
let cleanW = w.trimmingCharacters(in: CharacterSet(charactersIn: ".,،؛:!؟?-_")) |
|
|
|
|
|
for item in numberWords { |
|
|
|
|
|
if item.names.contains(cleanW) { |
|
|
|
|
|
return item.num |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return nil |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private func parseAppAndNumber(normalized: String) -> (appName: String, number: Int, type: String)? { |
|
|
|
|
|
var targetApp: String? = nil |
|
|
|
|
|
var isTelegram = false |
|
|
|
|
|
|
|
|
|
|
|
if normalized.contains("تلگرام") || normalized.contains("telegram") { |
|
|
|
|
|
targetApp = "Telegram" |
|
|
|
|
|
isTelegram = true |
|
|
|
|
|
} else if normalized.contains("فایرفاکس") || normalized.contains("firefox") || normalized.contains("فایر فاکس") || normalized.contains("فایرفکس") { |
|
|
|
|
|
targetApp = "Firefox" |
|
|
|
|
|
} else if normalized.contains("کروم") || normalized.contains("chrome") { |
|
|
|
|
|
targetApp = "Google Chrome" |
|
|
|
|
|
} else if normalized.contains("سافاری") || normalized.contains("safari") { |
|
|
|
|
|
targetApp = "Safari" |
|
|
|
|
|
} else if normalized.contains("انتی") && (normalized.contains("گراویتی") || normalized.contains("گرویتی")) { |
|
|
|
|
|
if normalized.contains("ای دی ای") || normalized.contains("ایدی ای") || normalized.contains("ide") { |
|
|
|
|
|
targetApp = "Antigravity IDE" |
|
|
|
|
|
} else { |
|
|
|
|
|
targetApp = "Antigravity" |
|
|
|
|
|
} |
|
|
|
|
|
} else if normalized.contains("وی اس کد") || normalized.contains("vscode") { |
|
|
|
|
|
targetApp = "Visual Studio Code" |
|
|
|
|
|
} else if normalized.contains("ترمینال") || normalized.contains("terminal") { |
|
|
|
|
|
targetApp = "Terminal" |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
guard let app = targetApp else { return nil } |
|
|
|
|
|
guard let num = extractTrailingOrEmbeddedNumber(from: normalized) else { return nil } |
|
|
|
|
|
|
|
|
|
|
|
let type = isTelegram ? "Account" : "Tab" |
|
|
|
|
|
return (app, num, type) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
private func handleAppWithTab(normalized: String) -> String? { |
|
|
private func handleAppWithTab(normalized: String) -> String? { |
|
|
guard normalized.contains("تب") || normalized.contains("tab") else { return nil } |
|
|
|
|
|
|
|
|
// 1. Direct App + Number (e.g. "تلگرام ۱", "فایرفاکس ۲", "کروم ۳", "سافاری ۱", "telegram 1", "firefox 2") |
|
|
|
|
|
if let parsed = parseAppAndNumber(normalized: normalized) { |
|
|
|
|
|
let appName = parsed.appName |
|
|
|
|
|
let num = parsed.number |
|
|
|
|
|
let type = parsed.type |
|
|
|
|
|
|
|
|
|
|
|
activateApp(named: appName) { |
|
|
|
|
|
if appName == "Telegram" { |
|
|
|
|
|
self.switchToAccount(number: num) |
|
|
|
|
|
} else { |
|
|
|
|
|
self.switchToTab(number: num) |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
HistoryManager.shared.addEntry("⚡️ [\(appName)] \(type) \(num)") |
|
|
|
|
|
return "\(appName) -> \(type) \(num)" |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 2. App + Named actions (e.g. "فایرفاکس تب جدید", "کروم بستن تب") |
|
|
var targetApp: String? = nil |
|
|
var targetApp: String? = nil |
|
|
if normalized.contains("فایرفاکس") || normalized.contains("firefox") { |
|
|
if normalized.contains("فایرفاکس") || normalized.contains("firefox") { |
|
|
targetApp = "Firefox" |
|
|
targetApp = "Firefox" |
|
|
@ -624,19 +715,15 @@ public final class VoiceCommandExecutor { |
|
|
targetApp = "Google Chrome" |
|
|
targetApp = "Google Chrome" |
|
|
} else if normalized.contains("سافاری") || normalized.contains("safari") { |
|
|
} else if normalized.contains("سافاری") || normalized.contains("safari") { |
|
|
targetApp = "Safari" |
|
|
targetApp = "Safari" |
|
|
|
|
|
} else if normalized.contains("تلگرام") || normalized.contains("telegram") { |
|
|
|
|
|
targetApp = "Telegram" |
|
|
} else if let anti = matchAntigravity(normalized: normalized) { |
|
|
} else if let anti = matchAntigravity(normalized: normalized) { |
|
|
targetApp = anti |
|
|
targetApp = anti |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
guard let appName = targetApp else { return nil } |
|
|
guard let appName = targetApp else { return nil } |
|
|
|
|
|
|
|
|
if let tabNum = extractTabNumber(from: normalized) { |
|
|
|
|
|
activateApp(named: appName) { |
|
|
|
|
|
self.switchToTab(number: tabNum) |
|
|
|
|
|
} |
|
|
|
|
|
HistoryManager.shared.addEntry("⚡️ [Command] \(appName): Tab \(tabNum)") |
|
|
|
|
|
return "\(appName) -> Tab \(tabNum)" |
|
|
|
|
|
} else if normalized.contains("تب جدید") || normalized.contains("new tab") { |
|
|
|
|
|
|
|
|
if normalized.contains("تب جدید") || normalized.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 |
|
|
} |
|
|
} |
|
|
@ -659,53 +746,28 @@ public final class VoiceCommandExecutor { |
|
|
return nil |
|
|
return nil |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
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") || t.contains("tab first") { |
|
|
|
|
|
return 1 |
|
|
|
|
|
} |
|
|
|
|
|
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") || t.contains("tab third") { |
|
|
|
|
|
return 3 |
|
|
|
|
|
} |
|
|
|
|
|
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") { |
|
|
|
|
|
return 5 |
|
|
|
|
|
} |
|
|
|
|
|
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") { |
|
|
|
|
|
return 7 |
|
|
|
|
|
} |
|
|
|
|
|
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("تب اخر") || |
|
|
|
|
|
t.contains("tab 9") || t.contains("tab nine") || t.contains("tab last") { |
|
|
|
|
|
return 9 |
|
|
|
|
|
|
|
|
public func switchToAccount(number: Int) { |
|
|
|
|
|
let keyCodes: [Int: CGKeyCode] = [ |
|
|
|
|
|
1: 18, 2: 19, 3: 20, 4: 21, 5: 23, |
|
|
|
|
|
6: 22, 7: 26, 8: 28, 9: 25 |
|
|
|
|
|
] |
|
|
|
|
|
guard let code = keyCodes[number] else { return } |
|
|
|
|
|
// For Telegram Desktop: Send Cmd+[number] and Cmd+Shift+[number] (configured via shortcuts-custom.json) |
|
|
|
|
|
pressHotkey(keyCode: code, modifiers: .maskCommand) |
|
|
|
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.12) { |
|
|
|
|
|
self.pressHotkey(keyCode: code, modifiers: [.maskCommand, .maskShift]) |
|
|
} |
|
|
} |
|
|
return nil |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// MARK: - Standalone Tab Commands |
|
|
// MARK: - Standalone Tab Commands |
|
|
private func handleStandaloneTab(normalized: String) -> String? { |
|
|
private func handleStandaloneTab(normalized: String) -> String? { |
|
|
if let num = extractTabNumber(from: normalized) { |
|
|
|
|
|
switchToTab(number: num) |
|
|
|
|
|
HistoryManager.shared.addEntry("⚡️ [Command] Switch to Tab \(num)") |
|
|
|
|
|
return "Switch to Tab \(num) (Cmd+\(num))" |
|
|
|
|
|
|
|
|
if normalized.hasPrefix("تب") || normalized.hasPrefix("tab") || |
|
|
|
|
|
normalized.contains("تب ") || normalized.contains("tab ") { |
|
|
|
|
|
if let num = extractTrailingOrEmbeddedNumber(from: normalized) { |
|
|
|
|
|
switchToTab(number: num) |
|
|
|
|
|
HistoryManager.shared.addEntry("⚡️ [Command] Switch to Tab \(num)") |
|
|
|
|
|
return "Switch to Tab \(num) (Cmd+\(num))" |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
if normalized.contains("تب جدید") || normalized.contains("نیو تب") || normalized.contains("new tab") { |
|
|
if normalized.contains("تب جدید") || normalized.contains("نیو تب") || normalized.contains("new tab") { |
|
|
pressHotkey(keyCode: 17, modifiers: .maskCommand) // Cmd+T |
|
|
pressHotkey(keyCode: 17, modifiers: .maskCommand) // Cmd+T |
|
|
|