وویس تایپینگ سانی اوکس مک
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

1396 lines
64 KiB

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