import Cocoa import Carbon import ApplicationServices public struct InstalledAppInfo { public let name: String public let normalizedName: String 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() private var cachedInstalledApps: [InstalledAppInfo] = [] private var lastAppsScanTime: TimeInterval = 0 // Canonical Persian to English application names private let persianAppDictionary: [String: String] = [ // Antigravity Suite (All phonetic variations with and without alef) "انتی گراویتی ای دی ای": "Antigravity IDE", "انتی گراویتی ایدی ای": "Antigravity IDE", "انتی گراویتی ایدیه": "Antigravity IDE", "انتی گراویتی ادیتور": "Antigravity IDE", "انتی گراویتی کد": "Antigravity IDE", "انتی گراویتی ide": "Antigravity IDE", "انتی گرویتی ای دی ای": "Antigravity IDE", "انتی گرویتی ایدی ای": "Antigravity IDE", "انتی گرویتی ادیتور": "Antigravity IDE", "انتیگراویتی ایدی ای": "Antigravity IDE", "انتیگراویتی ای دی ای": "Antigravity IDE", "انتیگرویتی ایدی ای": "Antigravity IDE", "antigravity ide": "Antigravity IDE", "anti gravity ide": "Antigravity IDE", "انتی گراویتی": "Antigravity", "انتی گرویتی": "Antigravity", "انتیگراویتی": "Antigravity", "انتیگرویتی": "Antigravity", "antigravity": "Antigravity", "anti gravity": "Antigravity", // System Core Apps "ستینگ": "System Settings", "ستینگز": "System Settings", "تنظیمات": "System Settings", "تنظیمات سیستم": "System Settings", "سیستم ستینگ": "System Settings", "settings": "System Settings", "system settings": "System Settings", "فایندر": "Finder", "finder": "Finder", "ترمینال": "Terminal", "terminal": "Terminal", "سافاری": "Safari", "safari": "Safari", "ماشین حساب": "Calculator", "کلکولیتور": "Calculator", "calculator": "Calculator", "تقویم": "Calendar", "کلندر": "Calendar", "calendar": "Calendar", "یادداشت": "Notes", "یادداشت ها": "Notes", "نوت": "Notes", "نوتس": "Notes", "notes": "Notes", "اپ استور": "App Store", "app store": "App Store", "فوتوز": "Photos", "عکس": "Photos", "photos": "Photos", "موزیک": "Music", "اهنگ": "Music", "music": "Music", "پادکست": "Podcasts", "podcasts": "Podcasts", "مخاطبین": "Contacts", "contacts": "Contacts", "پیام ها": "Messages", "مسج": "Messages", "messages": "Messages", "ایمیل": "Mail", "میل": "Mail", "mail": "Mail", "شورتکات": "Shortcuts", "میانبرها": "Shortcuts", "shortcuts": "Shortcuts", "فری فرم": "Freeform", "freeform": "Freeform", "پریویو": "Preview", "preview": "Preview", "اکتیویتی مانیتور": "Activity Monitor", "activity monitor": "Activity Monitor", "دیسک یوتیلیتی": "Disk Utility", "disk utility": "Disk Utility", "پسورد": "Passwords", "passwords": "Passwords", // Third-party & Productivity "فایرفاکس": "Firefox", "فایر فاکس": "Firefox", "فایرفکس": "Firefox", "firefox": "Firefox", "کروم": "Google Chrome", "گوگل کروم": "Google Chrome", "chrome": "Google Chrome", "google chrome": "Google Chrome", "تلگرام": "Telegram", "تلگرم": "Telegram", "telegram": "Telegram", "تیم بی": "TeamBy", "تیم بای": "TeamBy", "تیمبی": "TeamBy", "تیمبای": "TeamBy", "teamby": "TeamBy", "فیگما": "Figma", "figma": "Figma", "وی تو باکس": "V2Box", "وی۲باکس": "V2Box", "ویتوباکس": "V2Box", "v2box": "V2Box", "وی اس کد": "Visual Studio Code", "ویژوال استودیو کد": "Visual Studio Code", "ویژوال استودیو": "Visual Studio Code", "کد": "Visual Studio Code", "vscode": "Visual Studio Code", "vs code": "Visual Studio Code", "code": "Visual Studio Code", "سانی اوکس": "Soniox Voice", "soniox": "Soniox Voice", "روزگار": "Roozegar", "roozegar": "Roozegar", "شمسی کال": "ShamsiCal", "shamsical": "ShamsiCal", "چند": "Chand", "chand": "Chand", "میدل کلیک": "MiddleClick", "middleclick": "MiddleClick" ] private init() { refreshInstalledApps() } // MARK: - Normalization Helpers public static func normalizePersianText(_ text: String) -> String { var s = text.lowercased() // 1. ZWNJ, ZWJ, and non-standard spaces s = s.replacingOccurrences(of: "\u{200C}", with: " ") s = s.replacingOccurrences(of: "\u{200D}", with: " ") s = s.replacingOccurrences(of: "\u{FEFF}", with: "") s = s.replacingOccurrences(of: "\u{00A0}", with: " ") // 2. Arabic to Persian letter unification s = s.replacingOccurrences(of: "ي", with: "ی") s = s.replacingOccurrences(of: "ى", with: "ی") s = s.replacingOccurrences(of: "ك", with: "ک") s = s.replacingOccurrences(of: "آ", with: "ا") s = s.replacingOccurrences(of: "أ", with: "ا") s = s.replacingOccurrences(of: "إ", with: "ا") s = s.replacingOccurrences(of: "ؤ", with: "و") s = s.replacingOccurrences(of: "ة", with: "ه") // 3. Remove vowels and diacritics let diacritics = ["\u{064B}", "\u{064C}", "\u{064D}", "\u{064E}", "\u{064F}", "\u{0650}", "\u{0651}", "\u{0652}"] for d in diacritics { s = s.replacingOccurrences(of: d, with: "") } // 4. Punctuation to space let punct = CharacterSet(charactersIn: ".,،؛:!؟?\"'«»()[]{}—-_/\\") s = s.components(separatedBy: punct).joined(separator: " ") // 5. Clean whitespace let parts = s.components(separatedBy: .whitespacesAndNewlines).filter { !$0.isEmpty } return parts.joined(separator: " ") } public func isSlashCommand(text: String) -> Bool { return getSlashWordPosition(text: text) != nil } public func getSlashWordPosition(text: String) -> Int? { let cleanText = text.trimmingCharacters(in: .whitespacesAndNewlines) guard !cleanText.isEmpty else { return nil } // Starting with slash symbol, e.g. "/settings" if cleanText.hasPrefix("/") || cleanText.hasPrefix("\\") { return 0 } let punct = CharacterSet(charactersIn: ".,،؛:!؟?\"'«»()[]{}—-_") let rawWords = cleanText.components(separatedBy: .whitespacesAndNewlines).filter { !$0.isEmpty } guard !rawWords.isEmpty else { return nil } let slashTriggers = ["اسلش", "اصلش", "slash", "slsh", "/", "\\"] // Word 1: Direct slash command (e.g. "اسلش آنتی‌گراویتی") let firstWordClean = rawWords[0].trimmingCharacters(in: punct).lowercased() if slashTriggers.contains(firstWordClean) || firstWordClean.hasPrefix("/") || firstWordClean.hasPrefix("\\") { return 0 } // Word 2: Slash after a single filler word (e.g. "امم اسلش فایرفاکس" or "خب، اسلش چپ") if rawWords.count >= 2 { let secondWordClean = rawWords[1].trimmingCharacters(in: punct).lowercased() if slashTriggers.contains(secondWordClean) || secondWordClean.hasPrefix("/") || secondWordClean.hasPrefix("\\") { return 1 } } // STRICT RULE: If "slash" occurs at word 3 or later (middle of sentence), it is NOT a command! return nil } public func extractCommandBody(rawText: String) -> String { guard let slashIndex = getSlashWordPosition(text: rawText) else { return "" } let rawWords = rawText.trimmingCharacters(in: .whitespacesAndNewlines) .components(separatedBy: .whitespacesAndNewlines) .filter { !$0.isEmpty } guard rawWords.count > slashIndex + 1 else { // Case where command is attached to first symbol, e.g. "/settings" if slashIndex == 0, let first = rawWords.first, first.hasPrefix("/") || first.hasPrefix("\\") { let bodyFromFirst = String(first.dropFirst()).trimmingCharacters(in: .whitespacesAndNewlines) let rest = rawWords.dropFirst().joined(separator: " ") return ([bodyFromFirst, rest].filter { !$0.isEmpty }).joined(separator: " ") } return "" } let bodyWords = rawWords[(slashIndex + 1)...] var body = bodyWords.joined(separator: " ") let leadingPunct = CharacterSet(charactersIn: " :.,،؛!؟?-_—\"'«»()[]{}") body = body.trimmingCharacters(in: leadingPunct) return body } // MARK: - Main Execution Dispatcher @discardableResult public func execute(rawText: String) -> Bool { let trimmed = rawText.trimmingCharacters(in: .whitespacesAndNewlines) guard isSlashCommand(text: trimmed) else { return false } let body = extractCommandBody(rawText: trimmed) let normalized = Self.normalizePersianText(body) print("VoiceCommandExecutor: Executing command body: '\(body)' | Normalized: '\(normalized)'") // 1. User-Customized Commands (from custom_commands.json) if let customCmd = CustomCommandManager.shared.findMatchingCommand(query: body) ?? CustomCommandManager.shared.findMatchingCommand(query: normalized) { if CustomCommandManager.shared.executeCommand(customCmd) { playConfirmationSound() VoiceCommandLogger.shared.logCommand( rawSpeech: rawText, body: body, normalized: normalized, actionCategory: "Custom User Command", actionDetail: customCmd.title ?? "\(customCmd.action): \(customCmd.value)", status: "SUCCESS" ) return true } } // 2. Web Search & Direct Navigation ("سرچ X", "search X", "یوتیوب X", "youtube X", "برو به X") if let searchDetail = handleWebSearchAndNavigation(normalized: normalized, original: body) { playConfirmationSound() VoiceCommandLogger.shared.logCommand( rawSpeech: rawText, body: body, normalized: normalized, actionCategory: "Web Search / Navigation", actionDetail: searchDetail, status: "SUCCESS" ) return true } // 3. Window Tiling & Management (چپ, راست, بالا, پایین, تمام صفحه, وسط, مینیمایز, هاید, بستن) if let tileDetail = handleWindowTiling(normalized: normalized) { playConfirmationSound() VoiceCommandLogger.shared.logCommand( rawSpeech: rawText, body: body, normalized: normalized, actionCategory: "Window Management", actionDetail: tileDetail, status: "SUCCESS" ) return true } // 4. App + Tab/Account combinations (e.g. "تلگرام ۱", "فایرفاکس ۲", "کروم ۳", "سافاری ۱", "telegram 1", "firefox 2") if let appTabDetail = handleAppWithTab(normalized: normalized) { playConfirmationSound() VoiceCommandLogger.shared.logCommand( rawSpeech: rawText, body: body, normalized: normalized, actionCategory: "App Account / Tab Switch", actionDetail: appTabDetail, status: "SUCCESS" ) return true } // 5. Standalone Tab & Navigation commands (e.g. "تب ۱", "تب جدید", "بستن تب", "tab 1", "new tab") if let tabDetail = handleStandaloneTab(normalized: normalized) { playConfirmationSound() VoiceCommandLogger.shared.logCommand( rawSpeech: rawText, body: body, normalized: normalized, actionCategory: "Tab Navigation", actionDetail: tabDetail, status: "SUCCESS" ) return true } // 6. System controls (volume, mute, lock, screenshot, dark mode, editing hotkeys) if let sysDetail = handleSystemControls(normalized: normalized) { playConfirmationSound() VoiceCommandLogger.shared.logCommand( rawSpeech: rawText, body: body, normalized: normalized, actionCategory: "System Control", actionDetail: sysDetail, status: "SUCCESS" ) return true } // 7. Specialized Antigravity Matcher (Instant detection of standalone variation) if let antiMatch = matchAntigravity(normalized: normalized) { launchOrActivateApp(named: antiMatch) playConfirmationSound() HistoryManager.shared.addEntry("⚡️ [App] Open \(antiMatch)") VoiceCommandLogger.shared.logCommand( rawSpeech: rawText, body: body, normalized: normalized, actionCategory: "Application Launch", actionDetail: "Launched / Focused: \(antiMatch)", status: "SUCCESS" ) return true } // 8. Match App from Persian dictionary or exact names if let appName = matchKnownApp(normalized: normalized) { launchOrActivateApp(named: appName) playConfirmationSound() HistoryManager.shared.addEntry("⚡️ [App] Open \(appName)") VoiceCommandLogger.shared.logCommand( rawSpeech: rawText, body: body, normalized: normalized, actionCategory: "Application Launch", actionDetail: "Launched / Focused: \(appName)", status: "SUCCESS" ) return true } // 9. Universal fuzzy resolver across all installed apps in /Applications and /System/Applications if let universalMatch = handleUniversalInstalledApp(query: body, normalized: normalized) { playConfirmationSound() VoiceCommandLogger.shared.logCommand( rawSpeech: rawText, body: body, normalized: normalized, actionCategory: "Universal App Match", actionDetail: "Launched / Focused: \(universalMatch)", status: "SUCCESS" ) return true } // Unrecognized Slash Command print("VoiceCommandExecutor: Unrecognized slash command body: '\(body)'") HistoryManager.shared.addEntry("⚡️ [Command] Unrecognized: \(body)") VoiceCommandLogger.shared.logCommand( rawSpeech: rawText, body: body, normalized: normalized, actionCategory: "Unrecognized", actionDetail: "No matching application, shortcut, or search trigger found", status: "UNRECOGNIZED" ) playWarningSound() return false } // MARK: - Specialized Antigravity Matcher private func matchAntigravity(normalized: String) -> String? { let hasAnti = normalized.contains("انتی") || normalized.contains("anti") || normalized.contains("on the") || normalized.contains("in the") || normalized.contains("and the") let hasGravity = normalized.contains("گراویتی") || normalized.contains("گرویتی") || normalized.contains("گریویتی") || normalized.contains("گریتی") || normalized.contains("گرایتی") || normalized.contains("گروتی") || normalized.contains("گویتی") || normalized.contains("gravity") || normalized.contains("graphity") || normalized.contains("graviti") || normalized.contains("gravty") let isFusedAntiGravity = normalized.contains("antigravity") || normalized.contains("انتیگراویتی") || normalized.contains("انتیگرویتی") || normalized.contains("انتیگریویتی") || normalized.contains("انتیگریتی") || normalized.contains("انتیگرایتی") guard (hasAnti && hasGravity) || isFusedAntiGravity else { return nil } // Strict explicit check for IDE / Editor (Black Icon App) let isIDE = normalized.contains("ای دی ای") || normalized.contains("ایدی ای") || normalized.contains("ای دی") || normalized.contains("ایدی") || normalized.contains("ایدیای") || normalized.contains("ایدیا") || normalized.contains("آیدی ای") || normalized.contains("آی دی ای") || normalized.contains("آیدی") || normalized.contains("آی دی") || normalized.contains("ide") || normalized.contains("ادیتور") || normalized.contains("کد") // If IDE is mentioned -> "Antigravity IDE" (Black icon) // If standalone / "2" -> "Antigravity" (White icon app, Antigravity 2) return isIDE ? "Antigravity IDE" : "Antigravity" } // MARK: - Web Search & Direct Navigation private func handleWebSearchAndNavigation(normalized: String, original: String) -> String? { // Google Search for p in ["سرچ", "جستجو", "search", "google"] { if normalized.hasPrefix(p) { var q = original // Clean original string from search keyword and punctuations let lowerOrig = q.lowercased() if let r = lowerOrig.range(of: p) { q = String(q[r.upperBound...]) } let trimSet = CharacterSet(charactersIn: " :.,،؛!؟?-_—\"'«»()[]{}") q = q.trimmingCharacters(in: trimSet) 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 "Google Search: \(q)" } } } // YouTube Search for p in ["یوتیوب", "youtube"] { if normalized.hasPrefix(p) { var q = original let lowerOrig = q.lowercased() if let r = lowerOrig.range(of: p) { q = String(q[r.upperBound...]) } let trimSet = CharacterSet(charactersIn: " :.,،؛!؟?-_—\"'«»()[]{}") q = q.trimmingCharacters(in: trimSet) 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 "YouTube Search: \(q)" } } } // Direct URL navigation ("برو به X" / "go to X") for p in ["برو به", "برو تو", "برو", "goto", "go to"] { if normalized.hasPrefix(p) { var target = original let lowerOrig = target.lowercased() if let r = lowerOrig.range(of: p) { target = String(target[r.upperBound...]) } let trimSet = CharacterSet(charactersIn: " :.,،؛!؟?-_—\"'«»()[]{}") target = target.trimmingCharacters(in: trimSet) 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 "Direct Navigation: \(urlStr)" } } } } return nil } // MARK: - Window Tiling & Management private func handleWindowTiling(normalized: String) -> String? { if normalized == "چپ" || normalized.contains("پنجره چپ") || normalized == "left" || normalized == "window left" { tileFrontmostWindow(action: .left) HistoryManager.shared.addEntry("⚡️ [Window] Tile Left Half") return "Tile Left Half" } if normalized == "راست" || normalized.contains("پنجره راست") || normalized == "right" || normalized == "window right" { tileFrontmostWindow(action: .right) HistoryManager.shared.addEntry("⚡️ [Window] Tile Right Half") return "Tile Right Half" } if normalized == "بالا" || normalized.contains("پنجره بالا") || normalized == "top" || normalized == "window top" { tileFrontmostWindow(action: .top) HistoryManager.shared.addEntry("⚡️ [Window] Tile Top Half") return "Tile Top Half" } if normalized == "پایین" || normalized.contains("پنجره پایین") || normalized == "bottom" || normalized == "window bottom" { tileFrontmostWindow(action: .bottom) HistoryManager.shared.addEntry("⚡️ [Window] Tile Bottom Half") return "Tile Bottom Half" } if normalized.contains("تمام صفحه") || normalized.contains("فول اسکرین") || normalized.contains("ماکزیمایز") || normalized == "maximize" || normalized == "full screen" { tileFrontmostWindow(action: .maximize) if let front = NSWorkspace.shared.frontmostApplication { if let bid = front.bundleIdentifier { WindowStateManager.shared.setPrefersFullScreen(true, for: bid) } if let name = front.localizedName { WindowStateManager.shared.setPrefersFullScreen(true, for: name) } } HistoryManager.shared.addEntry("⚡️ [Window] Maximize Window") return "Maximize Window" } if normalized.contains("خروج از تمام صفحه") || normalized.contains("پنجره عادی") || normalized == "دسکتاپ" || normalized == "پنجره" || normalized == "exit full screen" || normalized == "restore window" { exitFrontmostWindowFullScreen() if let front = NSWorkspace.shared.frontmostApplication { if let bid = front.bundleIdentifier { WindowStateManager.shared.setPrefersFullScreen(false, for: bid) } if let name = front.localizedName { WindowStateManager.shared.setPrefersFullScreen(false, for: name) } } HistoryManager.shared.addEntry("⚡️ [Window] Exit Full Screen (Desktop Mode)") return "Exit Full Screen" } if normalized == "وسط" || normalized == "مرکز" || normalized.contains("پنجره وسط") || normalized == "center" || normalized == "window center" { tileFrontmostWindow(action: .center) HistoryManager.shared.addEntry("⚡️ [Window] Center Window") return "Center Window" } if normalized == "مینیمایز" || normalized.contains("کوچک کردن") || normalized == "minimize" { pressHotkey(keyCode: 46, modifiers: .maskCommand) // Cmd+M HistoryManager.shared.addEntry("⚡️ [Window] Minimize") return "Minimize Window (Cmd+M)" } if normalized == "هاید" || normalized == "مخفی" || normalized.contains("مخفی کن") || normalized == "hide" { pressHotkey(keyCode: 4, modifiers: .maskCommand) // Cmd+H HistoryManager.shared.addEntry("⚡️ [Window] Hide App") return "Hide Frontmost App (Cmd+H)" } if normalized.contains("هاید بقیه") || normalized.contains("مخفی کردن بقیه") || normalized == "hide others" { pressHotkey(keyCode: 4, modifiers: [.maskCommand, .maskAlternate]) // Cmd+Option+H HistoryManager.shared.addEntry("⚡️ [Window] Hide Other Apps") return "Hide Other Apps (Cmd+Option+H)" } if normalized == "بستن پنجره" || normalized == "کلوز پنجره" || normalized == "close window" { pressHotkey(keyCode: 13, modifiers: .maskCommand) // Cmd+W HistoryManager.shared.addEntry("⚡️ [Window] Close Window") return "Close Window (Cmd+W)" } if normalized == "ببند" || normalized.contains("خروج از برنامه") || normalized.contains("کویت") || normalized == "quit" || normalized == "quit app" { pressHotkey(keyCode: 12, modifiers: .maskCommand) // Cmd+Q HistoryManager.shared.addEntry("⚡️ [Window] Quit App") return "Quit Application (Cmd+Q)" } return nil } 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: - App + Tab/Account Combinations private func extractTrailingOrEmbeddedNumber(from text: String) -> Int? { let t = text.lowercased() let words = t.components(separatedBy: .whitespacesAndNewlines) // 1. Direct digits (English and Persian) anywhere inside words (e.g. "تلگرام ۱", "تلگرام1", "اکانت۱") for w in words { let cleanW = w.trimmingCharacters(in: CharacterSet(charactersIn: ".,،؛:!؟?-_/\"'()[]{}«»—–")) for ch in cleanW { if let d = Int(String(ch)), d >= 1 && d <= 9 { return d } let faDigits: [Character: Int] = [ "۱": 1, "۲": 2, "۳": 3, "۴": 4, "۵": 5, "۶": 6, "۷": 7, "۸": 8, "۹": 9, "١": 1, "٢": 2, "٣": 3, "٤": 4, "٥": 5, "٦": 6, "٧": 7, "٨": 8, "٩": 9 ] if let d = faDigits[ch] { return d } } } // 2. Comprehensive Word numbers (Persian & English cardinal, ordinal, colloquial) let numberWords: [(names: [String], num: Int)] = [ (["یک", "یکی", "اول", "اولی", "اولین", "یکمی", "نخست", "ی", "وان", "one", "first"], 1), (["دو", "دومی", "دومین", "دوتا", "دوتایی", "د", "تو", "two", "second"], 2), (["سه", "سومی", "سومین", "سه تا", "سه تایی", "س", "تری", "three", "third"], 3), (["چهار", "چهارم", "چهارمی", "چهارمین", "چار", "چاری", "چارم", "چارمی", "فور", "four", "fourth"], 4), (["پنج", "پنجم", "پنجمی", "پنجمین", "پنج تا", "فایو", "five", "fifth"], 5), (["شش", "ششم", "ششمی", "ششمین", "شیش", "شیشم", "شیشمی", "شیشمین", "سیکس", "six", "sixth"], 6), (["هفت", "هفتم", "هفتمی", "هفتمین", "سون", "seven", "seventh"], 7), (["هشت", "هشتم", "هشتمی", "هشتمین", "ایت", "eight", "eighth"], 8), (["نه", "نهم", "نهمی", "نهمین", "ناین", "اخر", "آخر", "اخری", "آخری", "آخرین", "nine", "ninth", "last"], 9) ] for w in words { let cleanW = w.trimmingCharacters(in: CharacterSet(charactersIn: ".,،؛:!؟?-_/\"'()[]{}«»—–")) for item in numberWords { if item.names.contains(cleanW) { return item.num } } } return nil } private func parseAppAndNumber(normalized: String) -> (appName: String, number: Int, type: String)? { var targetApp: String? = nil var isTelegram = false var isAntigravity = false if normalized.contains("تلگرام") || normalized.contains("telegram") || normalized.contains("اکانت") || normalized.contains("account") || normalized.contains("حساب") { targetApp = "Telegram" isTelegram = true } else if normalized.contains("فایرفاکس") || normalized.contains("firefox") || normalized.contains("فایر فاکس") || normalized.contains("فایرفکس") { targetApp = "Firefox" } else if normalized.contains("کروم") || normalized.contains("chrome") { targetApp = "Google Chrome" } else if normalized.contains("سافاری") || normalized.contains("safari") { targetApp = "Safari" } else if normalized.contains("انتی") && (normalized.contains("گراویتی") || normalized.contains("گرویتی")) { isAntigravity = true if normalized.contains("ای دی ای") || normalized.contains("ایدی ای") || normalized.contains("ide") { targetApp = "Antigravity IDE" } else { targetApp = "Antigravity" } } else if normalized.contains("وی اس کد") || normalized.contains("vscode") { targetApp = "Visual Studio Code" } else if normalized.contains("ترمینال") || normalized.contains("terminal") { targetApp = "Terminal" } guard let app = targetApp else { return nil } // For Antigravity: "آنتی گرویتی ۲" / "antigravity 2" without explicit "تب"/"tab" refers to Antigravity.app (the white icon app)! if isAntigravity && !normalized.contains("تب") && !normalized.contains("tab") { return nil } guard let num = extractTrailingOrEmbeddedNumber(from: normalized) else { return nil } let type = isTelegram ? "Account" : "Tab" return (app, num, type) } private func handleAppWithTab(normalized: String) -> String? { // 1. Direct App + Number (e.g. "تلگرام ۱", "فایرفاکس ۲", "کروم ۳", "سافاری ۱", "telegram 1", "firefox 2") if let parsed = parseAppAndNumber(normalized: normalized) { let appName = parsed.appName let num = parsed.number let type = parsed.type activateApp(named: appName) { if appName == "Telegram" { self.switchToAccount(number: num) } else { self.switchToTab(number: num) } } HistoryManager.shared.addEntry("⚡️ [\(appName)] \(type) \(num)") return "\(appName) -> \(type) \(num)" } // 2. App + Named actions (e.g. "فایرفاکس تب جدید", "کروم بستن تب") var targetApp: String? = nil if normalized.contains("فایرفاکس") || normalized.contains("firefox") { targetApp = "Firefox" } else if normalized.contains("کروم") || normalized.contains("chrome") { targetApp = "Google Chrome" } else if normalized.contains("سافاری") || normalized.contains("safari") { targetApp = "Safari" } else if normalized.contains("تلگرام") || normalized.contains("telegram") { targetApp = "Telegram" } else if let anti = matchAntigravity(normalized: normalized) { targetApp = anti } guard let appName = targetApp else { return nil } if normalized.contains("تب جدید") || normalized.contains("new tab") { activateApp(named: appName) { self.pressHotkey(keyCode: 17, modifiers: .maskCommand) // Cmd+T } HistoryManager.shared.addEntry("⚡️ [Command] \(appName): New Tab") return "\(appName) -> New Tab (Cmd+T)" } else if normalized.contains("بستن تب") || normalized.contains("close tab") { activateApp(named: appName) { self.pressHotkey(keyCode: 13, modifiers: .maskCommand) // Cmd+W } HistoryManager.shared.addEntry("⚡️ [Command] \(appName): Close Tab") return "\(appName) -> Close Tab (Cmd+W)" } else if normalized.contains("ری اپن") || normalized.contains("reopen tab") || normalized.contains("باز کردن مجدد تب") { activateApp(named: appName) { self.pressHotkey(keyCode: 17, modifiers: [.maskCommand, .maskShift]) // Cmd+Shift+T } HistoryManager.shared.addEntry("⚡️ [Command] \(appName): Reopen Tab") return "\(appName) -> Reopen Tab (Cmd+Shift+T)" } return nil } public func switchToAccount(number: Int) { let keyCodes: [Int: CGKeyCode] = [ 1: 18, 2: 19, 3: 20, 4: 21, 5: 23, 6: 22, 7: 26, 8: 28, 9: 25 ] guard let code = keyCodes[number] else { return } // For Telegram Desktop: Send Cmd+[number], Cmd+Shift+[number], and Ctrl combinations (configured via shortcuts-custom.json) pressHotkey(keyCode: code, modifiers: .maskCommand) DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) { self.pressHotkey(keyCode: code, modifiers: [.maskCommand, .maskShift]) } DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { self.pressHotkey(keyCode: code, modifiers: .maskControl) } DispatchQueue.main.asyncAfter(deadline: .now() + 0.08) { self.pressHotkey(keyCode: code, modifiers: [.maskControl, .maskShift]) } } // MARK: - Standalone Tab Commands private func handleStandaloneTab(normalized: String) -> String? { if normalized.hasPrefix("تب") || normalized.hasPrefix("tab") || normalized.contains("تب ") || normalized.contains("tab ") { if let num = extractTrailingOrEmbeddedNumber(from: normalized) { switchToTab(number: num) HistoryManager.shared.addEntry("⚡️ [Command] Switch to Tab \(num)") return "Switch to Tab \(num) (Cmd+\(num))" } } if normalized.contains("تب جدید") || normalized.contains("نیو تب") || normalized.contains("new tab") { pressHotkey(keyCode: 17, modifiers: .maskCommand) // Cmd+T HistoryManager.shared.addEntry("⚡️ [Command] New Tab") return "New Tab (Cmd+T)" } if normalized.contains("بستن تب") || normalized.contains("کلوز تب") || normalized.contains("close tab") { pressHotkey(keyCode: 13, modifiers: .maskCommand) // Cmd+W HistoryManager.shared.addEntry("⚡️ [Command] Close Tab") return "Close Tab (Cmd+W)" } if normalized.contains("ری اپن") || normalized.contains("reopen tab") || normalized.contains("باز کردن مجدد تب") { pressHotkey(keyCode: 17, modifiers: [.maskCommand, .maskShift]) // Cmd+Shift+T HistoryManager.shared.addEntry("⚡️ [Command] Reopen Closed Tab") return "Reopen Closed Tab (Cmd+Shift+T)" } if normalized.contains("تب بعد") || normalized.contains("تب بعدی") || normalized.contains("next tab") { pressHotkey(keyCode: 48, modifiers: .maskControl) // Ctrl+Tab HistoryManager.shared.addEntry("⚡️ [Command] Next Tab") return "Next Tab (Ctrl+Tab)" } if normalized.contains("تب قبل") || normalized.contains("تب قبلی") || normalized.contains("prev tab") || normalized.contains("previous tab") { pressHotkey(keyCode: 48, modifiers: [.maskControl, .maskShift]) // Ctrl+Shift+Tab HistoryManager.shared.addEntry("⚡️ [Command] Previous Tab") return "Previous Tab (Ctrl+Shift+Tab)" } if normalized.contains("رفرش") || normalized.contains("ریلود") || normalized.contains("reload") || normalized.contains("refresh") { pressHotkey(keyCode: 15, modifiers: .maskCommand) // Cmd+R HistoryManager.shared.addEntry("⚡️ [Command] Refresh Page") return "Reload Page (Cmd+R)" } if normalized.contains("هارد رفرش") || normalized.contains("hard refresh") { pressHotkey(keyCode: 15, modifiers: [.maskCommand, .maskShift]) // Cmd+Shift+R HistoryManager.shared.addEntry("⚡️ [Command] Hard Refresh Page") return "Hard Refresh Page (Cmd+Shift+R)" } if normalized.contains("ادرس بار") || normalized.contains("address bar") { pressHotkey(keyCode: 37, modifiers: .maskCommand) // Cmd+L HistoryManager.shared.addEntry("⚡️ [Command] Focus Address Bar") return "Focus Address Bar (Cmd+L)" } return nil } // MARK: - System Controls private func handleSystemControls(normalized: String) -> String? { // Volume exact levels if normalized.contains("صدا ۱۰۰") || normalized.contains("صدا 100") || normalized == "volume 100" || normalized == "max volume" { runAppleScript("set volume output volume 100") HistoryManager.shared.addEntry("⚡️ [Audio] Volume 100%") return "Set Output Volume to 100%" } if normalized.contains("صدا ۵۰") || normalized.contains("صدا 50") || normalized == "volume 50" { runAppleScript("set volume output volume 50") HistoryManager.shared.addEntry("⚡️ [Audio] Volume 50%") return "Set Output Volume to 50%" } if normalized.contains("صدا صفر") || normalized.contains("صدا 0") || normalized == "volume 0" { runAppleScript("set volume output volume 0") HistoryManager.shared.addEntry("⚡️ [Audio] Volume 0%") return "Set Output Volume to 0%" } // Volume up / down if normalized.contains("صدا زیاد") || normalized.contains("افزایش صدا") || normalized.contains("volume up") { runAppleScript("set volume output volume ((output volume of (get volume settings)) + 15)") HistoryManager.shared.addEntry("⚡️ [Audio] Volume Up") return "Volume Up (+15%)" } if normalized.contains("صدا کم") || normalized.contains("کاهش صدا") || normalized.contains("volume down") { runAppleScript("set volume output volume ((output volume of (get volume settings)) - 15)") HistoryManager.shared.addEntry("⚡️ [Audio] Volume Down") return "Volume Down (-15%)" } if normalized.contains("میوت") || normalized.contains("بی صدا") || normalized == "mute" { runAppleScript("set volume output muted not (output muted of (get volume settings))") HistoryManager.shared.addEntry("⚡️ [Audio] Toggle Mute") return "Toggle Audio Mute" } // Media Play / Pause if normalized == "پلی" || normalized == "پاز" || normalized == "توقف" || normalized == "play" || normalized == "pause" { runAppleScript("tell application \"System Events\" to key code 16 using {}") HistoryManager.shared.addEntry("⚡️ [Media] Play / Pause") return "Media Play / Pause" } if normalized.contains("اهنگ بعد") || normalized == "next track" { runAppleScript("tell application \"System Events\" to key code 19 using {}") HistoryManager.shared.addEntry("⚡️ [Media] Next Track") return "Media Next Track" } if normalized.contains("اهنگ قبل") || normalized == "prev track" { runAppleScript("tell application \"System Events\" to key code 20 using {}") HistoryManager.shared.addEntry("⚡️ [Media] Previous Track") return "Media Previous Track" } // Lock Screen if normalized.contains("قفل") || normalized.contains("لاک") || normalized.contains("lock screen") || normalized == "lock" { let task = Process() task.executableURL = URL(fileURLWithPath: "/usr/bin/pmset") task.arguments = ["displaysleepnow"] try? task.run() HistoryManager.shared.addEntry("⚡️ [System] Lock Screen") return "Display Sleep / Lock Screen" } // Sleep if normalized == "اسلیپ" || normalized == "sleep" { let task = Process() task.executableURL = URL(fileURLWithPath: "/usr/bin/pmset") task.arguments = ["sleepnow"] try? task.run() HistoryManager.shared.addEntry("⚡️ [System] Sleep Mac") return "System Sleep (pmset sleepnow)" } // Screenshot (Interactive) if normalized.contains("اسکرین شات") || normalized.contains("عکس صفحه") || normalized == "screenshot" { let task = Process() task.executableURL = URL(fileURLWithPath: "/usr/sbin/screencapture") task.arguments = ["-i", "-c"] try? task.run() HistoryManager.shared.addEntry("⚡️ [System] Interactive Screenshot") return "Interactive Screenshot to Clipboard" } // Dark Mode toggle if normalized.contains("دارک مود") || normalized.contains("لایت مود") || normalized.contains("حالت شب") || normalized.contains("حالت روز") || normalized.contains("dark mode") { runAppleScript("tell application \"System Events\" to tell appearance preferences to set dark mode to not dark mode") HistoryManager.shared.addEntry("⚡️ [System] Toggle Dark Mode") return "Toggle Dark Mode Appearance" } // Editing hotkeys if normalized == "کپی" || normalized == "copy" { pressHotkey(keyCode: 8, modifiers: .maskCommand) // Cmd+C HistoryManager.shared.addEntry("⚡️ [Edit] Copy") return "Copy (Cmd+C)" } if normalized == "پیست" || normalized == "paste" { pressHotkey(keyCode: 9, modifiers: .maskCommand) // Cmd+V HistoryManager.shared.addEntry("⚡️ [Edit] Paste") return "Paste (Cmd+V)" } if normalized == "کات" || normalized == "cut" { pressHotkey(keyCode: 7, modifiers: .maskCommand) // Cmd+X HistoryManager.shared.addEntry("⚡️ [Edit] Cut") return "Cut (Cmd+X)" } if normalized.contains("انتخاب همه") || normalized.contains("سلکت ال") || normalized == "select all" { pressHotkey(keyCode: 0, modifiers: .maskCommand) // Cmd+A HistoryManager.shared.addEntry("⚡️ [Edit] Select All") return "Select All (Cmd+A)" } if normalized == "اندو" || normalized == "برگرد" || normalized == "undo" { pressHotkey(keyCode: 6, modifiers: .maskCommand) // Cmd+Z HistoryManager.shared.addEntry("⚡️ [Edit] Undo") return "Undo (Cmd+Z)" } if normalized == "ریدو" || normalized == "redo" { pressHotkey(keyCode: 6, modifiers: [.maskCommand, .maskShift]) // Cmd+Shift+Z HistoryManager.shared.addEntry("⚡️ [Edit] Redo") return "Redo (Cmd+Shift+Z)" } if normalized == "سیو" || normalized == "ذخیره" || normalized == "save" { pressHotkey(keyCode: 1, modifiers: .maskCommand) // Cmd+S HistoryManager.shared.addEntry("⚡️ [Edit] Save") return "Save (Cmd+S)" } if normalized == "فایند" || normalized == "پیدا کن" || normalized == "find" { pressHotkey(keyCode: 3, modifiers: .maskCommand) // Cmd+F HistoryManager.shared.addEntry("⚡️ [Edit] Find") return "Find (Cmd+F)" } if normalized == "اینتر" || normalized == "enter" { pressHotkey(keyCode: 36, modifiers: []) // Return HistoryManager.shared.addEntry("⚡️ [Edit] Enter") return "Press Enter Key" } if normalized.contains("پاک کردن خط") || normalized == "delete line" { pressHotkey(keyCode: 51, modifiers: .maskCommand) // Cmd+Backspace HistoryManager.shared.addEntry("⚡️ [Edit] Delete Line") return "Delete Line (Cmd+Backspace)" } return nil } // MARK: - Known Apps Matching private func matchKnownApp(normalized: String) -> String? { var query = normalized for prefix in ["باز کن ", "بازکن ", "برو به ", "برو تو ", "اجرا کن ", "open ", "launch ", "goto "] { if query.hasPrefix(prefix) { query = String(query.dropFirst(prefix.count)).trimmingCharacters(in: .whitespacesAndNewlines) break } } for (persianKey, englishAppName) in persianAppDictionary { if query == persianKey || query.contains(persianKey) { return englishAppName } } return nil } // MARK: - Universal Installed App Scanner private func handleUniversalInstalledApp(query: String, normalized: String) -> String? { var cleanQuery = query.trimmingCharacters(in: .whitespacesAndNewlines) for prefix in ["باز کن ", "بازکن ", "open ", "launch "] { if cleanQuery.lowercased().hasPrefix(prefix) { cleanQuery = String(cleanQuery.dropFirst(prefix.count)).trimmingCharacters(in: .whitespacesAndNewlines) break } } guard !cleanQuery.isEmpty else { return nil } let normalizedQuery = normalizeForMatching(cleanQuery) refreshInstalledApps() if let match = cachedInstalledApps.first(where: { $0.normalizedName == normalizedQuery }) { launchApp(at: match.url, name: match.name) return match.name } if let match = cachedInstalledApps.first(where: { $0.normalizedName.hasPrefix(normalizedQuery) }) { launchApp(at: match.url, name: match.name) return match.name } if let match = cachedInstalledApps.first(where: { $0.normalizedName.contains(normalizedQuery) || normalizedQuery.contains($0.normalizedName) }) { launchApp(at: match.url, name: match.name) return match.name } return nil } private func launchApp(at url: URL, name: String) { let ws = NSWorkspace.shared let shouldBeFullScreen = WindowStateManager.shared.prefersFullScreen(for: name) if let running = ws.runningApplications.first(where: { $0.bundleURL == url }) { running.activate(options: [.activateIgnoringOtherApps]) if shouldBeFullScreen { ensureWindowFullScreen(app: running) } } else { let conf = NSWorkspace.OpenConfiguration() conf.activates = true ws.openApplication(at: url, configuration: conf) { app, _ in if let launched = app { DispatchQueue.main.async { self.pollAndManageWindow( app: launched, targetBid: launched.bundleIdentifier, appName: name, shouldBeFullScreen: shouldBeFullScreen ) } } } } HistoryManager.shared.addEntry("⚡️ [App] Open \(name)") } private func normalizeForMatching(_ text: String) -> String { return text.lowercased() .components(separatedBy: CharacterSet.alphanumerics.inverted) .joined() } private func refreshInstalledApps() { let now = Date().timeIntervalSince1970 guard (now - lastAppsScanTime) > 60 || cachedInstalledApps.isEmpty else { return } lastAppsScanTime = now var apps: [InstalledAppInfo] = [] let searchDirs = [ URL(fileURLWithPath: "/Applications"), URL(fileURLWithPath: "/Applications/Utilities"), URL(fileURLWithPath: "/System/Applications"), URL(fileURLWithPath: "/System/Applications/Utilities"), FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("Applications") ] let fm = FileManager.default for dir in searchDirs { guard let contents = try? fm.contentsOfDirectory(at: dir, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles]) else { continue } for item in contents where item.pathExtension == "app" { let name = item.deletingPathExtension().lastPathComponent let normalized = normalizeForMatching(name) apps.append(InstalledAppInfo(name: name, normalizedName: normalized, url: item)) } } cachedInstalledApps = apps } public func switchToTab(number: Int) { let keyCodes: [Int: CGKeyCode] = [ 1: 18, 2: 19, 3: 20, 4: 21, 5: 23, 6: 22, 7: 26, 8: 28, 9: 25 ] guard let code = keyCodes[number] else { return } pressHotkey(keyCode: code, modifiers: .maskCommand) } private func pressHotkey(keyCode: CGKeyCode, modifiers: CGEventFlags) { let source = CGEventSource(stateID: .combinedSessionState) if let down = CGEvent(keyboardEventSource: source, virtualKey: keyCode, keyDown: true), let up = CGEvent(keyboardEventSource: source, virtualKey: keyCode, keyDown: false) { down.flags = modifiers up.flags = modifiers down.post(tap: .cghidEventTap) up.post(tap: .cghidEventTap) } } private func launchOrActivateApp(named appName: String, completion: (() -> Void)? = nil) { let ws = NSWorkspace.shared // 1. Precise bundle identifier map let bundleIDMap: [String: String] = [ "Antigravity": "com.google.antigravity", "Antigravity IDE": "com.google.antigravity-ide", "Telegram": "com.tdesktop.Telegram", "Firefox": "org.mozilla.firefox", "Google Chrome": "com.google.Chrome", "Safari": "com.apple.Safari" ] let targetBid = bundleIDMap[appName] let appPrefKey = targetBid ?? appName let shouldBeFullScreen = WindowStateManager.shared.prefersFullScreen(for: appPrefKey) // Check if currently running let runningApp = ws.runningApplications.first(where: { if let bid = targetBid, $0.bundleIdentifier == bid { return true } return $0.localizedName?.caseInsensitiveCompare(appName) == .orderedSame }) if let running = runningApp { // Instant path: if the app is ALREADY frontmost if running.isActive { if shouldBeFullScreen { ensureWindowFullScreen(app: running) } completion?() return } // Check if app has any open windows let appElem = AXUIElementCreateApplication(running.processIdentifier) var winListObj: CFTypeRef? let hasWindows = (AXUIElementCopyAttributeValue(appElem, kAXWindowsAttribute as CFString, &winListObj) == .success) && ((winListObj as? [AXUIElement])?.isEmpty == false) if hasWindows { running.activate(options: [.activateIgnoringOtherApps]) // Force macOS WindowServer to switch Spaces asynchronously DispatchQueue.global(qos: .userInteractive).async { let task = Process() task.executableURL = URL(fileURLWithPath: "/usr/bin/open") task.arguments = ["-a", appName] try? task.run() } if shouldBeFullScreen { ensureWindowFullScreen(app: running) } if let comp = completion { DispatchQueue.main.asyncAfter(deadline: .now() + 0.12) { comp() } } return } else { // Process is lingering with 0 windows! Re-open window via AppleScript & open -a runAppleScript("tell application \"\(appName)\" to reopen") runAppleScript("tell application \"\(appName)\" to activate") let task = Process() task.executableURL = URL(fileURLWithPath: "/usr/bin/open") task.arguments = ["-a", appName] try? task.run() DispatchQueue.main.asyncAfter(deadline: .now() + 0.08) { self.pollAndManageWindow(app: running, targetBid: targetBid, appName: appName, shouldBeFullScreen: shouldBeFullScreen, completion: completion) } return } } // Cold launch if app was closed if let bid = targetBid, let appURL = ws.urlForApplication(withBundleIdentifier: bid) { let conf = NSWorkspace.OpenConfiguration() conf.activates = true ws.openApplication(at: appURL, configuration: conf) { app, _ in DispatchQueue.main.async { self.pollAndManageWindow(app: app, targetBid: bid, appName: appName, shouldBeFullScreen: shouldBeFullScreen, completion: completion) } } return } // Fallback: launch via /usr/bin/open -a let task = Process() task.executableURL = URL(fileURLWithPath: "/usr/bin/open") task.arguments = ["-a", appName] try? task.run() DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { self.pollAndManageWindow(app: nil, targetBid: targetBid, appName: appName, shouldBeFullScreen: shouldBeFullScreen, completion: completion) } } private func activateApp(named appName: String, completion: @escaping () -> Void) { launchOrActivateApp(named: appName, completion: completion) } @discardableResult private func ensureWindowFullScreen(app: NSRunningApplication) -> Bool { let appElem = AXUIElementCreateApplication(app.processIdentifier) var windowObj: CFTypeRef? let hasWin = (AXUIElementCopyAttributeValue(appElem, kAXFocusedWindowAttribute as CFString, &windowObj) == .success && windowObj != nil) || (AXUIElementCopyAttributeValue(appElem, kAXMainWindowAttribute as CFString, &windowObj) == .success && windowObj != nil) guard hasWin, let winElem = windowObj else { var winListObj: CFTypeRef? if AXUIElementCopyAttributeValue(appElem, kAXWindowsAttribute as CFString, &winListObj) == .success, let winList = winListObj as? [AXUIElement], let firstWin = winList.first { return applyFullScreen(to: firstWin, app: app) } return false } return applyFullScreen(to: winElem as! AXUIElement, app: app) } private func applyFullScreen(to axWin: AXUIElement, app: NSRunningApplication) -> Bool { // 1. Check if already full screen var isFullObj: CFTypeRef? if AXUIElementCopyAttributeValue(axWin, "AXFullScreen" as CFString, &isFullObj) == .success, let isFull = isFullObj as? Bool, isFull { return true } // 2. Set native macOS full-screen space attribute let setRes = AXUIElementSetAttributeValue(axWin, "AXFullScreen" as CFString, kCFBooleanTrue) if setRes == .success { return true } // 3. Fallback: Zoom button (kAXFullScreenButton) var zoomObj: CFTypeRef? if AXUIElementCopyAttributeValue(axWin, "AXFullScreenButton" as CFString, &zoomObj) == .success, let btn = zoomObj { AXUIElementPerformAction(btn as! AXUIElement, kAXPressAction as CFString) return true } // 4. Fallback: Cmd+Ctrl+F shortcut app.activate(options: [.activateIgnoringOtherApps]) pressHotkey(keyCode: 3, modifiers: [.maskCommand, .maskControl]) // 'F' + Cmd + Ctrl return true } private func exitFrontmostWindowFullScreen() { guard let front = NSWorkspace.shared.frontmostApplication else { return } let appElem = AXUIElementCreateApplication(front.processIdentifier) var windowObj: CFTypeRef? let hasWin = (AXUIElementCopyAttributeValue(appElem, kAXFocusedWindowAttribute as CFString, &windowObj) == .success && windowObj != nil) || (AXUIElementCopyAttributeValue(appElem, kAXMainWindowAttribute as CFString, &windowObj) == .success && windowObj != nil) if hasWin, let winElem = windowObj { let axWin = winElem as! AXUIElement var isFullObj: CFTypeRef? if AXUIElementCopyAttributeValue(axWin, "AXFullScreen" as CFString, &isFullObj) == .success, let isFull = isFullObj as? Bool, isFull { let res = AXUIElementSetAttributeValue(axWin, "AXFullScreen" as CFString, kCFBooleanFalse) if res != .success { pressHotkey(keyCode: 3, modifiers: [.maskCommand, .maskControl]) } return } } pressHotkey(keyCode: 3, modifiers: [.maskCommand, .maskControl]) } private func pollAndManageWindow( app: NSRunningApplication?, targetBid: String?, appName: String, shouldBeFullScreen: Bool, attempt: Int = 1, completion: (() -> Void)? = nil ) { let ws = NSWorkspace.shared let target = app ?? ws.runningApplications.first(where: { if let bid = targetBid, $0.bundleIdentifier == bid { return true } return $0.localizedName?.caseInsensitiveCompare(appName) == .orderedSame }) if let target = target { target.activate(options: [.activateIgnoringOtherApps]) if shouldBeFullScreen { if ensureWindowFullScreen(app: target) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { target.activate(options: [.activateIgnoringOtherApps]) completion?() } return } } else { // Desktop mode preferred DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { target.activate(options: [.activateIgnoringOtherApps]) completion?() } return } } if attempt < 18 { DispatchQueue.main.asyncAfter(deadline: .now() + 0.08) { self.pollAndManageWindow( app: target, targetBid: targetBid, appName: appName, shouldBeFullScreen: shouldBeFullScreen, attempt: attempt + 1, completion: completion ) } } else { target?.activate(options: [.activateIgnoringOtherApps]) completion?() } } private func runAppleScript(_ script: String) { let full = "osascript -e '" + script.replacingOccurrences(of: "'", with: "'\\''") + "'" let task = Process() task.executableURL = URL(fileURLWithPath: "/bin/sh") task.arguments = ["-c", full] try? task.run() } private func playConfirmationSound() { if SonioxSettings.shared.playSounds { if !(NSSound(named: NSSound.Name("Hero"))?.play() ?? false) { NSSound(named: NSSound.Name("Glass"))?.play() } } } private func playWarningSound() { if SonioxSettings.shared.playSounds { if !(NSSound(named: NSSound.Name("Basso"))?.play() ?? false) { NSSound(named: NSSound.Name("Pop"))?.play() } } } }