Browse Source

feat: direct app name + number switching for Telegram accounts and browser/IDE tabs

main
Ali Alavi 12 hours ago
parent
commit
6e713173d0
  1. 19
      src/StatusBarController.swift
  2. 208
      src/VoiceCommandExecutor.swift

19
src/StatusBarController.swift

@ -447,14 +447,17 @@ public final class StatusBarController: NSObject, NSMenuDelegate {
/hide OR /هاید -> Hide frontmost app
/quit OR /ببند -> Quit frontmost app
[Browser & Tabs]
/firefox tab 1 OR /فایرفاکس تب ۱ -> Jump to Firefox Tab 1
/tab 1..9 OR /تب ۱..۹ -> Jump to Tab 1..9 in active app
/new tab OR /تب جدید -> Open new tab (Cmd+T)
/close tab OR /بستن تب -> Close active tab (Cmd+W)
/reopen tab OR /ری اپن تب -> Reopen closed tab (Cmd+Shift+T)
/next tab / /prev tab -> Switch tabs
/reload OR /رفرش -> Reload web page (Cmd+R)
[Browser & App Tabs / Accounts]
/telegram 1..3 OR /تلگرام ۱..۳ -> Open Telegram & Switch to Account 1..3
/firefox 1..9 OR /فایرفاکس ۱..۹ -> Open Firefox & Switch to Tab 1..9
/chrome 1..9 OR /کروم ۱..۹ -> Open Chrome & Switch to Tab 1..9
/safari 1..9 OR /سافاری ۱..۹ -> Open Safari & Switch to Tab 1..9
/tab 1..9 OR /تب ۱..۹ -> Jump to Tab 1..9 in active app
/new tab OR /تب جدید -> Open new tab (Cmd+T)
/close tab OR /بستن تب -> Close active tab (Cmd+W)
/reopen tab OR /ری اپن تب -> Reopen closed tab (Cmd+Shift+T)
/next tab / /prev tab -> Switch tabs
/reload OR /رفرش -> Reload web page (Cmd+R)
/search [query] OR /سرچ [موضوع] -> Search on Google
/youtube [query] OR /یوتیوب [موضوع] -> Search on YouTube

208
src/VoiceCommandExecutor.swift

@ -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) {
playConfirmationSound()
VoiceCommandLogger.shared.logCommand(
@ -314,7 +298,7 @@ public final class VoiceCommandExecutor {
return true
}
// 4. Window Tiling & Management (چپ, راست, بالا, پایین, تمام صفحه, وسط, مینیمایز, هاید, بستن)
// 3. Window Tiling & Management (چپ, راست, بالا, پایین, تمام صفحه, وسط, مینیمایز, هاید, بستن)
if let tileDetail = handleWindowTiling(normalized: normalized) {
playConfirmationSound()
VoiceCommandLogger.shared.logCommand(
@ -328,21 +312,21 @@ public final class VoiceCommandExecutor {
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) {
playConfirmationSound()
VoiceCommandLogger.shared.logCommand(
rawSpeech: rawText,
body: body,
normalized: normalized,
actionCategory: "Browser App Tab",
actionCategory: "App Account / Tab Switch",
actionDetail: appTabDetail,
status: "SUCCESS"
)
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) {
playConfirmationSound()
VoiceCommandLogger.shared.logCommand(
@ -356,7 +340,7 @@ public final class VoiceCommandExecutor {
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) {
playConfirmationSound()
VoiceCommandLogger.shared.logCommand(
@ -370,6 +354,22 @@ public final class VoiceCommandExecutor {
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
if let appName = matchKnownApp(normalized: normalized) {
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? {
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
if normalized.contains("فایرفاکس") || normalized.contains("firefox") {
targetApp = "Firefox"
@ -624,19 +715,15 @@ public final class VoiceCommandExecutor {
targetApp = "Google Chrome"
} else if normalized.contains("سافاری") || normalized.contains("safari") {
targetApp = "Safari"
} else if normalized.contains("تلگرام") || normalized.contains("telegram") {
targetApp = "Telegram"
} else if let anti = matchAntigravity(normalized: normalized) {
targetApp = anti
}
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) {
self.pressHotkey(keyCode: 17, modifiers: .maskCommand) // Cmd+T
}
@ -659,53 +746,28 @@ public final class VoiceCommandExecutor {
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
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") {
pressHotkey(keyCode: 17, modifiers: .maskCommand) // Cmd+T

Loading…
Cancel
Save