|
|
|
@ -419,19 +419,44 @@ public final class VoiceCommandExecutor { |
|
|
|
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("gravity") || normalized.contains("graphity") |
|
|
|
|
|
|
|
guard (hasAnti && hasGravity) || normalized.contains("antigravity") || normalized.contains("انتیگراویتی") || normalized.contains("انتیگرویتی") else { |
|
|
|
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("ادیتور") || |
|
|
|
normalized.contains("کد") |
|
|
|
|
|
|
|
// If IDE is mentioned -> "Antigravity IDE" (Black icon) |
|
|
|
// If standalone / "2" -> "Antigravity" (White icon app, Antigravity 2) |
|
|
|
@ -640,9 +665,9 @@ public final class VoiceCommandExecutor { |
|
|
|
|
|
|
|
// 2. Word numbers (Persian & English cardinal and ordinal) |
|
|
|
let numberWords: [(names: [String], num: Int)] = [ |
|
|
|
(["یک", "اول", "نخست", "one", "first"], 1), |
|
|
|
(["دو", "دوم", "two", "second"], 2), |
|
|
|
(["سه", "سوم", "three", "third"], 3), |
|
|
|
(["یک", "اول", "نخست", "ی", "one", "first"], 1), |
|
|
|
(["دو", "دوم", "د", "two", "second"], 2), |
|
|
|
(["سه", "سوم", "س", "three", "third"], 3), |
|
|
|
(["چهار", "چهارم", "four", "fourth"], 4), |
|
|
|
(["پنج", "پنجم", "five", "fifth"], 5), |
|
|
|
(["شش", "ششم", "six", "sixth"], 6), |
|
|
|
@ -768,7 +793,7 @@ public final class VoiceCommandExecutor { |
|
|
|
guard let code = keyCodes[number] else { return } |
|
|
|
// For Telegram Desktop: Send Cmd+[number] and Cmd+Shift+[number] (configured via shortcuts-custom.json) |
|
|
|
pressHotkey(keyCode: code, modifiers: .maskCommand) |
|
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.12) { |
|
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) { |
|
|
|
self.pressHotkey(keyCode: code, modifiers: [.maskCommand, .maskShift]) |
|
|
|
} |
|
|
|
} |
|
|
|
@ -1080,15 +1105,13 @@ public final class VoiceCommandExecutor { |
|
|
|
} |
|
|
|
|
|
|
|
private func pressHotkey(keyCode: CGKeyCode, modifiers: CGEventFlags) { |
|
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.08) { |
|
|
|
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) |
|
|
|
} |
|
|
|
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) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
@ -1114,6 +1137,13 @@ public final class VoiceCommandExecutor { |
|
|
|
}) |
|
|
|
|
|
|
|
if let running = runningApp { |
|
|
|
// Instant path: if the app is ALREADY frontmost (e.g. user is in Telegram switching accounts) |
|
|
|
if running.isActive { |
|
|
|
ensureWindowFullScreen(app: running) |
|
|
|
completion?() |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
// Check if app has any open windows |
|
|
|
let appElem = AXUIElementCreateApplication(running.processIdentifier) |
|
|
|
var winListObj: CFTypeRef? |
|
|
|
@ -1121,18 +1151,22 @@ public final class VoiceCommandExecutor { |
|
|
|
((winListObj as? [AXUIElement])?.isEmpty == false) |
|
|
|
|
|
|
|
if hasWindows { |
|
|
|
// Force macOS WindowServer to switch Spaces |
|
|
|
runAppleScript("tell application \"\(appName)\" to activate") |
|
|
|
let task = Process() |
|
|
|
task.executableURL = URL(fileURLWithPath: "/usr/bin/open") |
|
|
|
task.arguments = ["-a", appName] |
|
|
|
try? task.run() |
|
|
|
|
|
|
|
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() |
|
|
|
} |
|
|
|
|
|
|
|
ensureWindowFullScreen(app: running) |
|
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { |
|
|
|
running.activate(options: [.activateIgnoringOtherApps]) |
|
|
|
completion?() |
|
|
|
|
|
|
|
if let comp = completion { |
|
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.03) { |
|
|
|
comp() |
|
|
|
} |
|
|
|
} |
|
|
|
return |
|
|
|
} else { |
|
|
|
@ -1144,7 +1178,7 @@ public final class VoiceCommandExecutor { |
|
|
|
task.arguments = ["-a", appName] |
|
|
|
try? task.run() |
|
|
|
|
|
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { |
|
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.08) { |
|
|
|
self.pollAndMakeFullScreen(app: running, targetBid: targetBid, appName: appName, completion: completion) |
|
|
|
} |
|
|
|
return |
|
|
|
|