diff --git a/src/CustomCommandManager.swift b/src/CustomCommandManager.swift new file mode 100644 index 0000000..7f6cd21 --- /dev/null +++ b/src/CustomCommandManager.swift @@ -0,0 +1,140 @@ +import Foundation +import Cocoa + +public struct CustomCommand: Codable { + public let triggers: [String] + public let action: String // "open_url", "open_app", "shell" + public let value: String // URL, App Name, or Shell command string + public let title: String? + + public init(triggers: [String], action: String, value: String, title: String? = nil) { + self.triggers = triggers + self.action = action + self.value = value + self.title = title + } +} + +public final class CustomCommandManager { + public static let shared = CustomCommandManager() + + public let fileURL: URL + private var commands: [CustomCommand] = [] + private let queue = DispatchQueue(label: "com.soniox.customcommands", qos: .utility) + + private init() { + let appSupport = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first! + let dir = appSupport.appendingPathComponent("SonioxVoice") + try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) + self.fileURL = dir.appendingPathComponent("custom_commands.json") + ensureDefaultFile() + loadCommands() + } + + private func ensureDefaultFile() { + guard !FileManager.default.fileExists(atPath: fileURL.path) else { return } + + let sampleCommands: [CustomCommand] = [ + CustomCommand( + triggers: ["گیت ما", "گیت شرکت", "our git", "company git"], + action: "open_url", + value: "https://git.nwhco.ir", + title: "Open NewHorizon Git" + ), + CustomCommand( + triggers: ["گیت هاب", "github"], + action: "open_url", + value: "https://github.com", + title: "Open GitHub" + ), + CustomCommand( + triggers: ["چت جی پی تی", "chatgpt"], + action: "open_url", + value: "https://chatgpt.com", + title: "Open ChatGPT" + ), + CustomCommand( + triggers: ["کلود", "claude"], + action: "open_url", + value: "https://claude.ai", + title: "Open Claude" + ), + CustomCommand( + triggers: ["ترمینال جدید", "new terminal"], + action: "open_app", + value: "Terminal", + title: "Open Terminal" + ) + ] + + let encoder = JSONEncoder() + encoder.outputFormatting = [.prettyPrinted, .sortedKeys] + if let data = try? encoder.encode(sampleCommands) { + try? data.write(to: fileURL, options: .atomic) + } + } + + public func loadCommands() { + queue.async { + guard let data = try? Data(contentsOf: self.fileURL), + let loaded = try? JSONDecoder().decode([CustomCommand].self, from: data) else { + return + } + self.commands = loaded + print("CustomCommandManager: Loaded \(loaded.count) custom voice commands.") + } + } + + public func findMatchingCommand(query: String) -> CustomCommand? { + let clean = query.lowercased().trimmingCharacters(in: .whitespacesAndNewlines) + return queue.sync { + for cmd in commands { + for trigger in cmd.triggers { + let trigLower = trigger.lowercased().trimmingCharacters(in: .whitespacesAndNewlines) + if clean == trigLower || clean.hasPrefix(trigLower) || clean.contains(trigLower) { + return cmd + } + } + } + return nil + } + } + + public func executeCommand(_ cmd: CustomCommand) -> Bool { + print("CustomCommandManager: Executing custom action '\(cmd.action)' for value '\(cmd.value)'") + let title = cmd.title ?? "\(cmd.action): \(cmd.value)" + + switch cmd.action.lowercased() { + case "open_url": + if let url = URL(string: cmd.value) { + NSWorkspace.shared.open(url) + HistoryManager.shared.addEntry("⚡️ [Custom] \(title)") + return true + } + + case "open_app": + let task = Process() + task.executableURL = URL(fileURLWithPath: "/usr/bin/open") + task.arguments = ["-a", cmd.value] + try? task.run() + HistoryManager.shared.addEntry("⚡️ [Custom] Open \(cmd.value)") + return true + + case "shell": + let task = Process() + task.executableURL = URL(fileURLWithPath: "/bin/sh") + task.arguments = ["-c", cmd.value] + try? task.run() + HistoryManager.shared.addEntry("⚡️ [Custom] Shell: \(title)") + return true + + default: + return false + } + return false + } + + public func openConfigFileInEditor() { + NSWorkspace.shared.open(fileURL) + } +} diff --git a/src/StatusBarController.swift b/src/StatusBarController.swift index c8d68f9..7e51410 100644 --- a/src/StatusBarController.swift +++ b/src/StatusBarController.swift @@ -194,6 +194,20 @@ public final class StatusBarController: NSObject, NSMenuDelegate { soundItem.state = SonioxSettings.shared.playSounds ? .on : .off advancedMenu.addItem(soundItem) + advancedMenu.addItem(NSMenuItem.separator()) + + let editCmdsItem = NSMenuItem(title: "📝 Customize Voice Commands (JSON)...", action: #selector(editCustomCommandsAction), keyEquivalent: "") + editCmdsItem.target = self + advancedMenu.addItem(editCmdsItem) + + let reloadCmdsItem = NSMenuItem(title: "🔄 Reload Custom Commands", action: #selector(reloadCustomCommandsAction), keyEquivalent: "") + reloadCmdsItem.target = self + advancedMenu.addItem(reloadCmdsItem) + + let cheatSheetItem = NSMenuItem(title: "📖 Voice Commands Cheat Sheet...", action: #selector(openCheatSheetAction), keyEquivalent: "") + cheatSheetItem.target = self + advancedMenu.addItem(cheatSheetItem) + let advancedMenuItem = NSMenuItem(title: "🛠️ Advanced Options", action: nil, keyEquivalent: "") advancedMenuItem.submenu = advancedMenu menu.addItem(advancedMenuItem) @@ -386,6 +400,81 @@ public final class StatusBarController: NSObject, NSMenuDelegate { buildMenu() } + @objc private func editCustomCommandsAction() { + CustomCommandManager.shared.openConfigFileInEditor() + } + + @objc private func reloadCustomCommandsAction() { + CustomCommandManager.shared.loadCommands() + buildMenu() + let alert = NSAlert() + alert.messageText = "Custom Commands Reloaded" + alert.informativeText = "Successfully reloaded custom voice commands from custom_commands.json." + alert.alertStyle = .informational + alert.addButton(withTitle: "OK") + alert.runModal() + } + + @objc private func openCheatSheetAction() { + let helpText = """ + === Soniox Voice Commands Cheat Sheet === + + [Applications & Windows] + • /settings OR /ستینگ -> Open macOS Settings + • /antigravity ide OR /آنتی گرویتی آیدی ای -> Open Antigravity IDE + • /antigravity OR /آنتی گرویتی -> Open Antigravity + • /firefox OR /فایرفاکس -> Open Firefox + • /chrome OR /کروم -> Open Google Chrome + • /telegram OR /تلگرام -> Open Telegram + • /left OR /چپ -> Tile window to Left half + • /right OR /راست -> Tile window to Right half + • /maximize OR /تمام صفحه -> Maximize window + • /center OR /وسط -> Center window on screen + • /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) + • /search [query] OR /سرچ [موضوع] -> Search on Google + • /youtube [query] OR /یوتیوب [موضوع] -> Search on YouTube + + [System Controls] + • /volume up OR /صدا زیاد -> Increase volume (+15%) + • /volume down OR /صدا کم -> Decrease volume (-15%) + • /volume 50 OR /صدا ۵۰ -> Set volume to 50% + • /mute OR /میوت -> Toggle sound mute + • /lock OR /قفل -> Lock screen + • /screenshot OR /اسکرین شات -> Interactive screenshot + • /dark mode OR /دارک مود -> Toggle Dark / Light mode + + [Editing & Clipboard] + • /copy OR /کپی -> Copy selection (Cmd+C) + • /paste OR /پیست -> Paste (Cmd+V) + • /cut OR /کات -> Cut selection (Cmd+X) + • /select all OR /انتخاب همه -> Select All (Cmd+A) + • /undo OR /آندو -> Undo (Cmd+Z) + • /redo OR /ریدو -> Redo (Cmd+Shift+Z) + • /save OR /سیو -> Save document (Cmd+S) + • /enter OR /اینتر -> Press Return key + + [Custom Voice Commands] + • Customize anytime via 'custom_commands.json' under Advanced Options! + """ + + let alert = NSAlert() + alert.messageText = "Soniox Voice Commands Reference" + alert.informativeText = helpText + alert.alertStyle = .informational + alert.addButton(withTitle: "Got it!") + alert.runModal() + } + @objc private func toggleLaunchAtLoginAction(_ sender: NSMenuItem) { SonioxSettings.shared.launchAtLogin.toggle() buildMenu() diff --git a/src/VoiceCommandExecutor.swift b/src/VoiceCommandExecutor.swift index 9d1a23f..d5d648a 100644 --- a/src/VoiceCommandExecutor.swift +++ b/src/VoiceCommandExecutor.swift @@ -1,5 +1,6 @@ import Cocoa import Carbon +import ApplicationServices public struct InstalledAppInfo { public let name: String @@ -7,6 +8,15 @@ public struct InstalledAppInfo { public let url: URL } +public enum WindowTileAction { + case left + case right + case top + case bottom + case maximize + case center +} + public final class VoiceCommandExecutor { public static let shared = VoiceCommandExecutor() @@ -15,7 +25,7 @@ public final class VoiceCommandExecutor { // Comprehensive Persian to Canonical English Application Names private let persianAppDictionary: [String: String] = [ - // System & Apple Apps + // System & Apple Core Apps "ستینگ": "System Settings", "ستینگز": "System Settings", "تنظیمات": "System Settings", @@ -137,31 +147,51 @@ public final class VoiceCommandExecutor { print("VoiceCommandExecutor: executing command for body: '\(body)'") - // 1. App + Tab combinations (e.g. "فایرفاکس تب ۱", "کروم تب ۲", "firefox tab 1", "chrome tab 3") + // 1. User-Customized Commands (from custom_commands.json) + if let customCmd = CustomCommandManager.shared.findMatchingCommand(query: body) { + if CustomCommandManager.shared.executeCommand(customCmd) { + playConfirmationSound() + return true + } + } + + // 2. Web Search & Direct Navigation ("سرچ X", "search X", "یوتیوب X", "youtube X") + if handleWebSearchAndNavigation(lower: lower, original: body) { + playConfirmationSound() + return true + } + + // 3. Window Tiling & Management (چپ, راست, بالا, پایین, تمام صفحه, وسط, مینیمایز, هاید) + if handleWindowTiling(lower: lower) { + playConfirmationSound() + return true + } + + // 4. App + Tab combinations (e.g. "فایرفاکس تب ۱", "کروم تب ۲", "firefox tab 1", "chrome tab 3") if handleAppWithTab(lower: lower, original: body) { playConfirmationSound() return true } - // 2. Standalone Tab & Navigation commands (e.g. "تب ۱", "تب جدید", "بستن تب", "tab 1", "new tab") + // 5. Standalone Tab & Navigation commands (e.g. "تب ۱", "تب جدید", "بستن تب", "tab 1", "new tab") if handleStandaloneTab(lower: lower) { playConfirmationSound() return true } - // 3. System controls (volume, mute, lock, screenshot, dark mode, clipboard) + // 6. System controls (volume, mute, lock, screenshot, dark mode, clipboard) if handleSystemControls(lower: lower) { playConfirmationSound() return true } - // 4. Match App from Persian dictionary or exact names + // 7. Match App from Persian dictionary or exact names if handleKnownApp(lower: lower, original: body) { playConfirmationSound() return true } - // 5. Universal fuzzy resolver across all installed apps in /Applications and /System/Applications + // 8. Universal fuzzy resolver across all installed apps in /Applications and /System/Applications if handleUniversalInstalledApp(query: body) { playConfirmationSound() return true @@ -174,6 +204,190 @@ public final class VoiceCommandExecutor { return false } + // MARK: - 1. Web Search & Direct Navigation + private func handleWebSearchAndNavigation(lower: String, original: String) -> Bool { + // Google Search + if lower.hasPrefix("سرچ ") || lower.hasPrefix("جستجو ") || lower.hasPrefix("search ") || lower.hasPrefix("google ") { + var q = original + for p in ["سرچ ", "جستجو ", "search ", "google "] { + if q.lowercased().hasPrefix(p) { + q = String(q.dropFirst(p.count)).trimmingCharacters(in: .whitespacesAndNewlines) + break + } + } + if !q.isEmpty, let enc = q.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), + let url = URL(string: "https://www.google.com/search?q=\(enc)") { + NSWorkspace.shared.open(url) + HistoryManager.shared.addEntry("⚡️ [Search] Google: \(q)") + return true + } + } + + // YouTube Search + if lower.hasPrefix("یوتیوب ") || lower.hasPrefix("youtube ") { + var q = original + for p in ["یوتیوب ", "youtube "] { + if q.lowercased().hasPrefix(p) { + q = String(q.dropFirst(p.count)).trimmingCharacters(in: .whitespacesAndNewlines) + break + } + } + if !q.isEmpty, let enc = q.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), + let url = URL(string: "https://www.youtube.com/results?search_query=\(enc)") { + NSWorkspace.shared.open(url) + HistoryManager.shared.addEntry("⚡️ [Search] YouTube: \(q)") + return true + } + } + + // Direct URL navigation ("برو به X" / "go to X") + if lower.hasPrefix("برو به ") || lower.hasPrefix("goto ") || lower.hasPrefix("go to ") { + var target = original + for p in ["برو به ", "goto ", "go to "] { + if target.lowercased().hasPrefix(p) { + target = String(target.dropFirst(p.count)).trimmingCharacters(in: .whitespacesAndNewlines) + break + } + } + if !target.isEmpty { + var urlStr = target + if !urlStr.contains("://") { + urlStr = "https://" + urlStr + } + if let url = URL(string: urlStr) { + NSWorkspace.shared.open(url) + HistoryManager.shared.addEntry("⚡️ [Nav] Go to: \(target)") + return true + } + } + } + + return false + } + + // MARK: - 2. Window Tiling & Management + private func handleWindowTiling(lower: String) -> Bool { + // Snap Left + if lower == "چپ" || lower.contains("پنجره چپ") || lower == "left" || lower == "window left" { + tileFrontmostWindow(action: .left) + HistoryManager.shared.addEntry("⚡️ [Window] Tile Left Half") + return true + } + // Snap Right + if lower == "راست" || lower.contains("پنجره راست") || lower == "right" || lower == "window right" { + tileFrontmostWindow(action: .right) + HistoryManager.shared.addEntry("⚡️ [Window] Tile Right Half") + return true + } + // Snap Top + if lower == "بالا" || lower.contains("پنجره بالا") || lower == "top" || lower == "window top" { + tileFrontmostWindow(action: .top) + HistoryManager.shared.addEntry("⚡️ [Window] Tile Top Half") + return true + } + // Snap Bottom + if lower == "پایین" || lower.contains("پنجره پایین") || lower == "bottom" || lower == "window bottom" { + tileFrontmostWindow(action: .bottom) + HistoryManager.shared.addEntry("⚡️ [Window] Tile Bottom Half") + return true + } + // Maximize / Full Screen + if lower.contains("تمام صفحه") || lower.contains("فول اسکرین") || lower.contains("ماکزیمایز") || lower == "maximize" || lower == "full screen" { + tileFrontmostWindow(action: .maximize) + HistoryManager.shared.addEntry("⚡️ [Window] Maximize Window") + return true + } + // Center Window + if lower == "وسط" || lower == "مرکز" || lower.contains("پنجره وسط") || lower == "center" || lower == "window center" { + tileFrontmostWindow(action: .center) + HistoryManager.shared.addEntry("⚡️ [Window] Center Window") + return true + } + // Minimize + if lower == "مینیمایز" || lower.contains("کوچک کردن") || lower == "minimize" { + pressHotkey(keyCode: 46, modifiers: .maskCommand) // Cmd+M + HistoryManager.shared.addEntry("⚡️ [Window] Minimize") + return true + } + // Hide Frontmost App + if lower == "هاید" || lower == "مخفی" || lower.contains("مخفی کن") || lower == "hide" { + pressHotkey(keyCode: 4, modifiers: .maskCommand) // Cmd+H + HistoryManager.shared.addEntry("⚡️ [Window] Hide App") + return true + } + // Hide Others + if lower.contains("هاید بقیه") || lower.contains("مخفی کردن بقیه") || lower == "hide others" { + pressHotkey(keyCode: 4, modifiers: [.maskCommand, .maskAlternate]) // Cmd+Option+H + HistoryManager.shared.addEntry("⚡️ [Window] Hide Other Apps") + return true + } + // Close Window + if lower == "بستن پنجره" || lower == "کلوز پنجره" || lower == "close window" { + pressHotkey(keyCode: 13, modifiers: .maskCommand) // Cmd+W + HistoryManager.shared.addEntry("⚡️ [Window] Close Window") + return true + } + // Quit App + if lower == "ببند" || lower.contains("خروج از برنامه") || lower.contains("کویت") || lower == "quit" || lower == "quit app" { + pressHotkey(keyCode: 12, modifiers: .maskCommand) // Cmd+Q + HistoryManager.shared.addEntry("⚡️ [Window] Quit App") + return true + } + + return false + } + + private func tileFrontmostWindow(action: WindowTileAction) { + guard let frontApp = NSWorkspace.shared.frontmostApplication, + let screen = NSScreen.main else { return } + + let appElem = AXUIElementCreateApplication(frontApp.processIdentifier) + var windowObj: CFTypeRef? + guard AXUIElementCopyAttributeValue(appElem, kAXFocusedWindowAttribute as CFString, &windowObj) == .success, + let winElem = windowObj else { return } + + let axWin = winElem as! AXUIElement + let screenFrame = screen.visibleFrame + let fullH = NSScreen.screens.first?.frame.height ?? screen.frame.height + let axY = fullH - (screenFrame.origin.y + screenFrame.size.height) + + var newPos = CGPoint.zero + var newSize = CGSize.zero + + switch action { + case .left: + newPos = CGPoint(x: screenFrame.origin.x, y: axY) + newSize = CGSize(width: screenFrame.width / 2, height: screenFrame.height) + case .right: + newPos = CGPoint(x: screenFrame.origin.x + screenFrame.width / 2, y: axY) + newSize = CGSize(width: screenFrame.width / 2, height: screenFrame.height) + case .top: + newPos = CGPoint(x: screenFrame.origin.x, y: axY) + newSize = CGSize(width: screenFrame.width, height: screenFrame.height / 2) + case .bottom: + newPos = CGPoint(x: screenFrame.origin.x, y: axY + screenFrame.height / 2) + newSize = CGSize(width: screenFrame.width, height: screenFrame.height / 2) + case .maximize: + newPos = CGPoint(x: screenFrame.origin.x, y: axY) + newSize = CGSize(width: screenFrame.width, height: screenFrame.height) + case .center: + let w = min(1100, screenFrame.width * 0.85) + let h = min(750, screenFrame.height * 0.85) + newPos = CGPoint(x: screenFrame.origin.x + (screenFrame.width - w) / 2, y: axY + (screenFrame.height - h) / 2) + newSize = CGSize(width: w, height: h) + } + + var size = newSize + var pos = newPos + if let sVal = AXValueCreate(.cgSize, &size) { + AXUIElementSetAttributeValue(axWin, kAXSizeAttribute as CFString, sVal) + } + if let pVal = AXValueCreate(.cgPoint, &pos) { + AXUIElementSetAttributeValue(axWin, kAXPositionAttribute as CFString, pVal) + } + } + + // MARK: - 3. App + Tab Combinations private func handleAppWithTab(lower: String, original: String) -> Bool { guard lower.contains("تب") || lower.contains("tab") else { return false } @@ -198,18 +412,24 @@ public final class VoiceCommandExecutor { } HistoryManager.shared.addEntry("⚡️ [Command] \(appName): Tab \(tabNum)") return true - } else if lower.contains("تب جدید") || lower.contains("نیو تب") || lower.contains("new tab") { + } 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("کلوز تب") || lower.contains("close tab") { + } 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 + } else if lower.contains("ری اپن") || lower.contains("reopen tab") || lower.contains("باز کردن مجدد تب") { + activateApp(named: appName) { + self.pressHotkey(keyCode: 17, modifiers: [.maskCommand, .maskShift]) // Cmd+Shift+T + } + HistoryManager.shared.addEntry("⚡️ [Command] \(appName): Reopen Tab") + return true } return false @@ -217,47 +437,38 @@ public final class VoiceCommandExecutor { private func extractTabNumber(from text: String) -> Int? { let t = text.lowercased() - // 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 } - // 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 } - // 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 } - // Tab 4 if t.contains("تب ۴") || t.contains("تب 4") || t.contains("تب چهار") || t.contains("تب چهارم") || t.contains("tab 4") || t.contains("tab four") { return 4 } - // Tab 5 if t.contains("تب ۵") || t.contains("تب 5") || t.contains("تب پنج") || t.contains("تب پنجم") || t.contains("tab 5") || t.contains("tab five") { return 5 } - // Tab 6 if t.contains("تب ۶") || t.contains("تب 6") || t.contains("تب شش") || t.contains("تب ششم") || t.contains("tab 6") || t.contains("tab six") { return 6 } - // Tab 7 if t.contains("تب ۷") || t.contains("تب 7") || t.contains("تب هفت") || t.contains("تب هفتم") || t.contains("tab 7") || t.contains("tab seven") { return 7 } - // Tab 8 if t.contains("تب ۸") || t.contains("تب 8") || t.contains("تب هشت") || t.contains("تب هشتم") || t.contains("tab 8") || t.contains("tab eight") { return 8 } - // 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 @@ -265,6 +476,7 @@ public final class VoiceCommandExecutor { return nil } + // MARK: - 4. Standalone Tab Commands private func handleStandaloneTab(lower: String) -> Bool { if let num = extractTabNumber(from: lower) { switchToTab(number: num) @@ -281,6 +493,11 @@ public final class VoiceCommandExecutor { HistoryManager.shared.addEntry("⚡️ [Command] Close Tab") return true } + if lower.contains("ری اپن") || lower.contains("reopen tab") || lower.contains("باز کردن مجدد تب") { + pressHotkey(keyCode: 17, modifiers: [.maskCommand, .maskShift]) // Cmd+Shift+T + HistoryManager.shared.addEntry("⚡️ [Command] Reopen Closed 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") @@ -296,91 +513,175 @@ public final class VoiceCommandExecutor { HistoryManager.shared.addEntry("⚡️ [Command] Refresh Page") return true } + if lower.contains("هارد رفرش") || lower.contains("hard refresh") { + pressHotkey(keyCode: 15, modifiers: [.maskCommand, .maskShift]) // Cmd+Shift+R + HistoryManager.shared.addEntry("⚡️ [Command] Hard Refresh Page") + return true + } + if lower.contains("آدرس بار") || lower.contains("ادرس بار") || lower.contains("address bar") { + pressHotkey(keyCode: 37, modifiers: .maskCommand) // Cmd+L + HistoryManager.shared.addEntry("⚡️ [Command] Focus Address Bar") + return true + } return false } + // MARK: - 5. System Controls private func handleSystemControls(lower: String) -> Bool { - // Volume controls + // Volume exact levels + if lower.contains("صدا ۱۰۰") || lower.contains("صدا 100") || lower == "volume 100" || lower == "max volume" { + runAppleScript("set volume output volume 100") + HistoryManager.shared.addEntry("⚡️ [Audio] Volume 100%") + return true + } + if lower.contains("صدا ۵۰") || lower.contains("صدا 50") || lower == "volume 50" { + runAppleScript("set volume output volume 50") + HistoryManager.shared.addEntry("⚡️ [Audio] Volume 50%") + return true + } + if lower.contains("صدا صفر") || lower.contains("صدا 0") || lower == "volume 0" { + runAppleScript("set volume output volume 0") + HistoryManager.shared.addEntry("⚡️ [Audio] Volume 0%") + return true + } + // Volume up / down 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") + HistoryManager.shared.addEntry("⚡️ [Audio] 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") + HistoryManager.shared.addEntry("⚡️ [Audio] Volume Down") return true } - if lower.contains("میوت") || lower.contains("بی صدا") || lower.contains("بی‌صدا") || lower.contains("mute") { + if lower.contains("میوت") || lower.contains("بی صدا") || lower.contains("بی‌صدا") || lower == "mute" { runAppleScript("set volume output muted not (output muted of (get volume settings))") - HistoryManager.shared.addEntry("⚡️ [Command] Toggle Mute") + HistoryManager.shared.addEntry("⚡️ [Audio] Toggle Mute") + return true + } + + // Media Play / Pause / Next / Prev + if lower == "پلی" || lower == "پاز" || lower == "توقف" || lower == "play" || lower == "pause" || lower == "play pause" { + runAppleScript("tell application \"System Events\" to key code 16 using {}") + HistoryManager.shared.addEntry("⚡️ [Media] Play / Pause") + return true + } + if lower.contains("آهنگ بعد") || lower.contains("اهنگ بعد") || lower == "next track" { + runAppleScript("tell application \"System Events\" to key code 19 using {}") + HistoryManager.shared.addEntry("⚡️ [Media] Next Track") + return true + } + if lower.contains("آهنگ قبل") || lower.contains("اهنگ قبل") || lower == "prev track" { + runAppleScript("tell application \"System Events\" to key code 20 using {}") + HistoryManager.shared.addEntry("⚡️ [Media] Previous Track") return true } - // Lock screen + // Lock Screen if lower.contains("قفل") || lower.contains("لاک") || lower.contains("lock screen") || lower == "lock" { let task = Process() task.executableURL = URL(fileURLWithPath: "/usr/bin/pmset") task.arguments = ["displaysleepnow"] try? task.run() - HistoryManager.shared.addEntry("⚡️ [Command] Lock Screen") + HistoryManager.shared.addEntry("⚡️ [System] Lock Screen") return true } - // Screenshot - if lower.contains("اسکرین شات") || lower.contains("عکس صفحه") || lower.contains("screenshot") { + // Sleep + if lower == "اسلیپ" || lower == "sleep" { + let task = Process() + task.executableURL = URL(fileURLWithPath: "/usr/bin/pmset") + task.arguments = ["sleepnow"] + try? task.run() + HistoryManager.shared.addEntry("⚡️ [System] Sleep Mac") + return true + } + + // Screenshot (Interactive) + if lower.contains("اسکرین شات") || lower.contains("عکس صفحه") || lower == "screenshot" { let task = Process() task.executableURL = URL(fileURLWithPath: "/usr/sbin/screencapture") task.arguments = ["-i", "-c"] try? task.run() - HistoryManager.shared.addEntry("⚡️ [Command] Interactive Screenshot") + HistoryManager.shared.addEntry("⚡️ [System] Interactive Screenshot") + return true + } + + // Full Screenshot + if lower.contains("اسکرین شات فول") || lower.contains("اسکرین شات کامل") || lower == "full screenshot" { + let task = Process() + task.executableURL = URL(fileURLWithPath: "/usr/sbin/screencapture") + task.arguments = ["-c"] + try? task.run() + HistoryManager.shared.addEntry("⚡️ [System] Full Screenshot to Clipboard") return true } // Dark Mode toggle 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") - HistoryManager.shared.addEntry("⚡️ [Command] Toggle Dark Mode") + HistoryManager.shared.addEntry("⚡️ [System] Toggle Dark Mode") return true } // Clipboard & Editing hotkeys if lower == "کپی" || lower == "copy" { pressHotkey(keyCode: 8, modifiers: .maskCommand) // Cmd+C - HistoryManager.shared.addEntry("⚡️ [Command] Copy") + HistoryManager.shared.addEntry("⚡️ [Edit] Copy") return true } if lower == "پیست" || lower == "paste" { pressHotkey(keyCode: 9, modifiers: .maskCommand) // Cmd+V - HistoryManager.shared.addEntry("⚡️ [Command] Paste") + HistoryManager.shared.addEntry("⚡️ [Edit] Paste") return true } if lower == "کات" || lower == "cut" { pressHotkey(keyCode: 7, modifiers: .maskCommand) // Cmd+X - HistoryManager.shared.addEntry("⚡️ [Command] Cut") + HistoryManager.shared.addEntry("⚡️ [Edit] Cut") return true } if lower.contains("انتخاب همه") || lower.contains("سلکت آل") || lower == "select all" { pressHotkey(keyCode: 0, modifiers: .maskCommand) // Cmd+A - HistoryManager.shared.addEntry("⚡️ [Command] Select All") + HistoryManager.shared.addEntry("⚡️ [Edit] Select All") return true } if lower == "آندو" || lower == "برگرد" || lower == "undo" { pressHotkey(keyCode: 6, modifiers: .maskCommand) // Cmd+Z - HistoryManager.shared.addEntry("⚡️ [Command] Undo") + HistoryManager.shared.addEntry("⚡️ [Edit] Undo") + return true + } + if lower == "ریدو" || lower == "redo" { + pressHotkey(keyCode: 6, modifiers: [.maskCommand, .maskShift]) // Cmd+Shift+Z + HistoryManager.shared.addEntry("⚡️ [Edit] Redo") + return true + } + if lower == "سیو" || lower == "ذخیره" || lower == "save" { + pressHotkey(keyCode: 1, modifiers: .maskCommand) // Cmd+S + HistoryManager.shared.addEntry("⚡️ [Edit] Save") + return true + } + if lower == "فایند" || lower == "پیدا کن" || lower == "find" { + pressHotkey(keyCode: 3, modifiers: .maskCommand) // Cmd+F + HistoryManager.shared.addEntry("⚡️ [Edit] Find") return true } if lower == "اینتر" || lower == "enter" { - pressHotkey(keyCode: 36, modifiers: []) // Return key - HistoryManager.shared.addEntry("⚡️ [Command] Enter") + pressHotkey(keyCode: 36, modifiers: []) // Return + HistoryManager.shared.addEntry("⚡️ [Edit] Enter") + return true + } + if lower.contains("پاک کردن خط") || lower == "delete line" { + pressHotkey(keyCode: 51, modifiers: .maskCommand) // Cmd+Backspace + HistoryManager.shared.addEntry("⚡️ [Edit] Delete Line") return true } return false } + // MARK: - 6. Persian Dictionary Apps 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) { @@ -389,11 +690,10 @@ public final class VoiceCommandExecutor { } } - // 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)") + HistoryManager.shared.addEntry("⚡️ [App] Open \(englishAppName)") return true } } @@ -401,6 +701,7 @@ public final class VoiceCommandExecutor { return false } + // MARK: - 7. Universal Installed App Scanner private func handleUniversalInstalledApp(query: String) -> Bool { var cleanQuery = query.trimmingCharacters(in: .whitespacesAndNewlines) for prefix in ["باز کن ", "بازکن ", "open ", "launch "] { @@ -414,19 +715,17 @@ public final class VoiceCommandExecutor { let normalizedQuery = normalizeForMatching(cleanQuery) refreshInstalledApps() - // 1. Check exact normalized match + // Exact match if let match = cachedInstalledApps.first(where: { $0.normalizedName == normalizedQuery }) { launchApp(at: match.url, name: match.name) return true } - - // 2. Check prefix match + // 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 + // Substring match if let match = cachedInstalledApps.first(where: { $0.normalizedName.contains(normalizedQuery) || normalizedQuery.contains($0.normalizedName) }) { launchApp(at: match.url, name: match.name) return true @@ -444,7 +743,7 @@ public final class VoiceCommandExecutor { conf.activates = true ws.openApplication(at: url, configuration: conf, completionHandler: nil) } - HistoryManager.shared.addEntry("⚡️ [Command] Open \(name)") + HistoryManager.shared.addEntry("⚡️ [App] Open \(name)") } private func normalizeForMatching(_ text: String) -> String {