Skip to content

Feat: automatically-quit-apps-with-zero-windows #962

Open
@nikitabobko

Description

Draft:

diff --git a/Sources/AppBundle/config/Config.swift b/Sources/AppBundle/config/Config.swift
index 2c0aad91..19009368 100644
--- a/Sources/AppBundle/config/Config.swift
+++ b/Sources/AppBundle/config/Config.swift
@@ -40,6 +40,7 @@ struct Config: Copyable {
     var defaultRootContainerOrientation: DefaultContainerOrientation = .auto
     var startAtLogin: Bool = false
     var automaticallyUnhideMacosHiddenApps: Bool = false
+    var automaticallyQuitAppsWithZeroWindows: AutomaticallyQuitAppsWithZeroWindows = .off
     var accordionPadding: Int = 30
     var enableNormalizationOppositeOrientationForNestedContainers: Bool = true
     var execOnWorkspaceChange: [String] = [] // todo deprecate
@@ -58,6 +59,12 @@ struct Config: Copyable {
     var preservedWorkspaceNames: [String] = []
 }

+enum AutomaticallyQuitAppsWithZeroWindows: String, CaseIterable {
+    case off
+    case all
+    case allExceptFinder = "all-except-finder"
+}
+
 enum DefaultContainerOrientation: String {
     case horizontal, vertical, auto
 }
diff --git a/Sources/AppBundle/config/parseConfig.swift b/Sources/AppBundle/config/parseConfig.swift
index bca2b5d6..bd9bef95 100644
--- a/Sources/AppBundle/config/parseConfig.swift
+++ b/Sources/AppBundle/config/parseConfig.swift
@@ -102,6 +102,7 @@ private let configParser: [String: any ParserProtocol<Config>] = [

     "start-at-login": Parser(\.startAtLogin, parseBool),
     "automatically-unhide-macos-hidden-apps": Parser(\.automaticallyUnhideMacosHiddenApps, parseBool),
+    "automatically-quit-apps-with-zero-windows": Parser(\.automaticallyQuitAppsWithZeroWindows, parseAutomaticallyQuitApps),
     "accordion-padding": Parser(\.accordionPadding, parseInt),
     "exec-on-workspace-change": Parser(\.execOnWorkspaceChange, parseExecOnWorkspaceChange),
     "exec": Parser(\.execConfig, parseExecConfig),
@@ -275,6 +276,11 @@ private func parseLayout(_ raw: TOMLValueConvertible, _ backtrace: TomlBacktrace
         .flatMap { $0.parseLayout().orFailure(.semantic(backtrace, "Can't parse layout '\($0)'")) }
 }

+private func parseAutomaticallyQuitApps(_ raw: TOMLValueConvertible, _ backtrace: TomlBacktrace) -> ParsedToml<AutomaticallyQuitAppsWithZeroWindows> {
+    parseString(raw, backtrace)
+        .flatMap { parseEnum($0, AutomaticallyQuitAppsWithZeroWindows.self).toParsedToml(backtrace) }
+}
+
 private func skipParsing<T>(_ value: T) -> (_ raw: TOMLValueConvertible, _ backtrace: TomlBacktrace) -> ParsedToml<T> {
     { _, _ in .success(value) }
 }
diff --git a/Sources/AppBundle/layout/refresh.swift b/Sources/AppBundle/layout/refresh.swift
index 141e50b7..ca5bc5ed 100644
--- a/Sources/AppBundle/layout/refresh.swift
+++ b/Sources/AppBundle/layout/refresh.swift
@@ -35,10 +35,28 @@ func refreshSession<T>(screenIsDefinitelyUnlocked: Bool, startup: Bool = false,
         updateTrayText()
         normalizeLayoutReason(startup: startup)
         layoutWorkspaces()
+
+        if config.automaticallyQuitAppsWithZeroWindows != .off { quitAppsWithZeroWindows() }
     }
     return result
 }

+func quitAppsWithZeroWindows() {
+    let exceptFinder = config.automaticallyQuitAppsWithZeroWindows == .allExceptFinder
+    let appsWithWindows = MacWindow.allWindowsMap.map { $0.value.app.pid }.toSet()
+    let frontmostApp = NSWorkspace.shared.frontmostApplication
+    let menuBarApp = NSWorkspace.shared.menuBarOwningApplication
+    for (pid, app) in MacApp.allAppsMap {
+        if pid == frontmostApp?.processIdentifier || pid == menuBarApp?.processIdentifier || appsWithWindows.contains(pid) {
+            continue
+        }
+        if exceptFinder && app.nsApp.bundleIdentifier == finderAppBundleId {
+            continue
+        }
+        app.nsApp.terminate()
+    }
+}
+
 func refreshAndLayout(screenIsDefinitelyUnlocked: Bool, startup: Bool = false) {
     refreshSession(screenIsDefinitelyUnlocked: screenIsDefinitelyUnlocked, startup: startup, body: {})
 }
diff --git a/Sources/AppBundle/util/appBundleUtil.swift b/Sources/AppBundle/util/appBundleUtil.swift
index 5802a18f..84b35fca 100644
--- a/Sources/AppBundle/util/appBundleUtil.swift
+++ b/Sources/AppBundle/util/appBundleUtil.swift
@@ -2,7 +2,8 @@ import AppKit
 import Common
 import Foundation

-let lockScreenAppBundleId = "com.apple.loginwindow"
+let lockScreenAppBundleId = "com.apple.loginwindow" // /System/Library/CoreServices/[loginwindow.app](http://loginwindow.app/)
+let finderAppBundleId = "com.apple.finder" // /System/Library/CoreServices/Finder.app
 let AEROSPACE_WINDOW_ID = "AEROSPACE_WINDOW_ID" // env var
 let AEROSPACE_WORKSPACE = "AEROSPACE_WORKSPACE" // env var

diff --git a/docs/config-examples/default-config.toml b/docs/config-examples/default-config.toml
index 705a4e9c..9147796c 100644
--- a/docs/config-examples/default-config.toml
+++ b/docs/config-examples/default-config.toml
@@ -43,6 +43,10 @@ on-focused-monitor-changed = ['move-mouse monitor-lazy-center']
 # Also see: https://nikitabobko.github.io/AeroSpace/goodies#disable-hide-app
 automatically-unhide-macos-hidden-apps = false

+# Quit apps when they have zero windows and when they are unfocused.
+# Possible values: off|all-except-finder|all
+automatically-quit-apps-with-zero-windows = 'off'
+
 # Possible values: (qwerty|dvorak)
 # See https://nikitabobko.github.io/AeroSpace/guide#key-mapping
 [key-mapping]

Metadata

Assignees

No one assigned

    Labels

    feature-proposalA well defined feature proposaltriagedThe issue makes sense to maintainers

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions