Browse Source

feat: pro voice commands engine: window tiling, web search, custom json commands, cheat sheet, and deep system controls

main
Ali Alavi 18 hours ago
parent
commit
894719b08e
  1. 140
      src/CustomCommandManager.swift
  2. 89
      src/StatusBarController.swift
  3. 387
      src/VoiceCommandExecutor.swift

140
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)
}
}

89
src/StatusBarController.swift

@ -194,6 +194,20 @@ public final class StatusBarController: NSObject, NSMenuDelegate {
soundItem.state = SonioxSettings.shared.playSounds ? .on : .off soundItem.state = SonioxSettings.shared.playSounds ? .on : .off
advancedMenu.addItem(soundItem) 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: "") let advancedMenuItem = NSMenuItem(title: "🛠️ Advanced Options", action: nil, keyEquivalent: "")
advancedMenuItem.submenu = advancedMenu advancedMenuItem.submenu = advancedMenu
menu.addItem(advancedMenuItem) menu.addItem(advancedMenuItem)
@ -386,6 +400,81 @@ public final class StatusBarController: NSObject, NSMenuDelegate {
buildMenu() 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) { @objc private func toggleLaunchAtLoginAction(_ sender: NSMenuItem) {
SonioxSettings.shared.launchAtLogin.toggle() SonioxSettings.shared.launchAtLogin.toggle()
buildMenu() buildMenu()

387
src/VoiceCommandExecutor.swift

@ -1,5 +1,6 @@
import Cocoa import Cocoa
import Carbon import Carbon
import ApplicationServices
public struct InstalledAppInfo { public struct InstalledAppInfo {
public let name: String public let name: String
@ -7,6 +8,15 @@ public struct InstalledAppInfo {
public let url: URL public let url: URL
} }
public enum WindowTileAction {
case left
case right
case top
case bottom
case maximize
case center
}
public final class VoiceCommandExecutor { public final class VoiceCommandExecutor {
public static let shared = VoiceCommandExecutor() public static let shared = VoiceCommandExecutor()
@ -15,7 +25,7 @@ public final class VoiceCommandExecutor {
// Comprehensive Persian to Canonical English Application Names // Comprehensive Persian to Canonical English Application Names
private let persianAppDictionary: [String: String] = [ private let persianAppDictionary: [String: String] = [
// System & Apple Apps
// System & Apple Core Apps
"ستینگ": "System Settings", "ستینگ": "System Settings",
"ستینگز": "System Settings", "ستینگز": "System Settings",
"تنظیمات": "System Settings", "تنظیمات": "System Settings",
@ -137,31 +147,51 @@ public final class VoiceCommandExecutor {
print("VoiceCommandExecutor: executing command for body: '\(body)'") 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) { if handleAppWithTab(lower: lower, original: body) {
playConfirmationSound() playConfirmationSound()
return true 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) { if handleStandaloneTab(lower: lower) {
playConfirmationSound() playConfirmationSound()
return true 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) { if handleSystemControls(lower: lower) {
playConfirmationSound() playConfirmationSound()
return true 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) { if handleKnownApp(lower: lower, original: body) {
playConfirmationSound() playConfirmationSound()
return true 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) { if handleUniversalInstalledApp(query: body) {
playConfirmationSound() playConfirmationSound()
return true return true
@ -174,6 +204,190 @@ public final class VoiceCommandExecutor {
return false 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 { private func handleAppWithTab(lower: String, original: String) -> Bool {
guard lower.contains("تب") || lower.contains("tab") else { return false } guard lower.contains("تب") || lower.contains("tab") else { return false }
@ -198,18 +412,24 @@ public final class VoiceCommandExecutor {
} }
HistoryManager.shared.addEntry("⚡️ [Command] \(appName): Tab \(tabNum)") HistoryManager.shared.addEntry("⚡️ [Command] \(appName): Tab \(tabNum)")
return true return true
} else if lower.contains("تب جدید") || lower.contains("نیو تب") || lower.contains("new tab") {
} else if lower.contains("تب جدید") || lower.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
} }
HistoryManager.shared.addEntry("⚡️ [Command] \(appName): New Tab") HistoryManager.shared.addEntry("⚡️ [Command] \(appName): New Tab")
return true return true
} else if lower.contains("بستن تب") || lower.contains("کلوز تب") || lower.contains("close tab") {
} else if lower.contains("بستن تب") || lower.contains("close tab") {
activateApp(named: appName) { activateApp(named: appName) {
self.pressHotkey(keyCode: 13, modifiers: .maskCommand) // Cmd+W self.pressHotkey(keyCode: 13, modifiers: .maskCommand) // Cmd+W
} }
HistoryManager.shared.addEntry("⚡️ [Command] \(appName): Close Tab") HistoryManager.shared.addEntry("⚡️ [Command] \(appName): Close Tab")
return true 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 return false
@ -217,47 +437,38 @@ public final class VoiceCommandExecutor {
private func extractTabNumber(from text: String) -> Int? { private func extractTabNumber(from text: String) -> Int? {
let t = text.lowercased() let t = text.lowercased()
// Tab 1
if t.contains("تب ۱") || t.contains("تب 1") || t.contains("تب یک") || t.contains("تب اول") || if t.contains("تب ۱") || t.contains("تب 1") || t.contains("تب یک") || t.contains("تب اول") ||
t.contains("tab 1") || t.contains("tab one") || t.contains("tab first") { t.contains("tab 1") || t.contains("tab one") || t.contains("tab first") {
return 1 return 1
} }
// Tab 2
if t.contains("تب ۲") || t.contains("تب 2") || t.contains("تب دو") || t.contains("تب دوم") || if t.contains("تب ۲") || t.contains("تب 2") || t.contains("تب دو") || t.contains("تب دوم") ||
t.contains("tab 2") || t.contains("tab two") || t.contains("tab second") { t.contains("tab 2") || t.contains("tab two") || t.contains("tab second") {
return 2 return 2
} }
// Tab 3
if t.contains("تب ۳") || t.contains("تب 3") || t.contains("تب سه") || t.contains("تب سوم") || if t.contains("تب ۳") || t.contains("تب 3") || t.contains("تب سه") || t.contains("تب سوم") ||
t.contains("tab 3") || t.contains("tab three") || t.contains("tab third") { t.contains("tab 3") || t.contains("tab three") || t.contains("tab third") {
return 3 return 3
} }
// Tab 4
if t.contains("تب ۴") || t.contains("تب 4") || t.contains("تب چهار") || t.contains("تب چهارم") || if t.contains("تب ۴") || t.contains("تب 4") || t.contains("تب چهار") || t.contains("تب چهارم") ||
t.contains("tab 4") || t.contains("tab four") { t.contains("tab 4") || t.contains("tab four") {
return 4 return 4
} }
// Tab 5
if t.contains("تب ۵") || t.contains("تب 5") || t.contains("تب پنج") || t.contains("تب پنجم") || if t.contains("تب ۵") || t.contains("تب 5") || t.contains("تب پنج") || t.contains("تب پنجم") ||
t.contains("tab 5") || t.contains("tab five") { t.contains("tab 5") || t.contains("tab five") {
return 5 return 5
} }
// Tab 6
if t.contains("تب ۶") || t.contains("تب 6") || t.contains("تب شش") || t.contains("تب ششم") || if t.contains("تب ۶") || t.contains("تب 6") || t.contains("تب شش") || t.contains("تب ششم") ||
t.contains("tab 6") || t.contains("tab six") { t.contains("tab 6") || t.contains("tab six") {
return 6 return 6
} }
// Tab 7
if t.contains("تب ۷") || t.contains("تب 7") || t.contains("تب هفت") || t.contains("تب هفتم") || if t.contains("تب ۷") || t.contains("تب 7") || t.contains("تب هفت") || t.contains("تب هفتم") ||
t.contains("tab 7") || t.contains("tab seven") { t.contains("tab 7") || t.contains("tab seven") {
return 7 return 7
} }
// Tab 8
if t.contains("تب ۸") || t.contains("تب 8") || t.contains("تب هشت") || t.contains("تب هشتم") || if t.contains("تب ۸") || t.contains("تب 8") || t.contains("تب هشت") || t.contains("تب هشتم") ||
t.contains("tab 8") || t.contains("tab eight") { t.contains("tab 8") || t.contains("tab eight") {
return 8 return 8
} }
// Tab 9 / Last Tab
if t.contains("تب ۹") || t.contains("تب 9") || t.contains("تب نه") || t.contains("تب نهم") || t.contains("تب آخر") || if t.contains("تب ۹") || t.contains("تب 9") || t.contains("تب نه") || t.contains("تب نهم") || t.contains("تب آخر") ||
t.contains("tab 9") || t.contains("tab nine") || t.contains("tab last") { t.contains("tab 9") || t.contains("tab nine") || t.contains("tab last") {
return 9 return 9
@ -265,6 +476,7 @@ public final class VoiceCommandExecutor {
return nil return nil
} }
// MARK: - 4. Standalone Tab Commands
private func handleStandaloneTab(lower: String) -> Bool { private func handleStandaloneTab(lower: String) -> Bool {
if let num = extractTabNumber(from: lower) { if let num = extractTabNumber(from: lower) {
switchToTab(number: num) switchToTab(number: num)
@ -281,6 +493,11 @@ public final class VoiceCommandExecutor {
HistoryManager.shared.addEntry("⚡️ [Command] Close Tab") HistoryManager.shared.addEntry("⚡️ [Command] Close Tab")
return true 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") { if lower.contains("تب بعد") || lower.contains("تب بعدی") || lower.contains("next tab") {
pressHotkey(keyCode: 48, modifiers: .maskControl) // Ctrl+Tab pressHotkey(keyCode: 48, modifiers: .maskControl) // Ctrl+Tab
HistoryManager.shared.addEntry("⚡️ [Command] Next Tab") HistoryManager.shared.addEntry("⚡️ [Command] Next Tab")
@ -296,91 +513,175 @@ public final class VoiceCommandExecutor {
HistoryManager.shared.addEntry("⚡️ [Command] Refresh Page") HistoryManager.shared.addEntry("⚡️ [Command] Refresh Page")
return true 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 return false
} }
// MARK: - 5. System Controls
private func handleSystemControls(lower: String) -> Bool { 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") { if lower.contains("صدا زیاد") || lower.contains("افزایش صدا") || lower.contains("volume up") {
runAppleScript("set volume output volume ((output volume of (get volume settings)) + 15)") 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 return true
} }
if lower.contains("صدا کم") || lower.contains("کاهش صدا") || lower.contains("volume down") { if lower.contains("صدا کم") || lower.contains("کاهش صدا") || lower.contains("volume down") {
runAppleScript("set volume output volume ((output volume of (get volume settings)) - 15)") 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 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))") 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 return true
} }
// Lock screen
// Lock Screen
if lower.contains("قفل") || lower.contains("لاک") || lower.contains("lock screen") || lower == "lock" { if lower.contains("قفل") || lower.contains("لاک") || lower.contains("lock screen") || lower == "lock" {
let task = Process() let task = Process()
task.executableURL = URL(fileURLWithPath: "/usr/bin/pmset") task.executableURL = URL(fileURLWithPath: "/usr/bin/pmset")
task.arguments = ["displaysleepnow"] task.arguments = ["displaysleepnow"]
try? task.run() try? task.run()
HistoryManager.shared.addEntry("⚡️ [Command] Lock Screen")
HistoryManager.shared.addEntry("⚡️ [System] Lock Screen")
return true
}
// 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 return true
} }
// Screenshot
if lower.contains("اسکرین شات") || lower.contains("عکس صفحه") || lower.contains("screenshot") {
// Screenshot (Interactive)
if lower.contains("اسکرین شات") || lower.contains("عکس صفحه") || lower == "screenshot" {
let task = Process() let task = Process()
task.executableURL = URL(fileURLWithPath: "/usr/sbin/screencapture") task.executableURL = URL(fileURLWithPath: "/usr/sbin/screencapture")
task.arguments = ["-i", "-c"] task.arguments = ["-i", "-c"]
try? task.run() 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 return true
} }
// Dark Mode toggle // Dark Mode toggle
if lower.contains("دارک مود") || lower.contains("لایت مود") || lower.contains("حالت شب") || lower.contains("حالت روز") || lower.contains("dark mode") { 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") 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 return true
} }
// Clipboard & Editing hotkeys // Clipboard & Editing hotkeys
if lower == "کپی" || lower == "copy" { if lower == "کپی" || lower == "copy" {
pressHotkey(keyCode: 8, modifiers: .maskCommand) // Cmd+C pressHotkey(keyCode: 8, modifiers: .maskCommand) // Cmd+C
HistoryManager.shared.addEntry("⚡️ [Command] Copy")
HistoryManager.shared.addEntry("⚡️ [Edit] Copy")
return true return true
} }
if lower == "پیست" || lower == "paste" { if lower == "پیست" || lower == "paste" {
pressHotkey(keyCode: 9, modifiers: .maskCommand) // Cmd+V pressHotkey(keyCode: 9, modifiers: .maskCommand) // Cmd+V
HistoryManager.shared.addEntry("⚡️ [Command] Paste")
HistoryManager.shared.addEntry("⚡️ [Edit] Paste")
return true return true
} }
if lower == "کات" || lower == "cut" { if lower == "کات" || lower == "cut" {
pressHotkey(keyCode: 7, modifiers: .maskCommand) // Cmd+X pressHotkey(keyCode: 7, modifiers: .maskCommand) // Cmd+X
HistoryManager.shared.addEntry("⚡️ [Command] Cut")
HistoryManager.shared.addEntry("⚡️ [Edit] Cut")
return true return true
} }
if lower.contains("انتخاب همه") || lower.contains("سلکت آل") || lower == "select all" { if lower.contains("انتخاب همه") || lower.contains("سلکت آل") || lower == "select all" {
pressHotkey(keyCode: 0, modifiers: .maskCommand) // Cmd+A pressHotkey(keyCode: 0, modifiers: .maskCommand) // Cmd+A
HistoryManager.shared.addEntry("⚡️ [Command] Select All")
HistoryManager.shared.addEntry("⚡️ [Edit] Select All")
return true return true
} }
if lower == "آندو" || lower == "برگرد" || lower == "undo" { if lower == "آندو" || lower == "برگرد" || lower == "undo" {
pressHotkey(keyCode: 6, modifiers: .maskCommand) // Cmd+Z 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 return true
} }
if lower == "اینتر" || lower == "enter" { 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 true
} }
return false return false
} }
// MARK: - 6. Persian Dictionary Apps
private func handleKnownApp(lower: String, original: String) -> Bool { private func handleKnownApp(lower: String, original: String) -> Bool {
// Strip common prefix like "باز کن" or "open"
var query = lower var query = lower
for prefix in ["باز کن ", "بازکن ", "برو به ", "برو تو ", "اجرا کن ", "open ", "launch ", "goto "] { for prefix in ["باز کن ", "بازکن ", "برو به ", "برو تو ", "اجرا کن ", "open ", "launch ", "goto "] {
if query.hasPrefix(prefix) { 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 { for (persianKey, englishAppName) in persianAppDictionary {
if query == persianKey || query.contains(persianKey) { if query == persianKey || query.contains(persianKey) {
launchOrActivateApp(named: englishAppName) launchOrActivateApp(named: englishAppName)
HistoryManager.shared.addEntry("⚡️ [Command] Open \(englishAppName)")
HistoryManager.shared.addEntry("⚡️ [App] Open \(englishAppName)")
return true return true
} }
} }
@ -401,6 +701,7 @@ public final class VoiceCommandExecutor {
return false return false
} }
// MARK: - 7. Universal Installed App Scanner
private func handleUniversalInstalledApp(query: String) -> Bool { private func handleUniversalInstalledApp(query: String) -> Bool {
var cleanQuery = query.trimmingCharacters(in: .whitespacesAndNewlines) var cleanQuery = query.trimmingCharacters(in: .whitespacesAndNewlines)
for prefix in ["باز کن ", "بازکن ", "open ", "launch "] { for prefix in ["باز کن ", "بازکن ", "open ", "launch "] {
@ -414,19 +715,17 @@ public final class VoiceCommandExecutor {
let normalizedQuery = normalizeForMatching(cleanQuery) let normalizedQuery = normalizeForMatching(cleanQuery)
refreshInstalledApps() refreshInstalledApps()
// 1. Check exact normalized match
// Exact match
if let match = cachedInstalledApps.first(where: { $0.normalizedName == normalizedQuery }) { if let match = cachedInstalledApps.first(where: { $0.normalizedName == normalizedQuery }) {
launchApp(at: match.url, name: match.name) launchApp(at: match.url, name: match.name)
return true return true
} }
// 2. Check prefix match
// Prefix match
if let match = cachedInstalledApps.first(where: { $0.normalizedName.hasPrefix(normalizedQuery) }) { if let match = cachedInstalledApps.first(where: { $0.normalizedName.hasPrefix(normalizedQuery) }) {
launchApp(at: match.url, name: match.name) launchApp(at: match.url, name: match.name)
return true return true
} }
// 3. Check contains match
// Substring match
if let match = cachedInstalledApps.first(where: { $0.normalizedName.contains(normalizedQuery) || normalizedQuery.contains($0.normalizedName) }) { if let match = cachedInstalledApps.first(where: { $0.normalizedName.contains(normalizedQuery) || normalizedQuery.contains($0.normalizedName) }) {
launchApp(at: match.url, name: match.name) launchApp(at: match.url, name: match.name)
return true return true
@ -444,7 +743,7 @@ public final class VoiceCommandExecutor {
conf.activates = true conf.activates = true
ws.openApplication(at: url, configuration: conf, completionHandler: nil) 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 { private func normalizeForMatching(_ text: String) -> String {

Loading…
Cancel
Save