Skip to content

Commit

Permalink
refactor: complete rework of the internals
Browse files Browse the repository at this point in the history
closes #93 closes #24

BREAKING CHANGE: Instead of asking the OS about the state of the whole system on trigger (what we do today; hard to do fast), or asking the state of the whole system on a timer (what HyperSwitch does today; inaccurate) - instead of one of 2 approaches, v3 observes the Accessibility events such as "an app was launched", "a window was closed". This means we build a cache as we receive these events in the background, and when the user trigger the app, we can show accurate state of the windows instantly.

Of course there is no free lunch, so this approach has its own issues. However from my work on it from the past week, I'm very optimistic! The thing I'm the most excited about actually is not the perf (because on my machine even v2 is instant; I have a recent macbook and no 4k displays), but the fact that we will finally have the thumbnails in order of recently-used to least-recently-used, instead of the order of their stack (z-index) on the desktop. It's a big difference! There are many more limitations that are no longer applying also with this approach.

More context: #45 (comment)
  • Loading branch information
louis.pontoise committed Jan 9, 2020
1 parent 5b8a911 commit 3a7f20c
Show file tree
Hide file tree
Showing 22 changed files with 611 additions and 352 deletions.
36 changes: 24 additions & 12 deletions alt-tab-macos.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@
4807A6C623A9CD190052A53E /* SkyLight.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4807A6C523A9CD190052A53E /* SkyLight.framework */; };
D04BA02DD4152997C32CF50B /* StatusItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA0AF7C5DCF367FBB663C /* StatusItem.swift */; };
D04BA0496ACF1427B6E9D369 /* CGWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA78E3B4E73B40DB77174 /* CGWindow.swift */; };
D04BA1BA0B3F2E0A47883569 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAF13DFAA6930676D0492 /* Application.swift */; };
D04BA20D4A240843293B3B52 /* Cell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA56355579F78776E6D51 /* Cell.swift */; };
D04BA278D9EFA568C8D18A4C /* TrackedWindows.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAD1BED44EAEB77FED8A4 /* TrackedWindows.swift */; };
D04BA2CBF0EFA04CC80EC1BC /* TrackedWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAE80772D25834E440975 /* TrackedWindow.swift */; };
D04BA2378832FD7E5DE3BC23 /* Applications.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA66B5B4143D2238F50B9 /* Applications.swift */; };
D04BA278D9EFA568C8D18A4C /* Windows.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAD1BED44EAEB77FED8A4 /* Windows.swift */; };
D04BA2CBF0EFA04CC80EC1BC /* Window.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAE80772D25834E440975 /* Window.swift */; };
D04BA308162F8043F8561D03 /* AXUIElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA40A4291E4F310527DBF /* AXUIElement.swift */; };
D04BA3261C7DA5F48310E654 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA90C6C36DB1D65BC2B66 /* Application.swift */; };
D04BA3261C7DA5F48310E654 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA90C6C36DB1D65BC2B66 /* App.swift */; };
D04BA4D356055A39B97712DE /* PrivateApis.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAF0DFC1F44322973CE1E /* PrivateApis.swift */; };
D04BA57A871B7269BEBAFF84 /* Keyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA35456DA0DDA74F9687E /* Keyboard.swift */; };
D04BA57FB9EF1373D59A1AA7 /* CGWindowID.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAEA3EDC4F80FA23DBEC4 /* CGWindowID.swift */; };
Expand All @@ -26,6 +28,7 @@
D04BA9119E2329DB5A35B3C7 /* ThumbnailsPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAE5BBE182DD5DDFE2E3E /* ThumbnailsPanel.swift */; };
D04BA960DDD1D32A3019C835 /* CollectionViewCenterFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA3202A2C22C347E849B3 /* CollectionViewCenterFlowLayout.swift */; };
D04BA9CCE02D30C8164A552A /* SystemPermissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA2D2AD6B1CCA3F3A4DD7 /* SystemPermissions.swift */; };
D04BAAD43731608067734ED3 /* DispatchQueues.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA56E285C3FCDA52ED262 /* DispatchQueues.swift */; };
D04BAD4DE538FDF7E7532EE2 /* Labels.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAD32E130E4A061DC8332 /* Labels.swift */; };
D04BAE2E8E9B9898A4DF9B3B /* FontIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAED53465957807CBF8B2 /* FontIcon.swift */; };
D04BAE369A14C3126A1606FE /* HelperExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA8F1AA48A323EE5638DC /* HelperExtensions.swift */; };
Expand Down Expand Up @@ -58,14 +61,16 @@
D04BA4F23325560BC0BCDDB7 /* 7 windows - 2 lines - tall window.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "7 windows - 2 lines - tall window.jpg"; sourceTree = "<group>"; };
D04BA51D43775E57CE91154A /* 3 windows - 1 line - wide window.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "3 windows - 1 line - wide window.jpg"; sourceTree = "<group>"; };
D04BA56355579F78776E6D51 /* Cell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Cell.swift; sourceTree = "<group>"; };
D04BA56E285C3FCDA52ED262 /* DispatchQueues.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DispatchQueues.swift; sourceTree = "<group>"; };
D04BA5ABFA5457A86536E2E4 /* 5 windows - 1 line.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "5 windows - 1 line.jpg"; sourceTree = "<group>"; };
D04BA5EB5ED248C8C22CC672 /* Spaces.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Spaces.swift; sourceTree = "<group>"; };
D04BA66B5B4143D2238F50B9 /* Applications.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Applications.swift; sourceTree = "<group>"; };
D04BA78E3B4E73B40DB77174 /* CGWindow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CGWindow.swift; sourceTree = "<group>"; };
D04BA7B6AAB0812631BBC7A2 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.info; path = Info.plist; sourceTree = "<group>"; };
D04BA7ECCE728582D9ECA613 /* determine_version.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = determine_version.sh; sourceTree = "<group>"; };
D04BA82F792DF53958D92572 /* alt-tab-macos.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "alt-tab-macos.app"; sourceTree = BUILT_PRODUCTS_DIR; };
D04BA8F1AA48A323EE5638DC /* HelperExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HelperExtensions.swift; sourceTree = "<group>"; };
D04BA90C6C36DB1D65BC2B66 /* Application.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = "<group>"; };
D04BA90C6C36DB1D65BC2B66 /* App.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = "<group>"; };
D04BA92541D46EA4F6943A72 /* package-lock.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "package-lock.json"; sourceTree = "<group>"; };
D04BA9EF65B2E7AF9E3ADCA3 /* 2 windows - 1 line.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "2 windows - 1 line.jpg"; sourceTree = "<group>"; };
D04BAA34E0CB00DED7C04B4F /* 2-rows.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "2-rows.jpg"; sourceTree = "<group>"; };
Expand All @@ -75,7 +80,7 @@
D04BAC02D60EF22D9CC7D969 /* commitlint.config.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = commitlint.config.js; sourceTree = "<group>"; };
D04BAC159731F80FDAF4EA6C /* 1-row.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "1-row.jpg"; sourceTree = "<group>"; };
D04BAC6AFC7F06D1A567F27A /* set_version_in_app.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = set_version_in_app.sh; sourceTree = "<group>"; };
D04BAD1BED44EAEB77FED8A4 /* TrackedWindows.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrackedWindows.swift; sourceTree = "<group>"; };
D04BAD1BED44EAEB77FED8A4 /* Windows.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Windows.swift; sourceTree = "<group>"; };
D04BAD1C9F215BCCD3B620AC /* alt_tab_macos.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = alt_tab_macos.entitlements; sourceTree = "<group>"; };
D04BAD32E130E4A061DC8332 /* Labels.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Labels.swift; sourceTree = "<group>"; };
D04BAD40CE2D3A8AAC3819D0 /* .gitignore */ = {isa = PBXFileReference; lastKnownFileType = file.gitignore; path = .gitignore; sourceTree = "<group>"; };
Expand All @@ -85,11 +90,12 @@
D04BADCB1C0F50340A6CAFC2 /* Preferences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = "<group>"; };
D04BAE1243C9B4BE3ED1B524 /* 7 windows - 2 lines - extra wide window.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "7 windows - 2 lines - extra wide window.jpg"; sourceTree = "<group>"; };
D04BAE5BBE182DD5DDFE2E3E /* ThumbnailsPanel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThumbnailsPanel.swift; sourceTree = "<group>"; };
D04BAE80772D25834E440975 /* TrackedWindow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrackedWindow.swift; sourceTree = "<group>"; };
D04BAE80772D25834E440975 /* Window.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Window.swift; sourceTree = "<group>"; };
D04BAEA3EDC4F80FA23DBEC4 /* CGWindowID.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CGWindowID.swift; sourceTree = "<group>"; };
D04BAED53465957807CBF8B2 /* FontIcon.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FontIcon.swift; sourceTree = "<group>"; };
D04BAF076A30A1BAFEDBEA66 /* 5 windows - 2 lines.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "5 windows - 2 lines.jpg"; sourceTree = "<group>"; };
D04BAF0DFC1F44322973CE1E /* PrivateApis.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrivateApis.swift; sourceTree = "<group>"; };
D04BAF13DFAA6930676D0492 /* Application.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = "<group>"; };
D04BAF249324297C07E31164 /* frontpage.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = frontpage.jpg; sourceTree = "<group>"; };
D04BAFA277EAE3BDDDB61110 /* CHANGELOG.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = "<group>"; };
F0298708E2B13DBD4738AE76 /* HyperlinkLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HyperlinkLabel.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -219,12 +225,15 @@
isa = PBXGroup;
children = (
D04BA35456DA0DDA74F9687E /* Keyboard.swift */,
D04BAD1BED44EAEB77FED8A4 /* TrackedWindows.swift */,
D04BAD1BED44EAEB77FED8A4 /* Windows.swift */,
D04BADCB1C0F50340A6CAFC2 /* Preferences.swift */,
D04BA3F15EAE8D8C39B6F2CF /* Screen.swift */,
D04BA2D2AD6B1CCA3F3A4DD7 /* SystemPermissions.swift */,
D04BA5EB5ED248C8C22CC672 /* Spaces.swift */,
D04BAE80772D25834E440975 /* TrackedWindow.swift */,
D04BAE80772D25834E440975 /* Window.swift */,
D04BAF13DFAA6930676D0492 /* Application.swift */,
D04BA66B5B4143D2238F50B9 /* Applications.swift */,
D04BA56E285C3FCDA52ED262 /* DispatchQueues.swift */,
);
path = logic;
sourceTree = "<group>";
Expand All @@ -234,7 +243,7 @@
children = (
D04BA3202A2C22C347E849B3 /* CollectionViewCenterFlowLayout.swift */,
D04BA56355579F78776E6D51 /* Cell.swift */,
D04BA90C6C36DB1D65BC2B66 /* Application.swift */,
D04BA90C6C36DB1D65BC2B66 /* App.swift */,
D04BA02F476DE30C4647886C /* PreferencesPanel.swift */,
D04BAE5BBE182DD5DDFE2E3E /* ThumbnailsPanel.swift */,
D04BA0AF7C5DCF367FBB663C /* StatusItem.swift */,
Expand Down Expand Up @@ -351,8 +360,8 @@
D04BAEF78503D7A2CEFB9E9E /* main.swift in Sources */,
D04BA20D4A240843293B3B52 /* Cell.swift in Sources */,
D04BA57A871B7269BEBAFF84 /* Keyboard.swift in Sources */,
D04BA278D9EFA568C8D18A4C /* TrackedWindows.swift in Sources */,
D04BA3261C7DA5F48310E654 /* Application.swift in Sources */,
D04BA278D9EFA568C8D18A4C /* Windows.swift in Sources */,
D04BA3261C7DA5F48310E654 /* App.swift in Sources */,
D04BA70FF7262BF5F9E6E13B /* Preferences.swift in Sources */,
D04BA6368E681BE3A408AC99 /* PreferencesPanel.swift in Sources */,
D04BA9119E2329DB5A35B3C7 /* ThumbnailsPanel.swift in Sources */,
Expand All @@ -368,8 +377,11 @@
D04BAE2E8E9B9898A4DF9B3B /* FontIcon.swift in Sources */,
D04BA4D356055A39B97712DE /* PrivateApis.swift in Sources */,
D04BA6B6B703DCEFE892D5A4 /* Spaces.swift in Sources */,
D04BA2CBF0EFA04CC80EC1BC /* TrackedWindow.swift in Sources */,
D04BA2CBF0EFA04CC80EC1BC /* Window.swift in Sources */,
D04BA57FB9EF1373D59A1AA7 /* CGWindowID.swift in Sources */,
D04BA1BA0B3F2E0A47883569 /* Application.swift in Sources */,
D04BA2378832FD7E5DE3BC23 /* Applications.swift in Sources */,
D04BAAD43731608067734ED3 /* DispatchQueues.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
114 changes: 41 additions & 73 deletions alt-tab-macos/api-wrappers/AXUIElement.swift
Original file line number Diff line number Diff line change
@@ -1,100 +1,68 @@
import Cocoa
import Foundation

// This list of keys is not exhaustive; it contains only the values used by this app
// full public list: ApplicationServices.HIServices.AXAttributeConstants.swift
// Note that the String value is transformed by the getters (e.g. kAXWindowsAttribute -> AXWindows)
enum AXAttributeKey: String {
case windows = "AXWindows"
case minimized = "AXMinimized"
case focusedWindow = "AXFocusedWindow"
case subrole = "AXSubrole"
}

extension AXUIElement {
func value<T>(_ key: AXAttributeKey, _ target: T, _ type: AXValueType) -> T? {
if let a = attribute(key, AXValue.self) {
var value = target
AXValueGetValue(a, type, &value)
return value
}
return nil
}

func attribute<T>(_ key: AXAttributeKey, _ type: T.Type) -> T? {
var value: AnyObject?
let result = AXUIElementCopyAttributeValue(self, key.rawValue as CFString, &value)
if result == .success, let value = value as? T {
return value
}
return nil
}

func cgId() -> CGWindowID {
func cgWindowId() -> CGWindowID {
var id = CGWindowID(0)
_AXUIElementGetWindow(self, &id)
return id
}

func focusedWindow() -> AXUIElement? {
return attribute(.focusedWindow, AXUIElement.self)
func pid() -> pid_t {
var pid = pid_t(0)
AXUIElementGetPid(self, &pid)
return pid
}

func isActualWindow() -> Bool {
let subrole = self.attribute(.subrole, String.self)
return subrole != nil && subrole != "AXUnknown"
func isActualWindow(_ isAppHidden: Bool = false) -> Bool {
// TODO: should we displays windows that disappear when invoking Expose? (e.g. Outlook meeting reminder window) (see https://stackoverflow.com/a/49723037/2249756)
// TODO: TotalFinder and XtraFinder double-window hacks (see #84)
// TODO: should we display menubar windows? (e.g. iStats Pro dropdown menu)
// Some non-windows have subrole: nil (e.g. some OS elements), "AXUnknown" (e.g. Bartender), "AXSystemDialog" (e.g. Intellij tooltips)
// Some non-windows have title: nil (e.g. some OS elements)
// Minimized windows or windows of a hidden app have subrole "AXDialog"
return title() != nil && (subrole() == "AXStandardWindow" || isMinimized() || isAppHidden)
}

func windows() -> [AXUIElement]? {
return attribute(.windows, [AXUIElement].self)
func title() -> String? {
return attribute(kAXTitleAttribute, String.self)
}

func window(_ id: CGWindowID) -> AXUIElement? {
return windows()?.first(where: { return id == $0.cgId() })
func windows() -> [AXUIElement]? {
return attribute(kAXWindowsAttribute, [AXUIElement].self)
}

func isMinimized() -> Bool {
return attribute(.minimized, Bool.self) == true
return attribute(kAXMinimizedAttribute, Bool.self) == true
}

func focus(_ id: CGWindowID) {
// implementation notes: the following sequence of actions repeats some calls. This is necessary for
// minimized windows on other spaces, and focuses windows faster (e.g. the Security & Privacy window)
// macOS bug: when switching to a System Preferences window in another space, it switches to that space,
// but quickly switches back to another window in that space
// You can reproduce this buggy behaviour by clicking on the dock icon, proving it's an OS bug
var elementConnection = UInt32(0)
CGSGetWindowOwner(cgsMainConnectionId, id, &elementConnection)
var psn = ProcessSerialNumber()
CGSGetConnectionPSN(elementConnection, &psn)
AXUIElementPerformAction(self, kAXRaiseAction as CFString)
makeKeyWindow(psn, id)
_SLPSSetFrontProcessWithOptions(&psn, id, .userGenerated)
makeKeyWindow(psn, id)
AXUIElementPerformAction(self, kAXRaiseAction as CFString)
func isHidden() -> Bool {
return attribute(kAXHiddenAttribute, Bool.self) == true
}

// The following function was ported from https://github.com/Hammerspoon/hammerspoon/issues/370#issuecomment-545545468
func makeKeyWindow(_ psn: ProcessSerialNumber, _ wid: CGWindowID) -> Void {
var wid_ = wid
var psn_ = psn

var bytes1 = [UInt8](repeating: 0, count: 0xf8)
bytes1[0x04] = 0xF8
bytes1[0x08] = 0x01
bytes1[0x3a] = 0x10
func focusedWindow() -> AXUIElement? {
return attribute(kAXFocusedWindowAttribute, AXUIElement.self)
}

var bytes2 = [UInt8](repeating: 0, count: 0xf8)
bytes2[0x04] = 0xF8
bytes2[0x08] = 0x02
bytes2[0x3a] = 0x10
func subrole() -> String? {
return attribute(kAXSubroleAttribute, String.self)
}

memcpy(&bytes1[0x3c], &wid_, MemoryLayout<UInt32>.size)
memset(&bytes1[0x20], 0xFF, 0x10)
memcpy(&bytes2[0x3c], &wid_, MemoryLayout<UInt32>.size)
memset(&bytes2[0x20], 0xFF, 0x10)
private func attribute<T>(_ key: String, _ type: T.Type) -> T? {
var value: AnyObject?
let result = AXUIElementCopyAttributeValue(self, key as CFString, &value)
if result == .success, let value = value as? T {
return value
}
return nil
}

SLPSPostEventRecordTo(&psn_, &(UnsafeMutablePointer(mutating: UnsafePointer<UInt8>(bytes1)).pointee))
SLPSPostEventRecordTo(&psn_, &(UnsafeMutablePointer(mutating: UnsafePointer<UInt8>(bytes2)).pointee))
private func value<T>(_ key: String, _ target: T, _ type: AXValueType) -> T? {
if let a = attribute(key, AXValue.self) {
var value = target
AXValueGetValue(a, type, &value)
return value
}
return nil
}
}
Loading

0 comments on commit 3a7f20c

Please sign in to comment.