|
|
@ -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 |
|
|
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() |
|
|
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 { |
|
|
|