diff --git a/AeroSpace.xcodeproj/project.pbxproj b/AeroSpace.xcodeproj/project.pbxproj index ca7d95f8..98413b8d 100644 --- a/AeroSpace.xcodeproj/project.pbxproj +++ b/AeroSpace.xcodeproj/project.pbxproj @@ -11,7 +11,6 @@ 059308740527CC10D1A79DDD /* AeroApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 996BDAB685567E430DF7D962 /* AeroApp.swift */; }; 0A90EEEAC020DD3A56736014 /* FocusCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = E761155C73F06E2CF5E292A4 /* FocusCommand.swift */; }; 0A9DAD847B5340FBE3D9CF88 /* FlattenWorkspaceTreeCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3C5B6719EF0BC14D7CF868C /* FlattenWorkspaceTreeCommand.swift */; }; - 115F5CA4BEB80B645E66D198 /* NSScreenEx.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF3BB3DD434C75536217CB88 /* NSScreenEx.swift */; }; 1311398A83B998908773C54D /* FocusCommandTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0EAADE8D2FB5D05FA5456B0 /* FocusCommandTest.swift */; }; 1C46EBB55D401C0D1AFD50F0 /* CollectionEx.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51CE37C1B8D858C81A396F40 /* CollectionEx.swift */; }; 1D408CDF1A489E527327EB15 /* CompositeCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = D82CD9670B7A6050073E0F76 /* CompositeCommand.swift */; }; @@ -36,13 +35,13 @@ 70A82A4A9DFC89286C4F7696 /* TilingContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00ECDFE176777828D560A737 /* TilingContainer.swift */; }; 77FA83225024151CD556E1ED /* CloseAllWindowsButCurrentCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99853C505D93E41F6531C324 /* CloseAllWindowsButCurrentCommand.swift */; }; 78EE0CEF814ABDBA67941B84 /* Rect.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28B788A95DD3C267878E05B5 /* Rect.swift */; }; - 7ED8C2A66DD6F903796F090C /* TestApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2431BEAFFF1AC12D3001317A /* TestApp.swift */; }; 852F88894A3B9FC385563665 /* HotKey in Frameworks */ = {isa = PBXBuildFile; productRef = 42BC1E757EF69233C2262FF4 /* HotKey */; }; 89064BDDB5E9D17BEDE52E8C /* MoveContainerToWorkspaceCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD62043D7F5113A8BA635FDF /* MoveContainerToWorkspaceCommand.swift */; }; 920FDF8498DCCB62149D1719 /* Monitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6507EBAA795220FD0C05384 /* Monitor.swift */; }; 93D44EA41776738B4758C28D /* util.swift in Sources */ = {isa = PBXBuildFile; fileRef = 976540EBEACF846D598CD6E1 /* util.swift */; }; 991943D50DF9EDBF321A66F1 /* SelectorComparator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1316FFED5D1044CB693EA45 /* SelectorComparator.swift */; }; 9A138A729245BD2723148583 /* focused.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61FE8B343B068F0FFFC2373 /* focused.swift */; }; + 9C6B3FEFCC73DA17B2D6246F /* TestApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34B239D50676DD5C78936C0B /* TestApp.swift */; }; 9D34BD7DE311254BF52F5EA2 /* testUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = F77C03164B8BFD1E59779C6E /* testUtil.swift */; }; A0765C31043BCFB0420BF1C9 /* parseConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67DBAF4ECF8A0B931FC34EAD /* parseConfig.swift */; }; A2CBF9674964F9083BB198D2 /* ArrayEx.swift in Sources */ = {isa = PBXBuildFile; fileRef = 883D7F7F87FBE7D0BDE4E87F /* ArrayEx.swift */; }; @@ -92,8 +91,8 @@ 1A2B673C67B00DBFCC27FFE7 /* LayoutCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutCommand.swift; sourceTree = ""; }; 1C0D40CBD65704BA9595C2FA /* keysMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = keysMap.swift; sourceTree = ""; }; 1E81623E8954701269A22322 /* AeroSpaceApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AeroSpaceApp.swift; sourceTree = ""; }; - 2431BEAFFF1AC12D3001317A /* TestApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestApp.swift; sourceTree = ""; }; 28B788A95DD3C267878E05B5 /* Rect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Rect.swift; sourceTree = ""; }; + 34B239D50676DD5C78936C0B /* TestApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestApp.swift; sourceTree = ""; }; 381743262EB4D8AB365235C8 /* Writer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Writer.swift; sourceTree = ""; }; 3A262B442A94C1964509B691 /* TreeNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TreeNode.swift; sourceTree = ""; }; 3A6EF465EF4129BCB10FE247 /* ExecCommandTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExecCommandTest.swift; sourceTree = ""; }; @@ -133,7 +132,6 @@ A9EDFD4A9F45182CA6E0BD7B /* OptionalEx.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionalEx.swift; sourceTree = ""; }; AAE5DCAEC5EE619CE33859E7 /* SequenceEx.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SequenceEx.swift; sourceTree = ""; }; AD1645D9939F3F896EF21393 /* TreeNodeTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TreeNodeTest.swift; sourceTree = ""; }; - AF3BB3DD434C75536217CB88 /* NSScreenEx.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSScreenEx.swift; sourceTree = ""; }; B1316FFED5D1044CB693EA45 /* SelectorComparator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectorComparator.swift; sourceTree = ""; }; B7DB782C527ABE0CF31740EB /* MacApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacApp.swift; sourceTree = ""; }; BEF353340822CD20E9DAB3EC /* AeroSpace.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = AeroSpace.entitlements; sourceTree = ""; }; @@ -198,6 +196,7 @@ DAC5AF4019D1F5DAB5AF2B56 /* layoutRecursive.swift */, B7DB782C527ABE0CF31740EB /* MacApp.swift */, 9245C6FACF389672EA71173B /* MacWindow.swift */, + 34B239D50676DD5C78936C0B /* TestApp.swift */, C848D6E57FDF22AAF0FB45E6 /* TilingContainer.swift */, 3A262B442A94C1964509B691 /* TreeNode.swift */, 9F6B8A501483ACBB62560101 /* TreeNodeEx.swift */, @@ -303,7 +302,6 @@ BAC4E4D413715557A1BBDCC8 /* tree */ = { isa = PBXGroup; children = ( - 2431BEAFFF1AC12D3001317A /* TestApp.swift */, 6F1905935B0C61590A96EFEF /* TestWindow.swift */, 00ECDFE176777828D560A737 /* TilingContainer.swift */, AD1645D9939F3F896EF21393 /* TreeNodeTest.swift */, @@ -323,7 +321,6 @@ 54B4C778D2594EB23C295741 /* Copyable.swift */, F6507EBAA795220FD0C05384 /* Monitor.swift */, 954A434EE57D76F5A9D4140D /* MruStack.swift */, - AF3BB3DD434C75536217CB88 /* NSScreenEx.swift */, A9EDFD4A9F45182CA6E0BD7B /* OptionalEx.swift */, 28B788A95DD3C267878E05B5 /* Rect.swift */, B1316FFED5D1044CB693EA45 /* SelectorComparator.swift */, @@ -433,7 +430,6 @@ 1311398A83B998908773C54D /* FocusCommandTest.swift in Sources */, A55F31B0CC357B37108B8F54 /* MoveContainerToWorkspaceCommandTest.swift in Sources */, B1E527CF4941A4E9B8D9C3D0 /* MoveThroughCommandTest.swift in Sources */, - 7ED8C2A66DD6F903796F090C /* TestApp.swift in Sources */, E22ACB36C90695FBAC78226E /* TestWindow.swift in Sources */, 70A82A4A9DFC89286C4F7696 /* TilingContainer.swift in Sources */, BB3C35BE40958AE6E7B4A9FD /* TreeNodeTest.swift in Sources */, @@ -470,13 +466,13 @@ 89064BDDB5E9D17BEDE52E8C /* MoveContainerToWorkspaceCommand.swift in Sources */, EDFDE707B4DC5E500B1709B1 /* MoveThroughCommand.swift in Sources */, 635733FDDF37E44364372B74 /* MruStack.swift in Sources */, - 115F5CA4BEB80B645E66D198 /* NSScreenEx.swift in Sources */, C0A88261ECF505FC5648FC0A /* OptionalEx.swift in Sources */, 78EE0CEF814ABDBA67941B84 /* Rect.swift in Sources */, FC35D6D0A678CC802972C6FE /* ReloadConfigCommand.swift in Sources */, D24D02B1FD87424B908986AF /* ResizeCommand.swift in Sources */, 991943D50DF9EDBF321A66F1 /* SelectorComparator.swift in Sources */, AE76A183D0454E4C8ADCE380 /* SequenceEx.swift in Sources */, + 9C6B3FEFCC73DA17B2D6246F /* TestApp.swift in Sources */, E5682579AEC6B84CF6FCE90D /* TilingContainer.swift in Sources */, 56E72B24303F5F337B31B776 /* TrayMenuModel.swift in Sources */, F892B5DCB4F731B3E173FF4C /* TreeNode.swift in Sources */, diff --git a/src/AeroSpaceApp.swift b/src/AeroSpaceApp.swift index d5d2051c..f4112075 100644 --- a/src/AeroSpaceApp.swift +++ b/src/AeroSpaceApp.swift @@ -52,7 +52,7 @@ struct AeroSpaceApp: App { } Button("Quit \(Bundle.appName)") { for app in apps { // Make all windows fullscreen before Quit - for window in app.macApp?.windows ?? [] { + for window in app.windows { let rect = window.workspace.monitor.visibleRect window.setSize(CGSize(width: rect.width, height: rect.height)) window.setTopLeftCorner(rect.topLeftCorner) diff --git a/src/TrayMenuModel.swift b/src/TrayMenuModel.swift index 898843c9..24bf8559 100644 --- a/src/TrayMenuModel.swift +++ b/src/TrayMenuModel.swift @@ -7,8 +7,8 @@ class TrayMenuModel: ObservableObject { } func updateTrayText() { - TrayMenuModel.shared.trayText = NSScreen.screens + TrayMenuModel.shared.trayText = monitors .sorted(using: [SelectorComparator(selector: \.rect.minX), SelectorComparator(selector: \.rect.minY)]) - .map { $0.monitor.getActiveWorkspace().name } + .map(\.activeWorkspace.name) .joined(separator: " │ ") } diff --git a/src/command/WorkspaceCommand.swift b/src/command/WorkspaceCommand.swift index 41959523..9a0faacd 100644 --- a/src/command/WorkspaceCommand.swift +++ b/src/command/WorkspaceCommand.swift @@ -16,8 +16,8 @@ struct WorkspaceCommand : Command { // The switching itself will be done by refreshWorkspaces and layoutWorkspaces later in refresh } else { // switch to empty workspace precondition(workspace.isEffectivelyEmpty) - // It's the only place in the app where I allow myself to use NSScreen.main. - // This function isn't invoked from callbacks that's why .main should be fine + // It's fine to call Unsafe from here because commands are not invoked from callbacks, + // the callbacks are triggered by user if let focusedMonitor = focusedMonitorOrNilIfDesktop ?? focusedMonitorUnsafe { focusedMonitor.setActiveWorkspace(workspace) } diff --git a/src/config/Config.swift b/src/config/Config.swift index 45ab5117..094a2363 100644 --- a/src/config/Config.swift +++ b/src/config/Config.swift @@ -24,7 +24,7 @@ struct Config { var accordionPadding: Int let modes: [String: Mode] - var workspaceNames: [String] + var preservedWorkspaceNames: [String] } enum FocusWrapping: String { // todo think about mental model diff --git a/src/config/parseConfig.swift b/src/config/parseConfig.swift index 61739a0a..a91f8225 100644 --- a/src/config/parseConfig.swift +++ b/src/config/parseConfig.swift @@ -140,7 +140,7 @@ func parseConfig(_ rawToml: String) -> ParsedTomlWriter { accordionPadding: value12 ?? defaultConfig.accordionPadding, modes: modesOrDefault, - workspaceNames: modesOrDefault.values.lazy + preservedWorkspaceNames: modesOrDefault.values.lazy .flatMap { (mode: Mode) -> [HotkeyBinding] in mode.bindings } .map { (binding: HotkeyBinding) -> Command in binding.command } .map { (command: Command) -> Command in (command as? CompositeCommand)?.subCommands.singleOrNil() ?? command } diff --git a/src/focused.swift b/src/focused.swift index e1a25073..12ad3864 100644 --- a/src/focused.swift +++ b/src/focused.swift @@ -24,36 +24,15 @@ var focusedApp: AeroApp? { } } -/// Motivation: -/// 1. NSScreen.main is a misleading name. -/// 2. NSScreen.main doesn't work correctly from NSWorkspace.didActivateApplicationNotification & -/// kAXFocusedWindowChangedNotification callbacks. -/// -/// Returns `nil` if the desktop is selected (which is when the app is active but doesn't show any window) -var focusedMonitorOrNilIfDesktop: Monitor? { - let window = focusedWindow as! MacWindow? // todo - return window?.getCenter()?.monitorApproximation ?? monitors.singleOrNil() - //NSWorkspace.activeApp?.macApp?.axFocusedWindow? - // .get(Ax.topLeftCornerAttr)?.monitorApproximation - // ?? NSScreen.screens.singleOrNil() -} - -/// It's unsafe because NSScreen.main doesn't work correctly from NSWorkspace.didActivateApplicationNotification & -/// kAXFocusedWindowChangedNotification callbacks. -var focusedMonitorUnsafe: Monitor? { - NSScreen.main?.monitor -} - -var monitors: [Monitor] { NSScreen.screens.map(\.monitor) } - var focusedWindow: Window? { focusedApp?.focusedWindow } var focusedWindowOrEffectivelyFocused: Window? { focusedWindow ?? Workspace.focused.mostRecentWindow ?? Workspace.focused.anyLeafWindowRecursive } -private var _focusedWorkspaceName: String = focusedMonitorUnsafe?.getActiveWorkspace().name - ?? mainMonitor.getActiveWorkspace().name +// It's fine to call this Unsafe during startup +private var _focusedWorkspaceName: String = focusedMonitorUnsafe?.activeWorkspace.name + ?? mainMonitor.activeWorkspace.name var focusedWorkspaceName: String { get { _focusedWorkspaceName } set { diff --git a/src/refresh.swift b/src/refresh.swift index c982be42..cc5e13df 100644 --- a/src/refresh.swift +++ b/src/refresh.swift @@ -36,7 +36,7 @@ private func refreshWorkspaces() { //debug("refreshWorkspaces: not empty") let focusedWorkspace: Workspace if focusedWindow.isFloating && !focusedWindow.isHiddenViaEmulation { // todo maybe drop once move with mouse is supported - focusedWorkspace = focusedWindow.getCenter()?.monitorApproximation.getActiveWorkspace() + focusedWorkspace = focusedWindow.getCenter()?.monitorApproximation.activeWorkspace ?? focusedWindow.workspace focusedWindow.bindAsFloatingWindowTo(workspace: focusedWorkspace) } else { @@ -67,7 +67,7 @@ private func normalizeContainers() { private func layoutWindows(firstStart: Bool) { for monitor in monitors { - let workspace = monitor.getActiveWorkspace() + let workspace = monitor.activeWorkspace if workspace.isEffectivelyEmpty { continue } let rect = monitor.visibleRect workspace.layoutRecursive(rect.topLeftCorner, width: rect.width, height: rect.height, firstStart: firstStart) @@ -76,6 +76,6 @@ private func layoutWindows(firstStart: Bool) { private func detectNewWindowsAndAttachThemToWorkspaces() { for app in apps { - let _ = app.macApp?.windows + let _ = app.windows } } diff --git a/src/tree/AeroApp.swift b/src/tree/AeroApp.swift index fbd3fef5..8a6c3374 100644 --- a/src/tree/AeroApp.swift +++ b/src/tree/AeroApp.swift @@ -20,4 +20,5 @@ class AeroApp: Hashable { } var focusedWindow: Window? { error("Not implemented") } + var windows: [Window] { error("Not implemented") } } diff --git a/src/tree/MacApp.swift b/src/tree/MacApp.swift index abc93483..b468803e 100644 --- a/src/tree/MacApp.swift +++ b/src/tree/MacApp.swift @@ -64,7 +64,7 @@ final class MacApp: AeroApp { axApp.get(Ax.focusedWindowAttr) } - var windows: [MacWindow] { + override var windows: [Window] { (axApp.get(Ax.windowsAttr) ?? []).compactMap({ MacWindow.get(app: self, axWindow: $0) }) } } diff --git a/src/tree/MacWindow.swift b/src/tree/MacWindow.swift index 21d17ae1..d110382c 100644 --- a/src/tree/MacWindow.swift +++ b/src/tree/MacWindow.swift @@ -20,7 +20,7 @@ final class MacWindow: Window, CustomStringConvertible { if let existing = allWindowsMap[id] { return existing } else { - let workspace: Workspace = (axWindow.center?.monitorApproximation ?? mainMonitor).getActiveWorkspace() + let workspace: Workspace = (axWindow.center?.monitorApproximation ?? mainMonitor).activeWorkspace let parent: NonLeafTreeNode let index: Int if shouldFloat(axWindow) { diff --git a/test/tree/TestApp.swift b/src/tree/TestApp.swift similarity index 73% rename from test/tree/TestApp.swift rename to src/tree/TestApp.swift index 8b1fb955..1155acef 100644 --- a/test/tree/TestApp.swift +++ b/src/tree/TestApp.swift @@ -1,5 +1,3 @@ -@testable import AeroSpace_Debug - final class TestApp: AeroApp { static var shared = TestApp(id: 0) @@ -7,8 +5,8 @@ final class TestApp: AeroApp { super.init(id: id) } - var _windows: [TestWindow] = [] - var windows: [TestWindow] { + var _windows: [Window] = [] + override var windows: [Window] { get { _windows } set { if let focusedWindow { @@ -18,8 +16,8 @@ final class TestApp: AeroApp { } } - private var _focusedWindow: TestWindow? = nil - override var focusedWindow: TestWindow? { + private var _focusedWindow: Window? = nil + override var focusedWindow: Window? { get { _focusedWindow } set { if let window = newValue { diff --git a/src/tree/Workspace.swift b/src/tree/Workspace.swift index 53c58a90..ff560652 100644 --- a/src/tree/Workspace.swift +++ b/src/tree/Workspace.swift @@ -26,7 +26,7 @@ var allMonitorsRectsUnion: Rect { monitors.map(\.rect).union() } -class Workspace: TreeNode, NonLeafTreeNode, Hashable, Identifiable { +class Workspace: TreeNode, NonLeafTreeNode, Hashable, Identifiable, CustomStringConvertible { let name: String var id: String { name } // satisfy Identifiable /// This variable must be interpreted only when the workspace is invisible @@ -59,8 +59,17 @@ class Workspace: TreeNode, NonLeafTreeNode, Hashable, Identifiable { error("It's not possible to change weight of Workspace") } + var description: String { + let description = [ + ("name", name), + ("isVisible", String(isVisible)), + ("isEffectivelyEmpty", String(isEffectivelyEmpty)), + ].map { "\($0.0): '\(String(describing: $0.1))'" }.joined(separator: ", ") + return "Workspace(\(description))" + } + static func garbageCollectUnusedWorkspaces() { - let preservedNames = config.workspaceNames.toSet() + let preservedNames = config.preservedWorkspaceNames.toSet() for name in preservedNames { _ = get(byName: name) // Make sure that all preserved workspaces are "cached" } @@ -105,7 +114,7 @@ extension Workspace { } extension Monitor { - func getActiveWorkspace() -> Workspace { + var activeWorkspace: Workspace { if let existing = screenPointToVisibleWorkspace[rect.topLeftCorner] { return existing } @@ -113,9 +122,11 @@ extension Monitor { rearrangeWorkspacesOnMonitors() // Normally, recursion should happen only once more because we must take the value from the cache // (Unless, monitor configuration data race happens) - return getActiveWorkspace() + return self.activeWorkspace } + // It can't be converted to property because stupid Swift requires Monitor to be `var` + // if you want to assign to calculated property func setActiveWorkspace(_ workspace: Workspace) { rect.topLeftCorner.setActiveWorkspace(workspace) } @@ -140,7 +151,7 @@ private extension CGPoint { private func rearrangeWorkspacesOnMonitors() { var oldVisibleScreens: Set = screenPointToVisibleWorkspace.keys.toSet() - let newScreens = NSScreen.screens.map(\.rect.topLeftCorner) + let newScreens = monitors.map(\.rect.topLeftCorner) var newScreenToOldScreenMapping: [CGPoint:CGPoint] = [:] var preservedOldScreens: [CGPoint] = [] for newScreen in newScreens { diff --git a/src/util/Monitor.swift b/src/util/Monitor.swift index 444a1f13..33b0f0d9 100644 --- a/src/util/Monitor.swift +++ b/src/util/Monitor.swift @@ -42,6 +42,49 @@ class LazyMonitor: Monitor { } } -extension NSScreen { +private extension NSScreen { var monitor: Monitor { MonitorImpl(name: localizedName, rect: rect, visibleRect: visibleRect) } + + var isMainScreen: Bool { + frame.minX == 0 && frame.minY == 0 + } + + /// The property is a replacement for Apple's crazy ``frame`` + /// + /// - For ``MacWindow.topLeftCorner``, (0, 0) is main screen top left corner, and positive y-axis goes down. + /// - For ``frame``, (0, 0) is main screen bottom left corner, and positive y-axis goes up (which is crazy). + /// + /// The property "normalizes" ``frame`` + var rect: Rect { frame.monitorFrameNormalized() } + + /// Same as ``rect`` but for ``visibleFrame`` + var visibleRect: Rect { visibleFrame.monitorFrameNormalized() } +} + +private let testMonitorRect = Rect(topLeftX: 0, topLeftY: 0, width: 1920, height: 1080) +private let testMonitor = MonitorImpl(name: "Test Monitor", rect: testMonitorRect, visibleRect: testMonitorRect) + +/// Motivation: +/// 1. NSScreen.main is a misleading name. +/// 2. NSScreen.main doesn't work correctly from NSWorkspace.didActivateApplicationNotification & +/// kAXFocusedWindowChangedNotification callbacks. +/// +/// Returns `nil` if the desktop is selected (which is when the app is active but doesn't show any window) +var focusedMonitorOrNilIfDesktop: Monitor? { + isUnitTest ? testMonitor : (focusedWindow?.getCenter()?.monitorApproximation ?? monitors.singleOrNil()) + //NSWorkspace.activeApp?.macApp?.axFocusedWindow? + // .get(Ax.topLeftCornerAttr)?.monitorApproximation + // ?? NSScreen.screens.singleOrNil() +} + +/// It's unsafe because NSScreen.main doesn't work correctly from NSWorkspace.didActivateApplicationNotification & +/// kAXFocusedWindowChangedNotification callbacks. +var focusedMonitorUnsafe: Monitor? { + isUnitTest ? testMonitor : NSScreen.main?.monitor } + +var mainMonitor: Monitor { + isUnitTest ? testMonitor : LazyMonitor(NSScreen.screens.singleOrNil(where: \.isMainScreen)!) +} + +var monitors: [Monitor] { isUnitTest ? [testMonitor] : NSScreen.screens.map(\.monitor) } diff --git a/src/util/NSScreenEx.swift b/src/util/NSScreenEx.swift deleted file mode 100644 index 1211d874..00000000 --- a/src/util/NSScreenEx.swift +++ /dev/null @@ -1,18 +0,0 @@ -extension NSScreen { - var isMainScreen: Bool { - frame.minX == 0 && frame.minY == 0 - } - - /// The property is a replacement for Apple's crazy ``frame`` - /// - /// - For ``MacWindow.topLeftCorner``, (0, 0) is main screen top left corner, and positive y-axis goes down. - /// - For ``frame``, (0, 0) is main screen bottom left corner, and positive y-axis goes up (which is crazy). - /// - /// The property "normalizes" ``frame`` - var rect: Rect { frame.monitorFrameNormalized() } - - /// Same as ``rect`` but for ``visibleFrame`` - var visibleRect: Rect { visibleFrame.monitorFrameNormalized() } -} - -var mainMonitor: Monitor { LazyMonitor(NSScreen.screens.singleOrNil(where: \.isMainScreen)!) } diff --git a/src/util/util.swift b/src/util/util.swift index 4a969059..014c695c 100644 --- a/src/util/util.swift +++ b/src/util/util.swift @@ -18,8 +18,10 @@ extension String? { var isUnitTest: Bool { NSClassFromString("XCTestCase") != nil } -var apps: [NSRunningApplication] { - NSWorkspace.shared.runningApplications.filter { $0.activationPolicy == .regular } +var apps: [AeroApp] { + isUnitTest + ? [TestApp.shared] + : NSWorkspace.shared.runningApplications.lazy.filter { $0.activationPolicy == .regular }.map(\.macApp).filterNotNil() } func terminateApp() -> Never { diff --git a/test/command/FlattenWorkspaceTreeCommandTest.swift b/test/command/FlattenWorkspaceTreeCommandTest.swift index c2c900c9..8424d7f9 100644 --- a/test/command/FlattenWorkspaceTreeCommandTest.swift +++ b/test/command/FlattenWorkspaceTreeCommandTest.swift @@ -3,7 +3,6 @@ import XCTest final class FlattenWorkspaceTreeCommandTest: XCTestCase { override func setUpWithError() throws { setUpWorkspacesForTests() } - override func tearDownWithError() throws { tearDownWorkspacesForTests() } func testSimple() async { let workspace = Workspace.get(byName: name).apply { diff --git a/test/command/FocusCommandTest.swift b/test/command/FocusCommandTest.swift index 30d30230..1e89a36e 100644 --- a/test/command/FocusCommandTest.swift +++ b/test/command/FocusCommandTest.swift @@ -23,7 +23,6 @@ expected: mru(window3, window4) is focused final class FocusCommandTest: XCTestCase { override func setUpWithError() throws { setUpWorkspacesForTests() } - override func tearDownWithError() throws { tearDownWorkspacesForTests() } func testFocus() async { XCTAssertEqual(focusedWindow, nil) diff --git a/test/command/MoveContainerToWorkspaceCommandTest.swift b/test/command/MoveContainerToWorkspaceCommandTest.swift index 205c05b8..7be93c36 100644 --- a/test/command/MoveContainerToWorkspaceCommandTest.swift +++ b/test/command/MoveContainerToWorkspaceCommandTest.swift @@ -3,7 +3,6 @@ import XCTest final class MoveContainerToWorkspaceCommandTest: XCTestCase { override func setUpWithError() throws { setUpWorkspacesForTests() } - override func tearDownWithError() throws { tearDownWorkspacesForTests() } func testSimple() async { let workspaceA = Workspace.get(byName: "a") diff --git a/test/command/MoveThroughCommandTest.swift b/test/command/MoveThroughCommandTest.swift index 30fa83c4..6c400af7 100644 --- a/test/command/MoveThroughCommandTest.swift +++ b/test/command/MoveThroughCommandTest.swift @@ -3,7 +3,6 @@ import XCTest final class MoveThroughCommandTest: XCTestCase { override func setUpWithError() throws { setUpWorkspacesForTests() } - override func tearDownWithError() throws { tearDownWorkspacesForTests() } func testMove_swapWindows() async { let root = Workspace.get(byName: name).rootTilingContainer.apply { diff --git a/test/config/ConfigTest.swift b/test/config/ConfigTest.swift index 4c01ea95..61677bd3 100644 --- a/test/config/ConfigTest.swift +++ b/test/config/ConfigTest.swift @@ -68,7 +68,7 @@ final class ConfigTest: XCTestCase { """ ).toTuple() XCTAssertEqual(errors.descriptions, []) - XCTAssertEqual(config.workspaceNames, ["1", "2", "3"]) + XCTAssertEqual(config.preservedWorkspaceNames, ["1", "2", "3"]) } func testUnknownKeyParseError() { diff --git a/test/testUtil.swift b/test/testUtil.swift index 868d06b3..a901b777 100644 --- a/test/testUtil.swift +++ b/test/testUtil.swift @@ -18,20 +18,18 @@ func setUpWorkspacesForTests() { // Don't create any workspaces for tests modes: [mainModeId: Mode(name: nil, bindings: [])], - workspaceNames: [] + preservedWorkspaceNames: [] ) - focusedWorkspaceName = "WORKSPACE FOR TESTS" - precondition(Workspace.all.singleOrNil() === Workspace.focused) - precondition(Workspace.focused.isEffectivelyEmpty) - - TestApp.shared.focusedWindow = nil - TestApp.shared.windows = [] -} - -func tearDownWorkspacesForTests() { for workspace in Workspace.all { for child in workspace.children { child.unbindFromParent() } } + focusedWorkspaceName = mainMonitor.activeWorkspace.name + Workspace.garbageCollectUnusedWorkspaces() + precondition(Workspace.focused.isEffectivelyEmpty) + precondition(Workspace.focused === Workspace.all.singleOrNil(), Workspace.all.map(\.description).joined(separator: ", ")) + + TestApp.shared.focusedWindow = nil + TestApp.shared.windows = [] } diff --git a/test/tree/TreeNodeTest.swift b/test/tree/TreeNodeTest.swift index 66305384..98271796 100644 --- a/test/tree/TreeNodeTest.swift +++ b/test/tree/TreeNodeTest.swift @@ -3,7 +3,6 @@ import XCTest final class TreeNodeTest: XCTestCase { override func setUpWithError() throws { setUpWorkspacesForTests() } - override func tearDownWithError() throws { tearDownWorkspacesForTests() } func testChildParentCyclicReferenceMemoryLeak() { let workspace = Workspace.get(byName: name) // Don't cache root node