From b4d67685f82a74c45bbed60ee356ea8de31ad5ff Mon Sep 17 00:00:00 2001 From: Ali Alavi Date: Tue, 15 Sep 2026 18:01:41 +0000 Subject: [PATCH] feat: executive voice slash commands for instant app launching and tab control --- src/AppDelegate.swift | 20 +- src/SonioxSettings.swift | 10 + src/StatusBarController.swift | 10 + src/VoiceCommandExecutor.swift | 435 +++++++++++++++++++++++++++++++++ 4 files changed, 470 insertions(+), 5 deletions(-) create mode 100644 src/VoiceCommandExecutor.swift diff --git a/src/AppDelegate.swift b/src/AppDelegate.swift index d40ee61..00b9536 100644 --- a/src/AppDelegate.swift +++ b/src/AppDelegate.swift @@ -120,13 +120,23 @@ public final class AppDelegate: NSObject, NSApplicationDelegate { switch result { case .success(let text): let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines) - if !trimmed.isEmpty { - let processed = TextProcessor.shared.process(text: trimmed) - if !processed.isEmpty { - HistoryManager.shared.addEntry(processed) - self.pasteTextToFrontmostApp(text: processed) + guard !trimmed.isEmpty else { return } + + // 1. Check for Executive Voice Slash Commands + 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! } } + + // 2. Normal dictation processing & paste + let processed = TextProcessor.shared.process(text: trimmed) + if !processed.isEmpty { + HistoryManager.shared.addEntry(processed) + self.pasteTextToFrontmostApp(text: processed) + } case .failure(let error): print("Soniox finalize error:", error) } diff --git a/src/SonioxSettings.swift b/src/SonioxSettings.swift index 67764b9..f558d0e 100644 --- a/src/SonioxSettings.swift +++ b/src/SonioxSettings.swift @@ -94,6 +94,16 @@ public final class SonioxSettings { } } + public var voiceCommands: Bool { + get { + if defaults.object(forKey: "SonioxVoiceCommands") == nil { return true } + return defaults.bool(forKey: "SonioxVoiceCommands") + } + set { + defaults.set(newValue, forKey: "SonioxVoiceCommands") + } + } + public var domainVocabulary: Bool { get { if defaults.object(forKey: "SonioxDomainVocabulary") == nil { return true } diff --git a/src/StatusBarController.swift b/src/StatusBarController.swift index d0d455f..c8d68f9 100644 --- a/src/StatusBarController.swift +++ b/src/StatusBarController.swift @@ -164,6 +164,11 @@ public final class StatusBarController: NSObject, NSMenuDelegate { // 8. Advanced Options Submenu let advancedMenu = NSMenu() + let slashCmdItem = NSMenuItem(title: "⚡️ Executive Voice Slash Commands", action: #selector(toggleVoiceCommandsAction(_:)), keyEquivalent: "") + slashCmdItem.target = self + slashCmdItem.state = SonioxSettings.shared.voiceCommands ? .on : .off + advancedMenu.addItem(slashCmdItem) + let punctItem = NSMenuItem(title: "🗣️ Voice Punctuation Commands", action: #selector(togglePunctuationAction(_:)), keyEquivalent: "") punctItem.target = self punctItem.state = SonioxSettings.shared.voicePunctuation ? .on : .off @@ -351,6 +356,11 @@ public final class StatusBarController: NSObject, NSMenuDelegate { buildMenu() } + @objc private func toggleVoiceCommandsAction(_ sender: NSMenuItem) { + SonioxSettings.shared.voiceCommands.toggle() + buildMenu() + } + @objc private func togglePunctuationAction(_ sender: NSMenuItem) { SonioxSettings.shared.voicePunctuation.toggle() buildMenu() diff --git a/src/VoiceCommandExecutor.swift b/src/VoiceCommandExecutor.swift new file mode 100644 index 0000000..378c46c --- /dev/null +++ b/src/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() + } + } + } +}