From 4de20c812d8c6c56076572a11218c1f52510c1f7 Mon Sep 17 00:00:00 2001 From: Hugo Mallinson Date: Fri, 27 Feb 2026 13:44:32 +0000 Subject: [PATCH] Added --window option to target keypresses at a specific window. --- .../Configuration/SendConfig.swift | 4 ++- Sources/SendKeysLib/Sender.swift | 28 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/Sources/SendKeysLib/Configuration/SendConfig.swift b/Sources/SendKeysLib/Configuration/SendConfig.swift index 829c2df..968260a 100644 --- a/Sources/SendKeysLib/Configuration/SendConfig.swift +++ b/Sources/SendKeysLib/Configuration/SendConfig.swift @@ -7,11 +7,12 @@ struct SendConfig: Codable { var remap: [String: String]? var targeted: Bool? var terminateCommand: String? + var window: String? init( activate: Bool? = nil, animationInterval: Double? = nil, delay: Double? = nil, initialDelay: Double? = nil, keyboardLayout: KeyMappings.Layouts? = nil, remap: [String: String]? = nil, targeted: Bool? = nil, - terminateCommand: String? = nil + terminateCommand: String? = nil, window: String? = nil ) { self.activate = activate self.animationInterval = animationInterval @@ -21,6 +22,7 @@ struct SendConfig: Codable { self.remap = remap self.targeted = targeted self.terminateCommand = terminateCommand + self.window = window } func merge(with other: SendConfig?) -> SendConfig { diff --git a/Sources/SendKeysLib/Sender.swift b/Sources/SendKeysLib/Sender.swift index 4e35f14..a722955 100644 --- a/Sources/SendKeysLib/Sender.swift +++ b/Sources/SendKeysLib/Sender.swift @@ -17,6 +17,10 @@ public struct Sender: ParsableCommand { help: "Process id of a running application to send keys to.") var processId: Int? + @Option( + name: NameSpecification([.customLong("window")]), help: "Name of the window to send keystrokes to.") + var targetWindow: String? + @Flag( name: .long, inversion: FlagInversion.prefixedNo, help: "Activate the specified app or process before sending commands.") @@ -106,6 +110,30 @@ public struct Sender: ParsableCommand { if app == nil { throw RuntimeError("Application could not be found.") } + if targetWindow != nil { + let pid = app!.processIdentifier + let appRef = AXUIElementCreateApplication(pid) + var windowList: CFTypeRef? + let result = AXUIElementCopyAttributeValue(appRef, kAXWindowsAttribute as CFString, &windowList) + guard result == .success, let windows = windowList as? [AXUIElement] else { + throw RuntimeError("Failed to retrieve windows.") + } + var foundWindow: Bool = false + for window in windows { + var title: CFTypeRef? + AXUIElementCopyAttributeValue(window, kAXTitleAttribute as CFString, &title) + + if let titleString = title as? String, titleString.starts(with: targetWindow ?? "") { + AXUIElementSetAttributeValue(window, kAXMainAttribute as CFString, kCFBooleanTrue) + AXUIElementSetAttributeValue(window, kAXFocusedAttribute as CFString, kCFBooleanTrue) + foundWindow = true + break + } + } + if !foundWindow { + throw RuntimeError("Target window could not be found.") + } + } keyPresser = KeyPresser(app: app) } else { keyPresser = KeyPresser(app: nil)