Skip to content

Commit

Permalink
2/n Population and invalidation of closedWindowsCache
Browse files Browse the repository at this point in the history
  • Loading branch information
nikitabobko committed Nov 27, 2024
1 parent 21c68fb commit 46bf611
Show file tree
Hide file tree
Showing 12 changed files with 47 additions and 29 deletions.
10 changes: 5 additions & 5 deletions Sources/AppBundle/GlobalObserver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ class GlobalObserver {
if (notification.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication)?.bundleIdentifier == lockScreenAppBundleId {
return
}
refreshAndLayout()
refreshAndLayout(screenIsDefinitelyUnlocked: false)
}

private static func onHideApp(_ notification: Notification) {
refreshSession(body: {
refreshSession(screenIsDefinitelyUnlocked: false) {
if TrayMenuModel.shared.isEnabled && config.automaticallyUnhideMacosHiddenApps {
if let w = prevFocus?.windowOrNil,
w.macAppUnsafe.nsApp.isHidden,
Expand All @@ -26,7 +26,7 @@ class GlobalObserver {
app.nsApp.unhide()
}
}
})
}
}

static func initObserver() {
Expand All @@ -47,12 +47,12 @@ class GlobalObserver {
switch () {
// Detect clicks on desktop of different monitors
case _ where clickedMonitor.activeWorkspace != focus.workspace:
_ = refreshSession {
_ = refreshSession(screenIsDefinitelyUnlocked: true) {
clickedMonitor.activeWorkspace.focusWorkspace()
}
// Detect close button clicks for unfocused windows
case _ where focus.windowOrNil?.getRect()?.contains(mouseLocation) == false: // todo replace getRect with preflushRect when it later becomes available
refreshAndLayout()
refreshAndLayout(screenIsDefinitelyUnlocked: true)
default:
break
}
Expand Down
6 changes: 3 additions & 3 deletions Sources/AppBundle/MenuBar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public func menuBar(viewModel: TrayMenuModel) -> some Scene {
Text("Workspaces:")
ForEach(Workspace.all) { (workspace: Workspace) in
Button {
refreshSession { _ = workspace.focusWorkspace() }
refreshSession(screenIsDefinitelyUnlocked: true) { _ = workspace.focusWorkspace() }
} label: {
Toggle(isOn: workspace == focus.workspace
? Binding(get: { true }, set: { _, _ in })
Expand All @@ -28,7 +28,7 @@ public func menuBar(viewModel: TrayMenuModel) -> some Scene {
Divider()
}
Button(viewModel.isEnabled ? "Disable" : "Enable") {
refreshSession {
refreshSession(screenIsDefinitelyUnlocked: true) {
_ = EnableCommand(args: EnableCmdArgs(rawArgs: [], targetState: .toggle)).run(.defaultEnv, .emptyStdin)
}
}.keyboardShortcut("E", modifiers: .command)
Expand All @@ -47,7 +47,7 @@ public func menuBar(viewModel: TrayMenuModel) -> some Scene {
}.keyboardShortcut("O", modifiers: .command)
if viewModel.isEnabled {
Button("Reload config") {
refreshSession { _ = reloadConfig() }
refreshSession(screenIsDefinitelyUnlocked: true) { _ = reloadConfig() }
}.keyboardShortcut("R", modifiers: .command)
}
Button("Quit \(aeroSpaceAppName)") {
Expand Down
4 changes: 2 additions & 2 deletions Sources/AppBundle/command/impl/CloseCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@ struct CloseCommand: Command {
// Access ax directly. Not cool :(
if args.quitIfLastWindow && window.macAppUnsafe.axApp.get(Ax.windowsAttr)?.count == 1 {
if window.macAppUnsafe.nsApp.terminate() {
window.asMacWindow().garbageCollect()
window.asMacWindow().garbageCollect(skipClosedWindowsCache: true)
return true
} else {
return io.err("Failed to quit '\(window.app.name ?? "Unknown app")'")
}
} else {
if window.close() {
if !isUnitTest { window.asMacWindow().garbageCollect() }
if !isUnitTest { window.asMacWindow().garbageCollect(skipClosedWindowsCache: true) }
return true
} else {
return io.err("Can't close '\(window.app.name ?? "Unknown app")' window. Probably the window doesn't have a close button")
Expand Down
2 changes: 1 addition & 1 deletion Sources/AppBundle/config/HotkeyBinding.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func activateMode(_ targetMode: String?) {
hotkeys[binding.descriptionWithKeyCode] = HotKey(key: binding.keyCode, modifiers: binding.modifiers, keyDownHandler: {
check(Thread.current.isMainThread)
if let activeMode {
refreshSession {
refreshSession(screenIsDefinitelyUnlocked: true) {
_ = config.modes[activeMode]?.bindings[binding.descriptionWithKeyCode]?.commands
.runCmdSeq(.defaultEnv, .emptyStdin)
}
Expand Down
4 changes: 2 additions & 2 deletions Sources/AppBundle/initAppBundle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ public func initAppBundle() {
AXUIElementSetMessagingTimeout(systemWideAx, 1.0)
startUnixSocketServer()
GlobalObserver.initObserver()
refreshAndLayout(startup: true)
refreshSession {
refreshAndLayout(screenIsDefinitelyUnlocked: false, startup: true)
refreshSession(screenIsDefinitelyUnlocked: false) {
if serverArgs.startedAtLogin {
_ = config.afterLoginCommand.runCmdSeq(.defaultEnv, .emptyStdin)
}
Expand Down
11 changes: 6 additions & 5 deletions Sources/AppBundle/layout/refresh.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import Common
/// It's one of the most important function of the whole application.
/// The function is called as a feedback response on every user input.
/// The function is idempotent.
func refreshSession<T>(startup: Bool = false, forceFocus: Bool = false, body: () -> T) -> T {
func refreshSession<T>(screenIsDefinitelyUnlocked: Bool, startup: Bool = false, forceFocus: Bool = false, body: () -> T) -> T {
check(Thread.current.isMainThread)
if screenIsDefinitelyUnlocked { resetClosedWindowsCache() }
gc()
gcMonitors()

Expand Down Expand Up @@ -38,8 +39,8 @@ func refreshSession<T>(startup: Bool = false, forceFocus: Bool = false, body: ()
return result
}

func refreshAndLayout(startup: Bool = false) {
refreshSession(startup: startup, body: {})
func refreshAndLayout(screenIsDefinitelyUnlocked: Bool, startup: Bool = false) {
refreshSession(screenIsDefinitelyUnlocked: screenIsDefinitelyUnlocked, startup: startup, body: {})
}

func refreshModel() {
Expand All @@ -66,12 +67,12 @@ func gcWindows() {
// recovering from unlock
if toKill.count == MacWindow.allWindowsMap.count { return }
for window in toKill {
window.value.garbageCollect()
window.value.garbageCollect(skipClosedWindowsCache: false)
}
}

func refreshObs(_ obs: AXObserver, ax: AXUIElement, notif: CFString, data: UnsafeMutableRawPointer?) {
refreshAndLayout()
refreshAndLayout(screenIsDefinitelyUnlocked: false)
}

enum OptimalHideCorner {
Expand Down
3 changes: 2 additions & 1 deletion Sources/AppBundle/mouse/moveWithMouse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ func movedObs(_ obs: AXObserver, ax: AXUIElement, notif: CFString, data: UnsafeM
if let window = data?.window, TrayMenuModel.shared.isEnabled {
moveWithMouseIfTheCase(window)
}
refreshAndLayout()
refreshAndLayout(screenIsDefinitelyUnlocked: false)
}

private func moveWithMouseIfTheCase(_ window: Window) { // todo cover with tests
Expand All @@ -15,6 +15,7 @@ private func moveWithMouseIfTheCase(_ window: Window) { // todo cover with tests
{
return
}
resetClosedWindowsCache()
switch window.parent.cases {
case .workspace:
moveFloatingWindow(window)
Expand Down
5 changes: 3 additions & 2 deletions Sources/AppBundle/mouse/resizeWithMouse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ func resizedObs(_ obs: AXObserver, ax: AXUIElement, notif: CFString, data: Unsaf
if let window = data?.window, TrayMenuModel.shared.isEnabled {
resizeWithMouseIfTheCase(window)
}
refreshAndLayout()
refreshAndLayout(screenIsDefinitelyUnlocked: false)
}

func resetManipulatedWithMouseIfPossible() {
Expand All @@ -15,7 +15,7 @@ func resetManipulatedWithMouseIfPossible() {
for workspace in Workspace.all {
workspace.resetResizeWeightBeforeResizeRecursive()
}
refreshAndLayout()
refreshAndLayout(screenIsDefinitelyUnlocked: true)
}
}

Expand All @@ -29,6 +29,7 @@ private func resizeWithMouseIfTheCase(_ window: Window) { // todo cover with tes
{
return
}
resetClosedWindowsCache()
switch window.parent.cases {
case .workspace, .macosMinimizedWindowsContainer, .macosFullscreenWindowsContainer,
.macosPopupWindowsContainer, .macosHiddenAppsWindowsContainer:
Expand Down
2 changes: 1 addition & 1 deletion Sources/AppBundle/server.swift
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ private func newConnection(_ socket: Socket) async { // todo add exit codes
}
if let command {
let _answer: Result<ServerAnswer, Error> = await Task { @MainActor in
refreshSession {
refreshSession(screenIsDefinitelyUnlocked: false) {
let cmdResult = command.run(.defaultEnv, CmdStdin(request.stdin)) // todo pass AEROSPACE_ env vars from CLI instead of defaultEnv
return ServerAnswer(
exitCode: cmdResult.exitCode,
Expand Down
8 changes: 4 additions & 4 deletions Sources/AppBundle/tree/MacApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,24 +34,24 @@ final class MacApp: AbstractApp {
allAppsMap[pid] = app
return app
} else {
app.garbageCollect()
app.garbageCollect(skipClosedWindowsCache: true)
return nil
}
}
}

private func garbageCollect() {
private func garbageCollect(skipClosedWindowsCache: Bool) {
MacApp.allAppsMap.removeValue(forKey: nsApp.processIdentifier)
for obs in axObservers {
AXObserverRemoveNotification(obs.obs, obs.ax, obs.notif)
}
MacWindow.allWindows.lazy.filter { $0.app == self }.forEach { $0.garbageCollect() }
MacWindow.allWindows.lazy.filter { $0.app == self }.forEach { $0.garbageCollect(skipClosedWindowsCache: skipClosedWindowsCache) }
axObservers = []
}

static func garbageCollectTerminatedApps() {
for app in Array(allAppsMap.values) where app.nsApp.isTerminated {
app.garbageCollect()
app.garbageCollect(skipClosedWindowsCache: true)
}
}

Expand Down
10 changes: 7 additions & 3 deletions Sources/AppBundle/tree/MacWindow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ final class MacWindow: Window, CustomStringConvertible {
tryOnWindowDetected(window, startup: startup)
return window
} else {
window.garbageCollect()
window.garbageCollect(skipClosedWindowsCache: true)
return nil
}
}
Expand All @@ -58,7 +58,10 @@ final class MacWindow: Window, CustomStringConvertible {
return "Window(\(description))"
}

func garbageCollect() {
// skipClosedWindowsCache is an optimization when it's definitely not necessary to cache closed window.
// If you are unsure, it's better to pass `false`
func garbageCollect(skipClosedWindowsCache: Bool) {
if !skipClosedWindowsCache { cacheClosedWindowIfNeeded(window: self) }
if MacWindow.allWindowsMap.removeValue(forKey: windowId) == nil {
return
}
Expand All @@ -74,7 +77,8 @@ final class MacWindow: Window, CustomStringConvertible {
if let workspace, workspace == focus.workspace.name || workspace == prevFocusedWorkspace?.name {
switch parent.cases {
case .tilingContainer, .workspace, .macosHiddenAppsWindowsContainer, .macosFullscreenWindowsContainer:
refreshSession(forceFocus: focus.windowOrNil?.app != app) {
// todo is this refreshSession necessary? garbageCollect should already be called in a refreshSession
refreshSession(screenIsDefinitelyUnlocked: false, forceFocus: focus.windowOrNil?.app != app) {
_ = Workspace.get(byName: workspace).focusWorkspace()
}
case .macosPopupWindowsContainer, .macosMinimizedWindowsContainer:
Expand Down
11 changes: 11 additions & 0 deletions Sources/AppBundle/tree/frozen/FrozenTree.swift
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,17 @@ private func restoreTreeRecursive(frozenContainer: FrozenContainer, parent: NonL
}
}

// Consider the following case:
// 1. Close window
// 2. The previous step lead to caching the whole world
// 3. Change something in the layout
// 4. Lock the screen
// 5. The cache won't be updated because all alive windows are already cached
// 6. Unlock the screen
// 7. The wrong cache is used
//
// That's why we have to reset the cache every time layout changes. The layout can only be changed by running commands
// and with mouse manipulations
func resetClosedWindowsCache() {
closedWindowsCache = FrozenWorld(workspaces: [], monitors: [])
}

0 comments on commit 46bf611

Please sign in to comment.