|
|
|
@ -1022,10 +1022,17 @@ public final class VoiceCommandExecutor { |
|
|
|
let ws = NSWorkspace.shared |
|
|
|
if let running = ws.runningApplications.first(where: { $0.bundleURL == url }) { |
|
|
|
running.activate(options: [.activateIgnoringOtherApps]) |
|
|
|
ensureWindowFullScreen(app: running) |
|
|
|
} else { |
|
|
|
let conf = NSWorkspace.OpenConfiguration() |
|
|
|
conf.activates = true |
|
|
|
ws.openApplication(at: url, configuration: conf, completionHandler: nil) |
|
|
|
ws.openApplication(at: url, configuration: conf) { app, _ in |
|
|
|
if let launched = app { |
|
|
|
DispatchQueue.main.async { |
|
|
|
self.pollAndMakeFullScreen(app: launched, targetBid: launched.bundleIdentifier, appName: name) |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
HistoryManager.shared.addEntry("⚡️ [App] Open \(name)") |
|
|
|
} |
|
|
|
@ -1084,10 +1091,10 @@ public final class VoiceCommandExecutor { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private func launchOrActivateApp(named appName: String) { |
|
|
|
private func launchOrActivateApp(named appName: String, completion: (() -> Void)? = nil) { |
|
|
|
let ws = NSWorkspace.shared |
|
|
|
|
|
|
|
// 1. Precise bundle identifier map (prevents "Antigravity" substring matching "Antigravity IDE") |
|
|
|
// 1. Precise bundle identifier map |
|
|
|
let bundleIDMap: [String: String] = [ |
|
|
|
"Antigravity": "com.google.antigravity", |
|
|
|
"Antigravity IDE": "com.google.antigravity-ide", |
|
|
|
@ -1097,40 +1104,135 @@ public final class VoiceCommandExecutor { |
|
|
|
"Safari": "com.apple.Safari" |
|
|
|
] |
|
|
|
|
|
|
|
if let bid = bundleIDMap[appName] { |
|
|
|
// Check currently running app by exact bundleIdentifier |
|
|
|
if let running = ws.runningApplications.first(where: { $0.bundleIdentifier == bid }) { |
|
|
|
let targetBid = bundleIDMap[appName] |
|
|
|
|
|
|
|
// 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 { |
|
|
|
running.activate(options: [.activateIgnoringOtherApps]) |
|
|
|
ensureWindowFullScreen(app: running) |
|
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { |
|
|
|
running.activate(options: [.activateIgnoringOtherApps]) |
|
|
|
completion?() |
|
|
|
} |
|
|
|
return |
|
|
|
} |
|
|
|
// If not running, launch via exact URL |
|
|
|
if let appURL = ws.urlForApplication(withBundleIdentifier: bid) { |
|
|
|
|
|
|
|
// 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, completionHandler: nil) |
|
|
|
return |
|
|
|
ws.openApplication(at: appURL, configuration: conf) { app, _ in |
|
|
|
DispatchQueue.main.async { |
|
|
|
self.pollAndMakeFullScreen(app: app, targetBid: bid, appName: appName, completion: completion) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 2. Strict EXACT localized name match (NOT contains!) |
|
|
|
if let running = ws.runningApplications.first(where: { |
|
|
|
$0.localizedName?.caseInsensitiveCompare(appName) == .orderedSame |
|
|
|
}) { |
|
|
|
running.activate(options: [.activateIgnoringOtherApps]) |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
// 3. Fallback: launch via /usr/bin/open -a with exact app name |
|
|
|
// 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.pollAndMakeFullScreen(app: nil, targetBid: targetBid, appName: appName, completion: completion) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private func activateApp(named appName: String, completion: @escaping () -> Void) { |
|
|
|
launchOrActivateApp(named: appName) |
|
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.22) { |
|
|
|
completion() |
|
|
|
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 pollAndMakeFullScreen( |
|
|
|
app: NSRunningApplication?, |
|
|
|
targetBid: String?, |
|
|
|
appName: String, |
|
|
|
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 ensureWindowFullScreen(app: target) { |
|
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { |
|
|
|
target.activate(options: [.activateIgnoringOtherApps]) |
|
|
|
completion?() |
|
|
|
} |
|
|
|
return |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if attempt < 18 { |
|
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { |
|
|
|
self.pollAndMakeFullScreen( |
|
|
|
app: target, |
|
|
|
targetBid: targetBid, |
|
|
|
appName: appName, |
|
|
|
attempt: attempt + 1, |
|
|
|
completion: completion |
|
|
|
) |
|
|
|
} |
|
|
|
} else { |
|
|
|
target?.activate(options: [.activateIgnoringOtherApps]) |
|
|
|
completion?() |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|