4 changed files with 470 additions and 5 deletions
-
14src/AppDelegate.swift
-
10src/SonioxSettings.swift
-
10src/StatusBarController.swift
-
435src/VoiceCommandExecutor.swift
@ -0,0 +1,435 @@ |
|||
import Cocoa |
|||
import Carbon |
|||
|
|||
public final class VoiceCommandExecutor { |
|||
public static let shared = VoiceCommandExecutor() |
|||
|
|||
private init() {} |
|||
|
|||
public func isSlashCommand(text: String) -> Bool { |
|||
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() |
|||
return trimmed.hasPrefix("اسلش") || |
|||
trimmed.hasPrefix("slash") || |
|||
trimmed.hasPrefix("/") || |
|||
trimmed.hasPrefix("\\") |
|||
} |
|||
|
|||
@discardableResult |
|||
public func execute(rawText: String) -> Bool { |
|||
let trimmed = rawText.trimmingCharacters(in: .whitespacesAndNewlines) |
|||
guard isSlashCommand(text: trimmed) else { return false } |
|||
|
|||
// Strip trigger prefix |
|||
var body = trimmed |
|||
if body.lowercased().hasPrefix("اسلش") { |
|||
body = String(body.dropFirst(4)).trimmingCharacters(in: .whitespacesAndNewlines) |
|||
} else if body.lowercased().hasPrefix("slash") { |
|||
body = String(body.dropFirst(5)).trimmingCharacters(in: .whitespacesAndNewlines) |
|||
} else if body.hasPrefix("/") || body.hasPrefix("\\") { |
|||
body = String(body.dropFirst(1)).trimmingCharacters(in: .whitespacesAndNewlines) |
|||
} |
|||
|
|||
body = body.trimmingCharacters(in: CharacterSet(charactersIn: ":,،.؛ -_")) |
|||
let lower = body.lowercased() |
|||
|
|||
print("VoiceCommandExecutor: executing command for body: '\(body)'") |
|||
|
|||
// 1. App with Tab combinations (e.g. "فایرفاکس تب ۱", "کروم تب ۲") |
|||
if handleAppWithTab(lower: lower, original: body) { |
|||
playConfirmationSound() |
|||
return true |
|||
} |
|||
|
|||
// 2. Standalone Tab commands (e.g. "تب ۱", "تب جدید", "بستن تب") |
|||
if handleStandaloneTab(lower: lower) { |
|||
playConfirmationSound() |
|||
return true |
|||
} |
|||
|
|||
// 3. System controls (volume, 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) { |
|||
playConfirmationSound() |
|||
return true |
|||
} |
|||
|
|||
// 5. Generic application launch fallback |
|||
if handleGenericApp(original: body) { |
|||
playConfirmationSound() |
|||
return true |
|||
} |
|||
|
|||
return false |
|||
} |
|||
|
|||
private func handleAppWithTab(lower: String, original: String) -> Bool { |
|||
var targetApp: String? = nil |
|||
if lower.contains("فایرفاکس") || lower.contains("firefox") { |
|||
targetApp = "Firefox" |
|||
} else if lower.contains("کروم") || lower.contains("chrome") { |
|||
targetApp = "Google Chrome" |
|||
} else if lower.contains("سافاری") || lower.contains("safari") { |
|||
targetApp = "Safari" |
|||
} |
|||
|
|||
guard let appName = targetApp else { return false } |
|||
|
|||
if let tabNum = extractTabNumber(from: lower) { |
|||
activateApp(named: appName) { |
|||
self.switchToTab(number: tabNum) |
|||
} |
|||
HistoryManager.shared.addEntry("⚡️ [Command] \(appName): Tab \(tabNum)") |
|||
return true |
|||
} else if 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") { |
|||
activateApp(named: appName) { |
|||
self.pressHotkey(keyCode: 13, modifiers: .maskCommand) // Cmd+W |
|||
} |
|||
HistoryManager.shared.addEntry("⚡️ [Command] \(appName): Close Tab") |
|||
return true |
|||
} |
|||
|
|||
return false |
|||
} |
|||
|
|||
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") { |
|||
return 1 |
|||
} |
|||
if t.contains("تب ۲") || t.contains("تب 2") || t.contains("تب دو") || t.contains("تب دوم") || t.contains("tab 2") || t.contains("tab two") { |
|||
return 2 |
|||
} |
|||
if t.contains("تب ۳") || t.contains("تب 3") || t.contains("تب سه") || t.contains("تب سوم") || t.contains("tab 3") || t.contains("tab three") { |
|||
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("tab 9") || t.contains("tab last") { |
|||
return 9 |
|||
} |
|||
return nil |
|||
} |
|||
|
|||
private func handleStandaloneTab(lower: String) -> Bool { |
|||
if let num = extractTabNumber(from: lower) { |
|||
switchToTab(number: num) |
|||
HistoryManager.shared.addEntry("⚡️ [Command] Switch to Tab \(num)") |
|||
return true |
|||
} |
|||
if lower.contains("تب جدید") || lower.contains("نیو تب") || lower.contains("new tab") { |
|||
pressHotkey(keyCode: 17, modifiers: .maskCommand) // Cmd+T |
|||
HistoryManager.shared.addEntry("⚡️ [Command] New Tab") |
|||
return true |
|||
} |
|||
if lower.contains("بستن تب") || lower.contains("کلوز تب") || lower.contains("close tab") { |
|||
pressHotkey(keyCode: 13, modifiers: .maskCommand) // Cmd+W |
|||
HistoryManager.shared.addEntry("⚡️ [Command] Close Tab") |
|||
return true |
|||
} |
|||
if lower.contains("تب بعد") || lower.contains("تب بعدی") || lower.contains("next tab") { |
|||
pressHotkey(keyCode: 48, modifiers: .maskControl) // Ctrl+Tab |
|||
HistoryManager.shared.addEntry("⚡️ [Command] Next Tab") |
|||
return true |
|||
} |
|||
if lower.contains("تب قبل") || lower.contains("تب قبلی") || lower.contains("prev tab") || lower.contains("previous tab") { |
|||
pressHotkey(keyCode: 48, modifiers: [.maskControl, .maskShift]) // Ctrl+Shift+Tab |
|||
HistoryManager.shared.addEntry("⚡️ [Command] Previous Tab") |
|||
return true |
|||
} |
|||
if lower.contains("رفرش") || lower.contains("ریلود") || lower.contains("reload") || lower.contains("refresh") { |
|||
pressHotkey(keyCode: 15, modifiers: .maskCommand) // Cmd+R |
|||
HistoryManager.shared.addEntry("⚡️ [Command] Refresh Page") |
|||
return true |
|||
} |
|||
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") { |
|||
runAppleScript("set volume output volume ((output volume of (get volume settings)) + 15)") |
|||
HistoryManager.shared.addEntry("⚡️ [Command] Volume Up") |
|||
return true |
|||
} |
|||
if lower.contains("صدا کم") || lower.contains("کاهش صدا") || lower.contains("volume down") { |
|||
runAppleScript("set volume output volume ((output volume of (get volume settings)) - 15)") |
|||
HistoryManager.shared.addEntry("⚡️ [Command] Volume Down") |
|||
return true |
|||
} |
|||
if lower.contains("میوت") || lower.contains("بی صدا") || lower.contains("بیصدا") || lower.contains("mute") { |
|||
runAppleScript("set volume output muted not (output muted of (get volume settings))") |
|||
HistoryManager.shared.addEntry("⚡️ [Command] Toggle Mute") |
|||
return true |
|||
} |
|||
|
|||
// Lock screen |
|||
if lower.contains("قفل") || lower.contains("لاک") || lower.contains("lock screen") || lower.contains("lock") { |
|||
let task = Process() |
|||
task.executableURL = URL(fileURLWithPath: "/usr/bin/pmset") |
|||
task.arguments = ["displaysleepnow"] |
|||
try? task.run() |
|||
HistoryManager.shared.addEntry("⚡️ [Command] Lock Screen") |
|||
return true |
|||
} |
|||
|
|||
// Screenshot |
|||
if lower.contains("اسکرین شات") || lower.contains("عکس صفحه") || lower.contains("screenshot") { |
|||
let task = Process() |
|||
task.executableURL = URL(fileURLWithPath: "/usr/sbin/screencapture") |
|||
task.arguments = ["-i", "-c"] |
|||
try? task.run() |
|||
HistoryManager.shared.addEntry("⚡️ [Command] Interactive Screenshot") |
|||
return true |
|||
} |
|||
|
|||
// Dark Mode toggle |
|||
if 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 |
|||
if lower == "کپی" || lower == "copy" { |
|||
pressHotkey(keyCode: 8, modifiers: .maskCommand) // Cmd+C |
|||
return true |
|||
} |
|||
if lower == "پیست" || lower == "paste" { |
|||
pressHotkey(keyCode: 9, modifiers: .maskCommand) // Cmd+V |
|||
return true |
|||
} |
|||
if lower == "کات" || lower == "cut" { |
|||
pressHotkey(keyCode: 7, modifiers: .maskCommand) // Cmd+X |
|||
return true |
|||
} |
|||
if lower.contains("انتخاب همه") || lower.contains("سلکت آل") || lower == "select all" { |
|||
pressHotkey(keyCode: 0, modifiers: .maskCommand) // Cmd+A |
|||
return true |
|||
} |
|||
if lower == "آندو" || lower == "برگرد" || lower == "undo" { |
|||
pressHotkey(keyCode: 6, modifiers: .maskCommand) // Cmd+Z |
|||
return true |
|||
} |
|||
if lower == "اینتر" || lower == "enter" { |
|||
pressHotkey(keyCode: 36, modifiers: []) // Return |
|||
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) |
|||
} |
|||
|
|||
guard !appCandidate.isEmpty else { 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)") |
|||
return true |
|||
} |
|||
|
|||
return false |
|||
} |
|||
|
|||
public func switchToTab(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 } |
|||
pressHotkey(keyCode: code, modifiers: .maskCommand) |
|||
} |
|||
|
|||
private func pressHotkey(keyCode: CGKeyCode, modifiers: CGEventFlags) { |
|||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.08) { |
|||
let source = CGEventSource(stateID: .combinedSessionState) |
|||
if let down = CGEvent(keyboardEventSource: source, virtualKey: keyCode, keyDown: true), |
|||
let up = CGEvent(keyboardEventSource: source, virtualKey: keyCode, keyDown: false) { |
|||
down.flags = modifiers |
|||
up.flags = modifiers |
|||
down.post(tap: .cghidEventTap) |
|||
up.post(tap: .cghidEventTap) |
|||
} |
|||
} |
|||
} |
|||
|
|||
private func launchOrActivateApp(named appName: String) { |
|||
let ws = NSWorkspace.shared |
|||
if let running = ws.runningApplications.first(where: { |
|||
$0.localizedName?.localizedCaseInsensitiveContains(appName) == true |
|||
}) { |
|||
running.activate(options: [.activateIgnoringOtherApps]) |
|||
return |
|||
} |
|||
|
|||
let task = Process() |
|||
task.executableURL = URL(fileURLWithPath: "/usr/bin/open") |
|||
task.arguments = ["-a", appName] |
|||
try? task.run() |
|||
} |
|||
|
|||
private func activateApp(named appName: String, completion: @escaping () -> Void) { |
|||
launchOrActivateApp(named: appName) |
|||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.22) { |
|||
completion() |
|||
} |
|||
} |
|||
|
|||
private func runAppleScript(_ script: String) { |
|||
let full = "osascript -e '" + script.replacingOccurrences(of: "'", with: "'\\''") + "'" |
|||
let task = Process() |
|||
task.executableURL = URL(fileURLWithPath: "/bin/sh") |
|||
task.arguments = ["-c", full] |
|||
try? task.run() |
|||
} |
|||
|
|||
private func playConfirmationSound() { |
|||
if SonioxSettings.shared.playSounds { |
|||
if !(NSSound(named: NSSound.Name("Hero"))?.play() ?? false) { |
|||
NSSound(named: NSSound.Name("Glass"))?.play() |
|||
} |
|||
} |
|||
} |
|||
} |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue