Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Activate applications and increase delays #541

Merged
merged 2 commits into from
Jun 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions App/Sources/Core/Extensions/Task+timeout.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import Foundation

extension Task where Failure == Error {
// Start a new Task with a timeout. If the timeout expires before the operation is
// completed then the task is cancelled and an error is thrown.
init(priority: TaskPriority? = nil, timeout: TimeInterval, operation: @escaping @Sendable () async throws -> Success) {
self = Task(priority: priority) {
try await withThrowingTaskGroup(of: Success.self) { group -> Success in
group.addTask(operation: operation)
group.addTask {
try await _Concurrency.Task.sleep(nanoseconds: UInt64(timeout * 1_000_000_000))
throw TimeoutError()
}
guard let success = try await group.next() else {
throw _Concurrency.CancellationError()
}
group.cancelAll()
return success
}
}
}
}

private struct TimeoutError: LocalizedError {
var errorDescription: String? = "Task timed out before completion"
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ protocol ApplicationCommandRunnerDelegate: AnyObject {
final class ApplicationCommandRunner: @unchecked Sendable {
private struct Plugins {
let activate: ActivateApplicationPlugin
let addToStage: AddToStagePlugin
let bringToFront: BringToFrontApplicationPlugin
let close: CloseApplicationPlugin
let hide: HideApplicationPlugin
Expand All @@ -26,6 +27,7 @@ final class ApplicationCommandRunner: @unchecked Sendable {
self.workspace = workspace
self.plugins = Plugins(
activate: ActivateApplicationPlugin(),
addToStage: AddToStagePlugin(),
bringToFront: BringToFrontApplicationPlugin(scriptCommandRunner),
close: CloseApplicationPlugin(workspace: workspace),
hide: HideApplicationPlugin(workspace: workspace, userSpace: .shared),
Expand Down Expand Up @@ -86,7 +88,7 @@ final class ApplicationCommandRunner: @unchecked Sendable {
} else {

if command.modifiers.contains(.addToStage) {
if try await AddToStagePlugin.execute(command) {
if try await plugins.addToStage.execute(command) {
return
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,49 +9,68 @@ enum AddToStagePluginError: Error {
}

final class AddToStagePlugin {
static func execute(_ command: ApplicationCommand) async throws -> Bool {
func execute(_ command: ApplicationCommand) async throws -> Bool {
var snapshot = await UserSpace.shared.snapshot(resolveUserEnvironment: false, refreshWindows: true)

// Check if the application is already running.
if resolveRunningApplication(command.application) == nil {
let configuration = NSWorkspace.OpenConfiguration()
let url = URL(fileURLWithPath: command.application.path)

_ = try await NSWorkspace.shared.openApplication(at: url, configuration: configuration)

let frontMostUrl = URL(fileURLWithPath: snapshot.frontMostApplication.asApplication().path)
_ = try await NSWorkspace.shared.openApplication(at: frontMostUrl, configuration: configuration)
snapshot.frontMostApplication.ref.activate(options: .activateIgnoringOtherApps)

DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
Task {
try await AddToStagePlugin.execute(command)
if Self.resolveRunningApplication(command.application) == nil {
try await Self.activateTargetApplication(command)
try await Self.activateCurrentApplication(snapshot)

let result = try await Task(timeout: 5) {
var result: Bool = false
while result == false {
if Self.resolveRunningApplication(command.application) != nil {
result = true
}
try await Task.sleep(for: .milliseconds(100))
}
}
return result
}.value

return true
try await Self.activateCurrentApplication(snapshot)
snapshot = await UserSpace.shared.snapshot(resolveUserEnvironment: false, refreshWindows: true)
}

guard let runningApplication = resolveRunningApplication(command.application) else {
guard let runningApplication = Self.resolveRunningApplication(command.application) else {
return false
}

if runningApplication.isHidden {
runningApplication.unhide()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
Task {
try await AddToStagePlugin.execute(command)
_ = runningApplication.unhide()
let result = try await Task(timeout: 1) {
while runningApplication.isHidden {
try await Task.sleep(for: .milliseconds(100))
}
}
return true
return true
}.value
snapshot = await UserSpace.shared.snapshot(resolveUserEnvironment: false, refreshWindows: true)
}

let app = AppAccessibilityElement(runningApplication.processIdentifier)
guard let axWindow = try app.windows().first(where: { ($0.frame?.height ?? 0) > 20 }),
var window = resolveWindow(withId: axWindow.id, snapshot: snapshot) else {
return false
var axWindow = try app.windows().first(where: { ($0.frame?.height ?? 0) > 20 })

if axWindow == nil {
try await Self.activateTargetApplication(command)
try await Self.activateCurrentApplication(snapshot)
let newWindow = try await Task(timeout: 2) {
var window: WindowAccessibilityElement?
while window == nil {
window = try? app.windows().first(where: { ($0.frame?.height ?? 0) > 20 })
}
return window
}.value

guard let newWindow else { return false }

axWindow = newWindow
try await Self.activateCurrentApplication(snapshot)
try await Task.sleep(for: .seconds(1))
await snapshot = UserSpace.shared.snapshot(resolveUserEnvironment: false, refreshWindows: true)
}

guard let axWindow, var window = Self.resolveWindow(withId: axWindow.id, snapshot: snapshot) else { return false }

let isInStage = axWindow.frame == window.rect

if isInStage {
Expand All @@ -60,13 +79,13 @@ final class AddToStagePlugin {

let mouseLocation = CGEvent(source: nil)?.location
if window.rect.origin.x < 0 {
window = try await findWindowOnLeft(window, axWindow: axWindow, snapshot: &snapshot)
window = try await Self.findWindowOnLeft(window, axWindow: axWindow, snapshot: &snapshot)
} else if window.rect.origin.x + window.rect.width > NSScreen.main!.frame.size.width {
window = try await findWindowOnRight(window, axWindow: axWindow, snapshot: &snapshot)
window = try await Self.findWindowOnRight(window, axWindow: axWindow, snapshot: &snapshot)
}

performClick(on: window, mouseDown: .leftMouseDown,
mouseUp: .leftMouseUp, withFlags: .maskShift)
Self.performClick(on: window, mouseDown: .leftMouseDown,
mouseUp: .leftMouseUp, withFlags: .maskShift)

axWindow.isMinimized = false
axWindow.performAction(.raise)
Expand All @@ -84,6 +103,19 @@ final class AddToStagePlugin {
return true
}

static func activateCurrentApplication(_ snapshot: UserSpace.Snapshot) async throws {
let configuration = NSWorkspace.OpenConfiguration()
let url = URL(fileURLWithPath: snapshot.frontMostApplication.asApplication().path)
_ = try await NSWorkspace.shared.openApplication(at: url, configuration: configuration)
snapshot.frontMostApplication.ref.activate(options: .activateIgnoringOtherApps)
}

static func activateTargetApplication(_ command: ApplicationCommand) async throws {
let configuration = NSWorkspace.OpenConfiguration()
let url = URL(fileURLWithPath: command.application.path)
_ = try await NSWorkspace.shared.openApplication(at: url, configuration: configuration)
}

static func performClick(on window: WindowModel, mouseDown: CGEventType, mouseUp: CGEventType, withFlags flags: CGEventFlags?) {
let mouseEventDown = CGEvent(
mouseEventSource: nil,
Expand Down Expand Up @@ -156,8 +188,9 @@ final class AddToStagePlugin {
}

static func resolveRunningApplication(_ application: Application) -> NSRunningApplication? {
NSWorkspace.shared.runningApplications.first(where: { runningApplication in
runningApplication.bundleIdentifier == application.bundleIdentifier
return NSWorkspace.shared.runningApplications.first(where: { runningApplication in
runningApplication.bundleIdentifier == application.bundleIdentifier &&
runningApplication.isFinishedLaunching
})
}
}
Loading