Skip to content

Commit

Permalink
feat: improve windows detection
Browse files Browse the repository at this point in the history
  • Loading branch information
lwouis committed May 31, 2022
1 parent 1653c16 commit de0497a
Show file tree
Hide file tree
Showing 7 changed files with 88 additions and 65 deletions.
4 changes: 1 addition & 3 deletions src/api-wrappers/AXUIElement.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,6 @@ extension AXUIElement {
AXUIElementSetMessagingTimeout(AXUIElementCreateSystemWide(), globalTimeoutInSeconds + 5)
}

static let normalLevel = CGWindowLevelForKey(.normalWindow)

func axCallWhichCanThrow<T>(_ result: AXError, _ successValue: inout T) throws -> T? {
switch result {
case .success: return successValue
Expand Down Expand Up @@ -87,7 +85,7 @@ extension AXUIElement {
size != nil && size!.width > 100 && size!.height > 100 &&
(books(runningApp) || keynote(runningApp) || iina(runningApp) || (
// CGWindowLevel == .normalWindow helps filter out iStats Pro and other top-level pop-overs, and floating windows
level == AXUIElement.normalLevel &&
level == CGWindow.normalLevel &&
jetbrainApp(runningApp, title, subrole) &&
([kAXStandardWindowSubrole, kAXDialogSubrole].contains(subrole) ||
openBoard(runningApp) ||
Expand Down
2 changes: 2 additions & 0 deletions src/api-wrappers/CGWindow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import Cocoa
typealias CGWindow = [CFString: Any]

extension CGWindow {
static let normalLevel = CGWindowLevelForKey(.normalWindow)

static func windows(_ option: CGWindowListOption) -> [CGWindow] {
return CGWindowListCopyWindowInfo([.excludeDesktopElements, option], kCGNullWindowID) as! [CGWindow]
}
Expand Down
28 changes: 27 additions & 1 deletion src/api-wrappers/PrivateApis.swift
Original file line number Diff line number Diff line change
Expand Up @@ -110,11 +110,30 @@ func CGSGetConnectionPSN(_ cid: CGSConnectionID, _ psn: inout ProcessSerialNumbe
@_silgen_name("CGSCopyManagedDisplaySpaces")
func CGSCopyManagedDisplaySpaces(_ cid: CGSConnectionID) -> CFArray

struct CGSCopyWindowsOptions: OptionSet {
let rawValue: Int
static let minimized = CGSCopyWindowsOptions(rawValue: 1 << 0)
static let screenSaverLevel1000 = CGSCopyWindowsOptions(rawValue: 1 << 1)
static let minimized2 = CGSCopyWindowsOptions(rawValue: 1 << 2)
static let unknown1 = CGSCopyWindowsOptions(rawValue: 1 << 3)
static let unknown2 = CGSCopyWindowsOptions(rawValue: 1 << 4)
static let desktopIconWindowLevel2147483603 = CGSCopyWindowsOptions(rawValue: 1 << 5)
}

struct CGSCopyWindowsTags: OptionSet {
let rawValue: Int
static let level0 = CGSCopyWindowsTags(rawValue: 1 << 0)
static let noTitleMaybePopups = CGSCopyWindowsTags(rawValue: 1 << 1)
static let unknown1 = CGSCopyWindowsTags(rawValue: 1 << 2)
static let mainMenuWindowAndDesktopIconWindow = CGSCopyWindowsTags(rawValue: 1 << 3)
static let unknown2 = CGSCopyWindowsTags(rawValue: 1 << 4)
}

// returns an array of window IDs (as UInt32) for the space(s) provided as `spaces`
// the elements of the array are ordered by the z-index order of the windows in each space, with some exceptions where spaces mix
// * macOS 10.10+
@_silgen_name("CGSCopyWindowsWithOptionsAndTags")
func CGSCopyWindowsWithOptionsAndTags(_ cid: CGSConnectionID, _ owner: UInt32, _ spaces: CFArray, _ options: UInt32, _ setTags: inout UInt64, _ clearTags: inout UInt64) -> CFArray
func CGSCopyWindowsWithOptionsAndTags(_ cid: CGSConnectionID, _ owner: Int, _ spaces: CFArray, _ options: Int, _ setTags: inout Int, _ clearTags: inout Int) -> CFArray

// returns the current space ID on the provided display UUID
// * macOS 10.10+
Expand Down Expand Up @@ -217,6 +236,13 @@ enum CGSSpaceType: Int {
func CGSSpaceGetType(_ cid: CGSConnectionID, _ sid: CGSSpaceID) -> CGSSpaceType


// move a window to a Space; works with fullscreen windows
// with fullscreen window, sending it back to its original state later seems to mess with macOS internals. The Space appears fully black
// this API seems unreliable to use
// the last param seem to work with 0x80007; not sure what it means
// * macOS 10.10-12.2
@_silgen_name("CGSSpaceAddWindowsAndRemoveFromSpaces")
func CGSSpaceAddWindowsAndRemoveFromSpaces(_ cid: CGSConnectionID, _ sid: CGSSpaceID, _ wid: NSArray, _ notSure: Int) -> Void

// ------------------------------------------------------------
// below are some notes on some private APIs I experimented with
Expand Down
94 changes: 45 additions & 49 deletions src/logic/Application.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,29 +13,27 @@ class Application: NSObject {
var icon: NSImage?
var dockLabel: String?
var pid: pid_t!
var wasLaunchedBeforeAltTab = false
var focusedWindow: Window? = nil

init(_ runningApplication: NSRunningApplication, _ wasLaunchedBeforeAltTab: Bool = false) {
init(_ runningApplication: NSRunningApplication) {
self.runningApplication = runningApplication
self.wasLaunchedBeforeAltTab = wasLaunchedBeforeAltTab
pid = runningApplication.processIdentifier
super.init()
isHidden = runningApplication.isHidden
hasBeenActiveOnce = runningApplication.isActive
icon = runningApplication.icon
addAndObserveWindows()
observeEventsIfEligible()
kvObservers = [
runningApplication.observe(\.isFinishedLaunching, options: [.new]) { [weak self] _, _ in
guard let self = self else { return }
self.addAndObserveWindows()
self.observeEventsIfEligible()
},
runningApplication.observe(\.activationPolicy, options: [.new]) { [weak self] _, _ in
guard let self = self else { return }
if self.runningApplication.activationPolicy != .regular {
self.removeWindowslessAppWindow()
}
self.addAndObserveWindows()
self.observeEventsIfEligible()
},
]
}
Expand All @@ -51,7 +49,7 @@ class Application: NSObject {
}
}

func addAndObserveWindows() {
func observeEventsIfEligible() {
if runningApplication.activationPolicy != .prohibited && axUiElement == nil {
axUiElement = AXUIElementCreateApplication(pid)
AXObserverCreate(pid, axObserverCallback, &axObserver)
Expand All @@ -60,49 +58,46 @@ class Application: NSObject {
}
}

func observeNewWindows(_ group: DispatchGroup? = nil) {
if runningApplication.isFinishedLaunching && runningApplication.activationPolicy != .prohibited {
retryAxCallUntilTimeout(group, 5) { [weak self] in
guard let self = self else { return }
if let axWindows_ = try self.axUiElement!.windows(), axWindows_.count > 0 {
// bug in macOS: sometimes the OS returns multiple duplicate windows (e.g. Mail.app starting at login)
let axWindows = try Array(Set(axWindows_)).compactMap {
if let wid = try $0.cgWindowId() {
let title = try $0.title()
let subrole = try $0.subrole()
let role = try $0.role()
let size = try $0.size()
let level = try wid.level()
if AXUIElement.isActualWindow(self.runningApplication, wid, level, title, subrole, role, size) {
return ($0, wid, title, try $0.isFullscreen(), try $0.isMinimized(), try $0.position(), size)
}
func manuallyUpdateWindows(_ group: DispatchGroup? = nil) {
// TODO: this method manually checks windows, but will not find windows on other Spaces
retryAxCallUntilTimeout(group, 5) { [weak self] in
guard let self = self else { return }
if let axWindows_ = try self.axUiElement!.windows(), axWindows_.count > 0 {
// bug in macOS: sometimes the OS returns multiple duplicate windows (e.g. Mail.app starting at login)
let axWindows = try Array(Set(axWindows_)).compactMap {
if let wid = try $0.cgWindowId() {
let title = try $0.title()
let subrole = try $0.subrole()
let role = try $0.role()
let size = try $0.size()
let level = try wid.level()
if AXUIElement.isActualWindow(self.runningApplication, wid, level, title, subrole, role, size) {
return ($0, wid, title, try $0.isFullscreen(), try $0.isMinimized(), try $0.position(), size)
}
return nil
} as [(AXUIElement, CGWindowID, String?, Bool, Bool, CGPoint?, CGSize?)]
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
var windows = self.addWindows(axWindows)
if let window = self.addWindowslessAppsIfNeeded() {
windows.append(contentsOf: window)
}
App.app.refreshOpenUi(windows)
}
} else {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
let window = self.addWindowslessAppsIfNeeded()
App.app.refreshOpenUi(window)
}
if group == nil && !self.wasLaunchedBeforeAltTab && (
// workaround: opening an app while the active app is fullscreen; we wait out the space transition animation
CGSSpaceGetType(cgsMainConnectionId, Spaces.currentSpaceId) == .fullscreen ||
// workaround: some apps launch but have no window ready instantly. It's very unlikely an app would launch with no window
// so we retry until timeout, in those rare cases (e.g. Bear.app)
// we only do this for active app, to avoid wasting CPU, with the trade-off of maybe missing some windows
self.runningApplication.isActive
) {
throw AxError.runtimeError
return nil
} as [(AXUIElement, CGWindowID, String?, Bool, Bool, CGPoint?, CGSize?)]
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
var windows = self.addWindows(axWindows)
if let window = self.addWindowslessAppsIfNeeded() {
windows.append(contentsOf: window)
}
App.app.refreshOpenUi(windows)
}
} else {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
let window = self.addWindowslessAppsIfNeeded()
App.app.refreshOpenUi(window)
}
// workaround: some apps launch but take a while to create their window(s)
// initial windows don't trigger a windowCreated notification, so we won't get notified
// it's very unlikely an app would launch with no initial window
// so we retry until timeout, in those rare cases (e.g. Bear.app)
// we only do this for active app, to avoid wasting CPU, with the trade-off of maybe missing some windows
if self.runningApplication.isActive {
throw AxError.runtimeError
}
}
}
Expand Down Expand Up @@ -151,10 +146,11 @@ class Application: NSObject {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
// some apps have `isFinishedLaunching == true` but are actually not finished, and will return .cannotComplete
// we consider them ready when the first subscription succeeds, and list their windows again at that point
// we consider them ready when the first subscription succeeds
// windows opened before that point won't send a notification, so check those windows manually here
if !self.isReallyFinishedLaunching {
self.isReallyFinishedLaunching = true
self.observeNewWindows()
self.manuallyUpdateWindows()
}
}
}, self.runningApplication)
Expand Down
16 changes: 8 additions & 8 deletions src/logic/Applications.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import ApplicationServices
class Applications {
static var list = [Application]()

static func observeNewWindowsBlocking() {
static func manuallyUpdateWindowsFor2s() {
let group = DispatchGroup()
for app in list {
app.wasLaunchedBeforeAltTab = true
guard app.runningApplication.isFinishedLaunching else { continue }
app.observeNewWindows(group)
if app.runningApplication.isFinishedLaunching && app.runningApplication.activationPolicy != .prohibited {
app.manuallyUpdateWindows(group)
}
}
_ = group.wait(wallTimeout: .now() + .seconds(2))
}
Expand All @@ -21,7 +21,7 @@ class Applications {
}

static func addInitialRunningApplications() {
addRunningApplications(NSWorkspace.shared.runningApplications, true)
addRunningApplications(NSWorkspace.shared.runningApplications)
}

static func addInitialRunningApplicationsWindows() {
Expand All @@ -33,16 +33,16 @@ class Applications {
if windowsOnlyOnOtherSpaces.count > 0 {
// on initial launch, we use private APIs to bring windows from other spaces into the current space, observe them, then remove them from the current space
CGSAddWindowsToSpaces(cgsMainConnectionId, windowsOnlyOnOtherSpaces as NSArray, [Spaces.currentSpaceId])
Applications.observeNewWindowsBlocking()
Applications.manuallyUpdateWindowsFor2s()
CGSRemoveWindowsFromSpaces(cgsMainConnectionId, windowsOnlyOnOtherSpaces as NSArray, [Spaces.currentSpaceId])
}
}
}

static func addRunningApplications(_ runningApps: [NSRunningApplication], _ wasLaunchedBeforeAltTab: Bool = false) {
static func addRunningApplications(_ runningApps: [NSRunningApplication]) {
runningApps.forEach {
if isActualApplication($0) {
Applications.list.append(Application($0, wasLaunchedBeforeAltTab))
Applications.list.append(Application($0))
}
}
}
Expand Down
7 changes: 4 additions & 3 deletions src/logic/Spaces.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,10 @@ class Spaces {
}

static func windowsInSpaces(_ spaceIds: [CGSSpaceID]) -> [CGWindowID] {
var set_tags = UInt64(0)
var clear_tags = UInt64(0)
return CGSCopyWindowsWithOptionsAndTags(cgsMainConnectionId, 0, spaceIds as CFArray, 2, &set_tags, &clear_tags) as! [CGWindowID]
let options = ([.minimized] as CGSCopyWindowsOptions).rawValue
var set_tags = ([] as CGSCopyWindowsTags).rawValue
var clear_tags = ([] as CGSCopyWindowsTags).rawValue
return CGSCopyWindowsWithOptionsAndTags(cgsMainConnectionId, 0, spaceIds as CFArray, options, &set_tags, &clear_tags) as! [CGWindowID]
}

static func isSingleSpace() -> Bool {
Expand Down
2 changes: 1 addition & 1 deletion src/logic/events/AccessibilityEvents.swift
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ fileprivate func focusedWindowChanged(_ element: AXUIElement, _ pid: pid_t) thro
// these apps report isHidden=false, don't generate windowCreated events initially, and have a delay before their windows are created
// our only recourse is to manually check their windows once they emit
if (!app.hasBeenActiveOnce) {
app.observeNewWindows()
app.manuallyUpdateWindows()
}
}
}
Expand Down

0 comments on commit de0497a

Please sign in to comment.