```
├── .bundle/
├── config
├── .editorconfig (omitted)
├── .gitattributes (omitted)
├── .github/
├── DISCUSSION_TEMPLATE/
├── potential-bugs.yml (100 tokens)
├── FUNDING.yml
├── ISSUE_TEMPLATE/
├── config.yml
├── new-issue.yml (100 tokens)
├── gh-actions-runner-xcode-select.sh (100 tokens)
├── pull_request_template.md (200 tokens)
├── workflows/
├── build.yml (500 tokens)
├── close-third-party-issues.yml (200 tokens)
├── label-incoming-prs.yml (200 tokens)
├── .gitignore (100 tokens)
├── .idea/
├── codeStyles/
├── Project.xml (100 tokens)
├── codeStyleConfig.xml
├── inspectionProfiles/
├── Project_Default.xml (100 tokens)
├── .swift-version
├── .swiftformat (300 tokens)
├── .swiftlint.yml (400 tokens)
├── CONTRIBUTING.md (400 tokens)
├── Gemfile (100 tokens)
├── LICENSE.txt (200 tokens)
├── Package.resolved (200 tokens)
├── Package.swift (600 tokens)
├── README.md (2.1k tokens)
├── Sources/
├── AeroSpaceApp/
├── AeroSpaceApp.swift (100 tokens)
├── AppBundle/
├── GlobalObserver.swift (900 tokens)
├── command/
├── CmdEnv.swift (200 tokens)
├── CmdIo.swift (300 tokens)
├── Command.swift (200 tokens)
├── cmdManifest.swift (1000 tokens)
├── cmdResolveTargetOrReportError.swift (300 tokens)
├── format.swift (2.1k tokens)
├── formatToJson.swift (200 tokens)
├── impl/
├── BalanceSizesCommand.swift (200 tokens)
├── CloseAllWindowsButCurrentCommand.swift (200 tokens)
├── CloseCommand.swift (200 tokens)
├── ConfigCommand.swift (1400 tokens)
├── DebugWindowsCommand.swift (1200 tokens)
├── EchoCommand.swift (300 tokens)
├── EnableCommand.swift (300 tokens)
├── EvalCommand.swift (100 tokens)
├── ExecAndForgetCommand.swift (100 tokens)
├── FalseCommand.swift
├── FlattenWorkspaceTreeCommand.swift (100 tokens)
├── FocusBackAndForthCommand.swift (100 tokens)
├── FocusCommand.swift (1900 tokens)
├── FocusMonitorCommand.swift (700 tokens)
├── FullscreenCommand.swift (300 tokens)
├── JoinWithCommand.swift (300 tokens)
├── LayoutCommand.swift (1200 tokens)
├── ListAppsCommand.swift (200 tokens)
├── ListExecEnvVarsCommand.swift (100 tokens)
├── ListModesCommand.swift (100 tokens)
├── ListMonitorsCommand.swift (300 tokens)
├── ListWindowsCommand.swift (600 tokens)
├── ListWorkspacesCommand.swift (500 tokens)
├── MacosNativeFullscreenCommand.swift (500 tokens)
├── MacosNativeMinimizeCommand.swift (200 tokens)
├── ModeCommand.swift (100 tokens)
├── MoveCommand.swift (1700 tokens)
├── MoveMouseCommand.swift (600 tokens)
├── MoveNodeToMonitorCommand.swift (300 tokens)
├── MoveNodeToWorkspaceCommand.swift (400 tokens)
├── MoveWorkspaceToMonitorCommand.swift (300 tokens)
├── ReloadConfigCommand.swift (600 tokens)
├── ResizeCommand.swift (500 tokens)
├── RunCallbackCommand.swift (200 tokens)
├── SplitCommand.swift (500 tokens)
├── SummonWorkspaceCommand.swift (300 tokens)
├── SwapCommand.swift (400 tokens)
├── TestCommand.swift (500 tokens)
├── TestNotCommand.swift (100 tokens)
├── TriggerBindingCommand.swift (200 tokens)
├── TrueCommand.swift (100 tokens)
├── VolumeCommand.swift (200 tokens)
├── WorkspaceBackAndForthCommand.swift (100 tokens)
├── WorkspaceCommand.swift (600 tokens)
├── parseCommand.swift (300 tokens)
├── config/
├── Config.swift (600 tokens)
├── ConfigFile.swift (300 tokens)
├── ConfigFileWatcher.swift (300 tokens)
├── DynamicConfigValue.swift (700 tokens)
├── HotkeyBinding.swift (1000 tokens)
├── Mode.swift (300 tokens)
├── keysMap.swift (1900 tokens)
├── parseConfig.swift (4.4k tokens)
├── parseExecEnvVariables.swift (500 tokens)
├── parseFocusFollowsMouse.swift (100 tokens)
├── parseGaps.swift (800 tokens)
├── parseKeyMapping.swift (500 tokens)
├── parseOnWindowDetected.swift (1200 tokens)
├── parseWorkspaceToMonitorAssignment.swift (300 tokens)
├── startAtLogin.swift (200 tokens)
├── focus.swift (1600 tokens)
├── focusCache.swift (100 tokens)
├── getNativeFocusedWindow.swift (100 tokens)
├── initAppBundle.swift (800 tokens)
├── layout/
├── layoutRecursive.swift (1800 tokens)
├── refresh.swift (1600 tokens)
├── model/
├── AxUiElementWindowType.swift (1900 tokens)
├── Json.swift (500 tokens)
├── KnownBundleId.swift (300 tokens)
├── MonitorDescriptionEx.swift (100 tokens)
├── MonitorEx.swift (200 tokens)
├── MonitorInfo.swift (800 tokens)
├── OrderedJson.swift (500 tokens)
├── Rect.swift (400 tokens)
├── ServerEvent.swift (500 tokens)
├── mouse/
├── focusFollowsMouse.swift (600 tokens)
├── mouse.swift (100 tokens)
├── moveWithMouse.swift (900 tokens)
├── resizeWithMouse.swift (1000 tokens)
├── normalizeLayoutReason.swift (700 tokens)
├── runLoop.swift (600 tokens)
├── server.swift (1200 tokens)
├── shell/
├── Shell.swift (1300 tokens)
├── shellLexer.swift (1500 tokens)
├── shellParser.swift (900 tokens)
├── subscriptions.swift (400 tokens)
├── tree/
├── AbstractApp.swift (100 tokens)
├── FloatingWindowsContainer.swift
├── MacApp.swift (3.8k tokens)
├── MacWindow.swift (2.9k tokens)
├── MacosUnconventionalWindowsContainer.swift (300 tokens)
├── TilingContainer.swift (500 tokens)
├── TreeNode.swift (1300 tokens)
├── TreeNodeCases.swift (1900 tokens)
├── TreeNodeEx.swift (800 tokens)
├── Window.swift (600 tokens)
├── Workspace.swift (1600 tokens)
├── WorkspaceEx.swift (500 tokens)
├── frozen/
├── FrozenTreeNode.swift (300 tokens)
├── FrozenWorld.swift (100 tokens)
├── closedWindowsCache.swift (1100 tokens)
├── normalizeContainers.swift (300 tokens)
├── ui/
├── AppearanceTheme.swift (100 tokens)
├── ExperimentalUISettings.swift (400 tokens)
├── MenuBar.swift (1300 tokens)
├── MenuBarLabel.swift (1300 tokens)
├── MessageView.swift (1000 tokens)
├── NSPanelHud.swift (100 tokens)
├── SecureInputView.swift (600 tokens)
├── TrayMenuModel.swift (800 tokens)
├── VolumeView.swift (600 tokens)
├── util/
├── AwaitableOneTimeBroadcastLatch.swift (300 tokens)
├── AxSubscription.swift (400 tokens)
├── AxUiElementMock.swift (100 tokens)
├── CompletableFuture.swift (100 tokens)
├── LazySequenceProtocolEx.swift
├── MruStack.swift (300 tokens)
├── NSRunningApplicationEx.swift
├── NsApplicationEx.swift (100 tokens)
├── SetEx.swift (100 tokens)
├── ThreadGuardedValue.swift (200 tokens)
├── UniqueToken.swift (100 tokens)
├── accessibility.swift (2.5k tokens)
├── appBundleUtil.swift (1000 tokens)
├── axTrustedCheckOptionPrompt.swift
├── dumpAxRecursive.swift (1000 tokens)
├── windowLevelCache.swift (400 tokens)
├── AppBundleTests/
├── AwaitableOneTimeBroadcastLatchTest.swift (800 tokens)
├── AxUiElementWindowTypeTest.swift (600 tokens)
├── assert.swift (800 tokens)
├── command/
├── BalanceSizesCommandTest.swift (200 tokens)
├── CloseCommandTest.swift (300 tokens)
├── ConfigCommandTest.swift (2.1k tokens)
├── EchoCommandTest.swift (1100 tokens)
├── EvalCommandTest.swift (800 tokens)
├── ExecCommandTest.swift (100 tokens)
├── FalseCommandTest.swift (100 tokens)
├── FlattenWorkspaceTreeCommandTest.swift (200 tokens)
├── FocusCommandTest.swift (2.5k tokens)
├── FocusMonitorCommandTest.swift (300 tokens)
├── FormatTest.swift (4.3k tokens)
├── JoinWithCommandTest.swift (200 tokens)
├── LayoutCommandTest.swift (3.1k tokens)
├── ListAppsTest.swift (100 tokens)
├── ListModesTest.swift (600 tokens)
├── ListMonitorsTest.swift (100 tokens)
├── ListWindowsTest.swift (2.1k tokens)
├── ListWorkspacesTest.swift (1200 tokens)
├── ModeCommandTest.swift (200 tokens)
├── MoveCommandTest.swift (2.8k tokens)
├── MoveNodeToMonitorCommandTest.swift (300 tokens)
├── MoveNodeToWorkspaceCommandTest.swift (1700 tokens)
├── MoveWorkspaceToMonitorCommandTest.swift (200 tokens)
├── ResizeCommandTest.swift (1500 tokens)
├── RunCallbackCommandTest.swift (500 tokens)
├── SplitCommandTest.swift (500 tokens)
├── SubscribeCmdArgsTest.swift (500 tokens)
├── SummonWorkspaceCommandTest.swift (200 tokens)
├── SwapCommandTest.swift (1300 tokens)
├── TestCommandTest.swift (2000 tokens)
├── TriggerBindingCommandTest.swift (200 tokens)
├── TrueCommandTest.swift (100 tokens)
├── WorkspaceCommandTest.swift (1800 tokens)
├── config/
├── ConfigTest.swift (5.6k tokens)
├── ParseEnvVariablesTest.swift (500 tokens)
├── model/
├── ClientServerTest.swift (1000 tokens)
├── shell/
├── ShellLexerTest.swift (1600 tokens)
├── ShellParserTest.swift (1400 tokens)
├── ShellRunTest.swift (1100 tokens)
├── testUtil.swift (700 tokens)
├── tree/
├── OnWindowDetectedTest.swift (400 tokens)
├── TestApp.swift (200 tokens)
├── TestWindow.swift (300 tokens)
├── TilingContainer.swift (100 tokens)
├── TreeNodeTest.swift (700 tokens)
├── Cli/
├── _main.swift (1500 tokens)
├── cliUtil.swift
├── subcommandDescriptionsGenerated.swift (700 tokens)
├── Common/
├── appMetadata.swift (100 tokens)
├── cmdArgs/
├── ArgParser.swift (700 tokens)
├── ArgParserInput.swift (200 tokens)
├── ExitCode.swift (300 tokens)
├── ParsedCliArgs.swift (200 tokens)
├── ParsedCmd.swift (300 tokens)
├── SubArgParser.swift (300 tokens)
├── cmdArgsManifest.swift (1400 tokens)
├── impl/
├── BalanceSizesCmdArgs.swift (100 tokens)
├── CloseAllWindowsButCurrentCmdArgs.swift (100 tokens)
├── CloseCmdArgs.swift (100 tokens)
├── ConfigCmdArgs.swift (500 tokens)
├── DebugWindowsCmdArgs.swift (100 tokens)
├── EchoCmdArgs.swift (200 tokens)
├── EnableCmdArgs.swift (300 tokens)
├── EvalCmdArgs.swift (200 tokens)
├── ExecAndForgetCmdArgs.swift (100 tokens)
├── FalseCmdArgs.swift (100 tokens)
├── FlattenWorkspaceTreeCmdArgs.swift (100 tokens)
├── FocusBackAndForthCmdArgs.swift (100 tokens)
├── FocusCmdArgs.swift (1200 tokens)
├── FocusMonitorCmdArgs.swift (500 tokens)
├── FullscreenCmdArgs.swift (200 tokens)
├── JoinWithCmdArgs.swift (100 tokens)
├── LayoutCmdArgs.swift (700 tokens)
├── ListAppsCmdArgs.swift (400 tokens)
├── ListExecEnvVarsCmdArgs.swift (100 tokens)
├── ListModesCmdArgs.swift (200 tokens)
├── ListMonitorsCmdArgs.swift (300 tokens)
├── ListWindowsCmdArgs.swift (2.2k tokens)
├── ListWorkspacesCmdArgs.swift (800 tokens)
├── MacosNativeFullscreenCmdArgs.swift (300 tokens)
├── MacosNativeMinimizeCmdArgs.swift (100 tokens)
├── ModeCmdArgs.swift (100 tokens)
├── MoveCmdArgs.swift (500 tokens)
├── MoveMouseCmdArgs.swift (300 tokens)
├── MoveNodeToMonitorCmdArgs.swift (300 tokens)
├── MoveNodeToWorkspaceCmdArgs.swift (400 tokens)
├── MoveWorkpsaceToMonitorCmdArgs.swift (200 tokens)
├── ReloadConfigCmdArgs.swift (100 tokens)
├── ResizeCmdArgs.swift (400 tokens)
├── RunCallbackCmdArgs.swift (400 tokens)
├── SplitCmdArgs.swift (200 tokens)
├── SubscribeCmdArgs.swift (500 tokens)
├── SummonWorkspaceCmdArgs.swift (200 tokens)
├── SwapCmdArgs.swift (200 tokens)
├── TestCmdArgs.swift (500 tokens)
├── TestNotCmdArgs.swift (100 tokens)
├── TriggerBindingCmdArgs.swift (200 tokens)
├── TrueCmdArgs.swift (100 tokens)
├── VolumeCmdArgs.swift (300 tokens)
├── WorkspaceBackAndForthCmdArgs.swift (100 tokens)
├── WorkspaceCmdArgs.swift (600 tokens)
├── joinArgs.swift (200 tokens)
├── parseCmdArgs.swift (600 tokens)
├── parseSpecificCmdArgs.swift (600 tokens)
├── subcommandParsers.swift (100 tokens)
├── cmdHelpGenerated.swift (1700 tokens)
├── gitHashGenerated.swift
├── model/
├── AeroSpaceEnvVars.swift
├── AxAppThreadToken.swift (100 tokens)
├── CardinalDirection.swift (100 tokens)
├── CardinalOrDfsDirection.swift (200 tokens)
├── DfsNextPrev.swift
├── Init.swift (100 tokens)
├── MonitorDescription.swift (200 tokens)
├── NextPrev.swift
├── Orientation.swift (100 tokens)
├── WorkspaceName.swift (300 tokens)
├── clientServer.swift (500 tokens)
├── sponsorshipPrompts.swift (100 tokens)
├── util/
├── AeroAny.swift (200 tokens)
├── ArrSlice.swift (700 tokens)
├── ArrayEx.swift (300 tokens)
├── BoolEx.swift (100 tokens)
├── CaseInsensitiveRegex.swift (200 tokens)
├── CollectionEx.swift (100 tokens)
├── ComparableEx.swift
├── ConvenienceMutable.swift
├── DictionaryEx.swift (200 tokens)
├── EquatableNoop.swift (100 tokens)
├── JsonEncoderEx.swift (100 tokens)
├── Lateinit.swift (100 tokens)
├── MainActorEx.swift (100 tokens)
├── NWConnectionEx.swift (1000 tokens)
├── NonCopyable.swift (100 tokens)
├── Nullable.swift (100 tokens)
├── OptionalEx.swift (200 tokens)
├── ResultEx.swift (500 tokens)
├── SequenceEx.swift (1000 tokens)
├── StringEx.swift (1400 tokens)
├── StringLogicalSegments.swift (300 tokens)
├── TaskEx.swift (200 tokens)
├── commonUtil.swift (1400 tokens)
├── showMessageInGui.swift (200 tokens)
├── versionGenerated.swift
├── PrivateApi/
├── include/
├── module.modulemap
├── private.h (200 tokens)
├── private.m
├── axDumps/
├── 1password.json5 (1200 tokens)
├── 1password_large_type_window.json5 (1200 tokens)
├── 1password_mini_window.json5 (1200 tokens)
├── about_this_mac.json5 (1100 tokens)
├── alacritty_decorations_buttonless.json5 (1000 tokens)
├── apple_calendar.json5 (1300 tokens)
├── apple_calendar_settings.json5 (1100 tokens)
├── apple_followup_sign_in_to_a_new_device_confirmation.json5 (1000 tokens)
├── apple_mail.json5 (1500 tokens)
├── apple_mail_new_email.json5 (1200 tokens)
├── apple_mail_settings.json5 (1100 tokens)
├── archiveutility.json5 (1300 tokens)
├── brave.json5 (1200 tokens)
├── brave_pip.json5 (1100 tokens)
├── calculator.json5 (1000 tokens)
├── choose_1_5_0.json5 (700 tokens)
├── chrome.json5 (1100 tokens)
├── chrome_choose_what_to_share_popup.json5 (1200 tokens)
├── chrome_extensions_popup.json5 (600 tokens)
├── chrome_find_in_page.json5 (600 tokens)
├── chrome_pip.json5 (600 tokens)
├── chrome_sharing_is_in_progress_popup.json5 (600 tokens)
├── cleanshotx_monitor_1.json5 (700 tokens)
├── cleanshotx_monitor_2.json5 (700 tokens)
├── codex.json5 (1200 tokens)
├── codex_pet_window.json5 (700 tokens)
├── drracket.json5 (1300 tokens)
├── emacs.json5 (1300 tokens)
├── emacs_child_frame_corfu.json5 (600 tokens)
├── emacs_child_frame_posframe.json5 (600 tokens)
├── finder.json5 (1300 tokens)
├── finder_quick_look.json5 (900 tokens)
├── firefox.json5 (1100 tokens)
├── firefox_extensions_popup.json5 (700 tokens)
├── firefox_mouse_hover_over_extensions_button.json5 (600 tokens)
├── firefox_mouse_hover_over_tab.json5 (600 tokens)
├── firefox_non_native_fullscreen.json5 (600 tokens)
├── firefox_normal_window_when_non_native_fullscreen_in_background.json5 (1100 tokens)
├── firefox_pinterest_sign_in_with_google.json5 (1100 tokens)
├── firefox_pip.json5 (1100 tokens)
├── ghostty.json5 (1100 tokens)
├── ghostty_about.json5 (900 tokens)
├── ghostty_check_for_updates_1_dialog.json5 (700 tokens)
├── ghostty_check_for_updates_2_alert.json5 (800 tokens)
├── ghostty_config_error.json5 (1000 tokens)
├── ghostty_quick_terminal.json5 (800 tokens)
├── ghostty_window_decorations_false.json5 (600 tokens)
├── intellij.json5 (1500 tokens)
├── intellij_background_tasks.json5 (900 tokens)
├── intellij_context_menu.json5 (800 tokens)
├── intellij_native_open_window.json5 (800 tokens)
├── intellij_quick_doc_popup.json5 (900 tokens)
├── intellij_rebase_dialog.json5 (1200 tokens)
├── iphonesimulator.json5 (1300 tokens)
├── iterm2.json5 (1200 tokens)
├── iterm2_hotkey_window.json5 (1100 tokens)
├── iterm2_no_title_bar.json5 (1200 tokens)
├── jetbrains_toolbox.json5 (800 tokens)
├── karabiner_event_viewer.json5 (1300 tokens)
├── karabiner_settings.json5 (1600 tokens)
├── kitty_quick_access.json5 (700 tokens)
├── macos_capslock_popup_safari.json5 (700 tokens)
├── macos_capslock_popup_textedit.json5 (700 tokens)
├── macos_join_network.json5 (1200 tokens)
├── macos_share_window_purple_pill_sublime.json5 (800 tokens)
├── marta.json5 (1100 tokens)
├── microsoft_edge.json5 (1600 tokens)
├── microsoft_edge_pip.json5 (1100 tokens)
├── mpv_fullscreen.json5 (700 tokens)
├── mpv_windowed.json5 (1300 tokens)
├── nomachine_session_1.json5 (600 tokens)
├── nomachine_session_2.json5 (1000 tokens)
├── nomachine_welcome_window_1.json5 (800 tokens)
├── nomachine_welcome_window_2.json5 (1200 tokens)
├── outlook_reminder.json5 (1200 tokens)
├── qutebrowser.json5 (1300 tokens)
├── qutebrowser_context_menu.json5 (700 tokens)
├── qutebrowser_hide_decoration.json5 (800 tokens)
├── raycast.json5 (700 tokens)
├── raycast_settings.json5 (1100 tokens)
├── safari.json5 (1100 tokens)
├── safari_pinterest_sign_in_with_google.json5 (1400 tokens)
├── scenario_firefox_google_meet_share_window/
├── 01_firefox.json5 (300 tokens)
├── 02_firefox.json5 (600 tokens)
├── 03_firefox.json5 (300 tokens)
├── 04_firefox.json5 (600 tokens)
├── 05_apple_controlcenter.json5 (300 tokens)
├── 06_firefox.json5 (400 tokens)
├── 07_firefox.json5 (600 tokens)
├── README.md (100 tokens)
├── slack.json5 (1300 tokens)
├── slack_chat_in_a_separate_window.json5 (1300 tokens)
├── slack_huddle_share_screen_draw_on_screen_fake_window.json5 (1100 tokens)
├── slack_huddle_share_screen_floating_popup.json5 (1100 tokens)
├── slack_huddle_share_screen_target_picker.json5 (700 tokens)
├── spotify.json5 (1100 tokens)
├── spotify_miniplayer.json5 (1100 tokens)
├── steam_1.json5 (800 tokens)
├── steam_2.json5 (700 tokens)
├── sublime_text_4.json5 (1000 tokens)
├── system_settings.json5 (1200 tokens)
├── telegram.json5 (1100 tokens)
├── telegram_image_viewer.json5 (600 tokens)
├── terminal_app.json5 (1100 tokens)
├── transmission.json5 (1100 tokens)
├── transmission_torrent_inspector.json5 (1200 tokens)
├── vlc_empty.json5 (1100 tokens)
├── vlc_fullscreen.json5 (700 tokens)
├── vlc_video_playing.json5 (1100 tokens)
├── vs_code.json5 (1300 tokens)
├── vs_code_nativeFullScreen_false.json5 (1200 tokens)
├── vs_codium.json5 (1300 tokens)
├── vs_codium_nativeFullScreen_false.json5 (1200 tokens)
├── wisprFlow1.json5 (800 tokens)
├── wisprFlow2.json5 (800 tokens)
├── xcode.json5 (1300 tokens)
├── xcode_build_succeeded_popup.json5 (800 tokens)
├── xcode_installing_system_components.json5 (500 tokens)
├── xcode_open_quickly.json5 (1100 tokens)
├── xcode_quick_actions.json5 (1100 tokens)
├── xcode_settings.json5 (1300 tokens)
├── xcode_welcome_window.json5 (700 tokens)
├── zebar.json5 (600 tokens)
├── zed.json5 (1100 tokens)
├── zen_browser.json5 (1200 tokens)
├── zen_browser_pip.json5 (1300 tokens)
├── build-debug.sh (100 tokens)
├── build-docs.sh (400 tokens)
├── build-release.sh (900 tokens)
├── build-shell-completion.sh (100 tokens)
├── dev-docs/
├── architecture.md (500 tokens)
├── development.md (900 tokens)
├── docs/
├── aerospace-balance-sizes.adoc (200 tokens)
├── aerospace-close-all-windows-but-current.adoc (200 tokens)
├── aerospace-close.adoc (200 tokens)
├── aerospace-config.adoc (500 tokens)
├── aerospace-debug-windows.adoc (300 tokens)
├── aerospace-echo.adoc (400 tokens)
├── aerospace-enable.adoc (200 tokens)
├── aerospace-eval.adoc (300 tokens)
├── aerospace-exec-and-forget.adoc (300 tokens)
├── aerospace-false.adoc (200 tokens)
├── aerospace-flatten-workspace-tree.adoc (200 tokens)
├── aerospace-focus-back-and-forth.adoc (300 tokens)
├── aerospace-focus-monitor.adoc (300 tokens)
├── aerospace-focus.adoc (700 tokens)
├── aerospace-fullscreen.adoc (300 tokens)
├── aerospace-join-with.adoc (300 tokens)
├── aerospace-layout.adoc (500 tokens)
├── aerospace-list-apps.adoc (500 tokens)
├── aerospace-list-exec-env-vars.adoc (200 tokens)
├── aerospace-list-modes.adoc (200 tokens)
├── aerospace-list-monitors.adoc (500 tokens)
├── aerospace-list-windows.adoc (1000 tokens)
├── aerospace-list-workspaces.adoc (700 tokens)
├── aerospace-macos-native-fullscreen.adoc (300 tokens)
├── aerospace-macos-native-minimize.adoc (200 tokens)
├── aerospace-mode.adoc (200 tokens)
├── aerospace-move-mouse.adoc (400 tokens)
├── aerospace-move-node-to-monitor.adoc (400 tokens)
├── aerospace-move-node-to-workspace.adoc (400 tokens)
├── aerospace-move-workspace-to-monitor.adoc (400 tokens)
├── aerospace-move.adoc (600 tokens)
├── aerospace-reload-config.adoc (300 tokens)
├── aerospace-resize.adoc (300 tokens)
├── aerospace-run-callback.adoc (500 tokens)
├── aerospace-split.adoc (300 tokens)
├── aerospace-subscribe.adoc (500 tokens)
├── aerospace-summon-workspace.adoc (300 tokens)
├── aerospace-swap.adoc (300 tokens)
├── aerospace-test-not.adoc (200 tokens)
├── aerospace-test.adoc (800 tokens)
├── aerospace-trigger-binding.adoc (300 tokens)
├── aerospace-true.adoc (200 tokens)
├── aerospace-volume.adoc (200 tokens)
├── aerospace-workspace-back-and-forth.adoc (200 tokens)
├── aerospace-workspace.adoc (400 tokens)
├── aerospace.adoc (100 tokens)
├── assets/
├── h_accordion.png
├── h_tiles.png
├── icon.png
├── monitor-arrangement-1-bad.svg (19.8k tokens)
├── monitor-arrangement-1-good.svg (11.6k tokens)
├── monitor-arrangement-2-bad.svg (12.4k tokens)
├── monitor-arrangement-2-good.svg (7.5k tokens)
├── tree.png
├── v_accordion.png
├── commands.adoc (1600 tokens)
├── config-examples/
├── default-config.toml (1900 tokens)
├── i3-like-config-example.toml (600 tokens)
├── docinfo-footer.html (400 tokens)
├── goodies.adoc (1900 tokens)
├── guide.adoc (8.2k tokens)
├── index.html (100 tokens)
├── util/
├── all-monitors-option.adoc (100 tokens)
├── conditional-arguments-header.adoc
├── conditional-examples-header.adoc
├── conditional-exit-code-header.adoc
├── conditional-interpolation-variables-header.adoc
├── conditional-operators-header.adoc
├── conditional-options-header.adoc
├── conditional-output-format-header.adoc
├── header.adoc (100 tokens)
├── man-attributes.adoc
├── man-footer.adoc (100 tokens)
├── monitor-option.adoc (100 tokens)
├── site-attributes.adoc
├── window-id-flag-desc.adoc
├── workspace-flag-desc.adoc
├── format.sh
├── generate.sh (400 tokens)
├── grammar/
├── commands-bnf-grammar.txt (1500 tokens)
├── install-from-sources.sh (200 tokens)
├── legal/
├── LICENSE.txt
├── README.md (200 tokens)
├── third-party-license/
├── LICENSE-HotKey.txt (200 tokens)
├── LICENSE-ISSoundAdditions.txt (200 tokens)
├── LICENSE-TOMLDecoder.txt (200 tokens)
├── LICENSE-swift-collections.txt (2.4k tokens)
├── LICENSE-tomlplusplus.txt (200 tokens)
├── lint.sh (300 tokens)
├── makefile (100 tokens)
├── resources/
├── AeroSpace.entitlements
├── Assets.xcassets/
├── AccentColor.colorset/
├── Contents.json
├── AppIcon.appiconset/
├── Contents.json (200 tokens)
├── icon.png
├── Contents.json
├── run-cli.sh
├── run-debug.sh
├── script/
├── build-brew-cask.sh (600 tokens)
├── check-uncommitted-files.sh (100 tokens)
├── clean-project.sh
├── clean-xcode.sh
├── generate-cmd-help.sh (200 tokens)
├── install-dep.sh (800 tokens)
├── publish-release.sh (300 tokens)
├── reset-accessibility-permission-for-debug.sh (100 tokens)
├── setup.sh (600 tokens)
├── swift-test.sh (100 tokens)
├── test.sh (100 tokens)
├── xcode/
├── AeroSpace.xcodeproj/
├── project.pbxproj (2.7k tokens)
├── project.xcworkspace/
├── contents.xcworkspacedata
├── aerospace-package-symlink
├── project.yml (400 tokens)
```
## /.bundle/config
```bundle/config path="/.bundle/config"
---
BUNDLE_PATH: ".deps/bundler-path"
BUNDLE_DISABLE_SHARED_GEMS: "1"
```
## /.github/DISCUSSION_TEMPLATE/potential-bugs.yml
```yml path="/.github/DISCUSSION_TEMPLATE/potential-bugs.yml"
body:
- type: textarea
id: body
attributes:
label: Body
value: |
Steps to reproduce:
1.
2.
3.
Expected result:
Actual result:
### Additional info
\`\`\`shell
$ aerospace -v
\`\`\`
validations:
required: true
```
## /.github/FUNDING.yml
```yml path="/.github/FUNDING.yml"
github: [nikitabobko]
```
## /.github/ISSUE_TEMPLATE/config.yml
```yml path="/.github/ISSUE_TEMPLATE/config.yml"
blank_issues_enabled: false
```
## /.github/ISSUE_TEMPLATE/new-issue.yml
```yml path="/.github/ISSUE_TEMPLATE/new-issue.yml"
name: New Issue
description: New Issue
body:
- type: markdown
attributes:
value: |
AeroSpace project doesn't accept Issues directly. Please prefer GitHub Discussions. See: https://github.com/nikitabobko/AeroSpace/issues/947
- type: checkboxes
id: checkbox
attributes:
label: 'Sanity check'
options:
- label: |
I read https://github.com/nikitabobko/AeroSpace/issues/947
required: true
- type: textarea
id: body
attributes:
label: Body
value: |
YOUR ISSUE WILL BE IMMEDIATELY CLOSED BY BOT, DON'T OPEN NEW ISSUES DIRECTLY, USE DISCUSSIONS INSTEAD
validations:
required: true
```
## /.github/gh-actions-runner-xcode-select.sh
```sh path="/.github/gh-actions-runner-xcode-select.sh"
#!/usr/bin/env bash
set -e # Exit if one of commands exit with non-zero exit code
set -u # Treat unset variables and parameters other than the special parameters ‘@’ or ‘*’ as an error
set -o pipefail # Any command failed in the pipe fails the whole pipe
# set -x # Print shell commands as they are executed (or you can try -v which is less verbose)
sw_vers -productVersion
# Xcode version affects the target macOS SDK that we compile against + different Xcodes bundle different Swift verions
if sw_vers -productVersion | grep -q "^14"; then # macOS 14
sudo xcode-select -s "$XCODE_16_DEVELOPER_DIR"
else
sudo xcode-select -s "$XCODE_26_DEVELOPER_DIR"
fi
```
## /.github/pull_request_template.md
## PR checklist
- [ ] Explain your changes in the relevant commit messages rather than in the PR description. The PR description must not contain more information than the commit messages (except for images and other media).
- [ ] Each commit must explain what/why/how and motivation in its description. https://cbea.ms/git-commit/
- [ ] Don't forget to link the appropriate issues/discussions in commit messages (if applicable).
- [ ] Each commit must be an atomic change (a PR may contain several commits). Don't introduce new functional changes together with refactorings in the same commit.
- [ ] `./test.sh` exits with non-zero exit code.
- [ ] Avoid merge commits, always rebase and force push.
Failure to follow the checklist with no apparent reasons will result in silent PR rejection.
## /.github/workflows/build.yml
```yml path="/.github/workflows/build.yml"
name: build
on:
push:
branches:
- 'main'
- 'rr/**' # "rr" stands for "remote run"
pull_request:
branches: [ "main" ]
schedule:
- cron: '0 0 * * *' # every day at 00:00
jobs:
build-debug:
strategy:
# fail-fast: false # Disable fail-fast in matrix
matrix:
# https://docs.github.com/en/actions/reference/runners/github-hosted-runners#standard-github-hosted-runners-for-public-repositories
# https://github.com/actions/runner-images/blob/main/images/macos/macos-14-arm64-Readme.md
# https://github.com/actions/runner-images/blob/main/images/macos/macos-15-arm64-Readme.md
# https://github.com/actions/runner-images/blob/main/images/macos/macos-26-arm64-Readme.md
#
# Xcode versions:
# - https://en.wikipedia.org/wiki/Xcode
# - https://xcodereleases.com/?scope=release
os: [macos-14, macos-15, macos-26]
name: build-debug
runs-on: ${{ matrix.os }}
steps:
- run: env # Debug
- uses: actions/checkout@v6
- run: ./.github/gh-actions-runner-xcode-select.sh
- run: brew install swiftly bash
- run: swiftly init --skip-install --assume-yes --verbose && swiftly install
- run: swiftly run swift --version
- run: ./build-debug.sh
- run: ./test.sh
# We build release artifacts only on the latest macOS versions because:
# 1. It cuts the build time twice on GH Actions
# 2. The latest Xcode version is not available on old macOS, and old Xcode versions bundle too old Swift version
build-release:
strategy:
# fail-fast: false # Disable fail-fast in matrix
matrix:
os: [macos-15, macos-26]
name: build-release
runs-on: ${{ matrix.os }}
steps:
- run: env # Debug
- uses: actions/checkout@v6
- run: ./.github/gh-actions-runner-xcode-select.sh
- run: brew install bash fish xcbeautify swiftly
- run: swiftly init --skip-install --assume-yes --verbose && swiftly install
- run: swiftly run swift --version
- name: './build-release.sh'
run: |
# "-" means "Sign to run locally". There is no aerospace-codesign-certificate on GH Actions
./build-release.sh --codesign-identity -
brew trust nikitabobko/tap
./install-from-sources.sh --dont-rebuild
- name: cat ./.release/xcodebuild.log
if: ${{ always() }}
run: 'if test -f ./.release/xcodebuild.log; then cat ./.release/xcodebuild.log; fi'
```
## /.github/workflows/close-third-party-issues.yml
```yml path="/.github/workflows/close-third-party-issues.yml"
name: close-third-party-issues
on:
issues:
types: [opened]
jobs:
close-third-party-issues:
name: Close third party issues
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- name: Close third party issues
run: |
set -e # Exit if one of commands exit with non-zero exit code
set -u # Treat unset variables and parameters other than the special parameters ‘@’ or ‘*’ as an error
set -o pipefail # Any command failed in the pipe fails the whole pipe
author="$(gh issue view "$ISSUE" --json author --jq '.author.login')"
close() {
gh issue edit "$ISSUE" --add-label bin
gh issue close "$ISSUE" --comment "Please don't open issues directly, use GitHub Discussions instead. See: https://github.com/nikitabobko/AeroSpace/issues/947"
gh issue lock "$ISSUE"
}
test "$author" = nikitabobko || test "$author" = mobile-ar || test "$author" = rickyz || close
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
ISSUE: ${{ github.event.issue.number }}
```
## /.github/workflows/label-incoming-prs.yml
```yml path="/.github/workflows/label-incoming-prs.yml"
name: label-incoming-prs
on:
pull_request_target:
types: [opened]
jobs:
label-incoming-prs:
name: Label incoming PRs
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- name: Add not-actionable label
run: |
set -e # Exit if one of commands exit with non-zero exit code
set -u # Treat unset variables and parameters other than the special parameters ‘@’ or ‘*’ as an error
set -o pipefail # Any command failed in the pipe fails the whole pipe
author="$(gh pr view "$PR" --json author --jq '.author.login')"
label() {
gh pr edit "$PR" --add-label not-actionable
}
test "$author" = nikitabobko || test "$author" = mobile-ar || test "$author" = rickyz || label
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
PR: ${{ github.event.number }}
```
## /.gitignore
```gitignore path="/.gitignore"
/.idea
/.debug
/.release
/.shell-completion
/.site
/.man
/Gemfile.lock
# IDK, AppCode randomly creates this EMPTY file. I have no idea what this is
/default.profraw
.DS_Store
/xcode/.xcode-build
# Swift package manager
/.build
/.swiftpm
/.vscode
# External dependencies
/.deps
# For whatever local files that developers might want to keep there (I personally keep a separate `generated-html` git worktree here)
/.local
# XCode User settings
xcuserdata/
xcshareddata/
```
## /.idea/codeStyles/Project.xml
```xml path="/.idea/codeStyles/Project.xml"
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<SwiftCodeStyleSettings>
<option name="METHOD_CHAIN_INDENT" value="4" />
</SwiftCodeStyleSettings>
<codeStyleSettings language="Swift">
<option name="LINE_COMMENT_AT_FIRST_COLUMN" value="false" />
<option name="KEEP_SIMPLE_BLOCKS_IN_ONE_LINE" value="true" />
<option name="KEEP_SIMPLE_METHODS_IN_ONE_LINE" value="true" />
<option name="KEEP_SIMPLE_CLASSES_IN_ONE_LINE" value="true" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
</codeStyleSettings>
</code_scheme>
</component>
```
## /.idea/codeStyles/codeStyleConfig.xml
```xml path="/.idea/codeStyles/codeStyleConfig.xml"
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>
```
## /.idea/inspectionProfiles/Project_Default.xml
```xml path="/.idea/inspectionProfiles/Project_Default.xml"
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="SwiftUnnecessarySelfQualifier" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
</profile>
</component>
```
## /.swift-version
```swift-version path="/.swift-version"
6.3.2
```
## /.swiftformat
```swiftformat path="/.swiftformat"
# https://github.com/nicklockwood/SwiftFormat/blob/main/Rules.md
--indentcase true
--patternlet inline
--indentstrings true
# https://github.com/nicklockwood/SwiftFormat/issues/483 fix indentation for expressions nested in ternary
--wrapternary before-operators
--disable all
# --enable docComments
--enable anyObjectProtocol
--enable blankLineAfterImports
--enable blankLinesAtEndOfScope
--enable blankLinesBetweenImports
--enable conditionalAssignment
--enable docCommentsBeforeModifiers
--enable duplicateImports
--enable elseOnSameLine
--enable emptyBraces
--enable emptyExtensions
--enable enumNamespaces
--enable fileMacro
--enable genericExtensions
--enable indent
--enable leadingDelimiters
--enable linebreakAtEndOfFile
--enable linebreaks
--enable numberFormatting
--enable preferCountWhere
--enable preferFinalClasses
--enable redundantAsync
--enable redundantBackticks
--enable redundantBreak
--enable redundantClosure
--enable redundantEquatable
--enable redundantFileprivate
--enable redundantGet
--enable redundantLet
--enable redundantLetError
--enable redundantMemberwiseInit
--enable redundantObjc
--enable redundantOptionalBinding
--enable redundantThrows
--enable redundantTypedThrows
--enable redundantVoidReturnType
--enable semicolons
--enable spaceAroundBraces
--enable spaceAroundBrackets
--enable spaceAroundGenerics
--enable spaceAroundOperators
--enable spaceAroundParens
--enable spaceInsideBraces
--enable spaceInsideBrackets
--enable spaceInsideGenerics
--enable spaceInsideParens
--enable strongOutlets
--enable trailingClosures
--enable trailingCommas
--enable trailingSpace
--enable urlMacro
--enable wrapMultilineStatementBraces
--enable extensionAccessControl
--extensionacl on-declarations
```
## /.swiftlint.yml
```yml path="/.swiftlint.yml"
excluded:
- .build
- xcode
# https://realm.github.io/SwiftLint/rule-directory.html
only_rules:
# - line_length # todo fix it
- colon
- computed_accessors_order
- dynamic_inline # Avoid using ‘dynamic’ and ‘@inline(__always)’ together
- empty_enum_arguments
- empty_parameters # Prefer () -> over Void ->
- empty_string # Prefer checking isEmpty over comparing string to an empty string literal
- file_name_no_space
- first_where
- implicit_getter
- is_disjoint
- last_where # Prefer using .last(where:) over .filter { }.last in collections
- legacy_constant
- legacy_constructor # Swift constructors are preferred over legacy convenience functions
- legacy_hashing # Prefer using the hash(into:) function instead of overriding hashValue
- legacy_nsgeometry_functions # Struct extension properties and methods are preferred over legacy functions
- legacy_random # Prefer using type.random(in:) over legacy functions
- local_doc_comment # Prefer regular comments over doc comments in local scopes
- no_fallthrough_only
- no_space_in_method_call
- nsobject_prefer_isequal # NSObject subclasses should implement isEqual instead of ==
- function_name_whitespace
- optional_enum_case_matching
- redundant_discardable_let
- redundant_nil_coalescing # nil coalescing operator is only evaluated if the lhs is nil, coalescing operator with nil as rhs is redundant
- switch_case_alignment
- toggle_bool
- trailing_newline
- trailing_semicolon # Lines should not have trailing semicolons
- trailing_whitespace # Lines should not have trailing whitespace
- unavailable_condition # Use #unavailable/#available instead of #available/#unavailable with an empty body.
- unneeded_break_in_switch # Avoid using unneeded break statements
- unneeded_override # Remove overridden functions that don’t do anything except call their super
# - unused_closure_parameter
- unused_control_flow_label
- unused_enumerated # When the index or the item is not used, .enumerated() can be removed.
- unused_setter_value
- weak_delegate # Delegates should be weak to avoid reference cycles
switch_case_alignment:
indented_cases: true
strict: true
# quiet: true
```
## /CONTRIBUTING.md
# Contributing
The main and the most important rule: **read the room!**
* Does your patch look like typical commit in the repo?
* Does your commit message look like typical commit message in the repo?
* Does your test look like typical test in the repo?
* etc.
## Submiting bugs and feature ideas
Submit bugs to https://github.com/nikitabobko/AeroSpace/discussions/categories/potential-bugs
Submit feature ideas to https://github.com/nikitabobko/AeroSpace/discussions/categories/feature-ideas
Rules:
* Search for duplicates (in GitHub Issues and Discussions) before creating a new discussion
* Upvote for issues/discussions that you find useful
**Consider including in bug reports**
* `aerospace debug-windows` output, if the problem is about handling some windows
* Screenshots of problematic windows
* Videos of problematic windows
* What did you try to resolve the issue?
* Your config
* AeroSpace version
* macOS version
**Consider including in feature request**
* Use cases!
* Alternative approaches
* Links to docs of similar features in other window managers that you know
* Synopsis, if you suggest a new command
* Mental model description
## Submiting code
There are 2 options:
1. Send email patches to: `echo YWVyb3NwYWNlLXBhdGNoZXNAYm9ia28ueHl6Cg== | base64 --decode`
2. Send GitHub PRs
**License Agreement**. By contributing changes to this repository, you agree to license your contributions under the MIT license.
Maintainers can apply your patch with arbitrary modifications.
## Spread the word
Do you like the project? Does AeroSpace finally fix your problems with windows management on macOS? Good to hear it!
* Spread the word in social networks! (Don't forget to share the link :) )
* Talk about AeroSpace to your colleagues and friends
* Write a blogpost about your workflows
* Record a YouTube video
## Share your workflow and tips
Submit your tips to [the Goodies page](https://nikitabobko.github.io/AeroSpace/goodies). The source code of the page can be found in `./docs` directory
## Support the project financially
Supporting the project financially counts as a contribution (even if it's just a $1/month).
You can sponsor the project on GitHub Sponsors page: https://github.com/sponsors/nikitabobko
## /Gemfile
``` path="/Gemfile"
# frozen_string_literal: true
ruby '~> 3.0' # >= 3.0 and < 4.0
source "https://rubygems.org"
# https://github.com/asciidoctor/asciidoctor/releases
gem 'asciidoctor', '2.0.23'
# https://github.com/pygments/pygments.rb/releases
# https://github.com/pygments/pygments/releases
gem 'pygments.rb', git: 'https://github.com/nikitabobko/pygments.rb.git', ref: '1a4e502ee49c3fd93dae4abb171fea49f9d52561'
```
## /LICENSE.txt
MIT License
Copyright (c) 2023 Nikita Bobko
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
## /Package.resolved
```resolved path="/Package.resolved"
{
"originHash" : "6466a3de6b03a7f9499cc6ad528be037cd42ee688a16c00bd96a3fa208099b1e",
"pins" : [
{
"identity" : "hotkey",
"kind" : "remoteSourceControl",
"location" : "https://github.com/soffes/HotKey",
"state" : {
"revision" : "a3cf605d7a96f6ff50e04fcb6dea6e2613cfcbe4",
"version" : "0.2.1"
}
},
{
"identity" : "issoundadditions",
"kind" : "remoteSourceControl",
"location" : "https://github.com/InerziaSoft/ISSoundAdditions",
"state" : {
"revision" : "4b555f0354e6c280917bae8a598a258efe87ab98",
"version" : "2.0.1"
}
},
{
"identity" : "swift-collections",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-collections",
"state" : {
"revision" : "7b847a3b7008b2dc2f47ca3110d8c782fb2e5c7e",
"version" : "1.3.0"
}
},
{
"identity" : "tomldecoder",
"kind" : "remoteSourceControl",
"location" : "https://github.com/dduan/TOMLDecoder",
"state" : {
"revision" : "fa1103a62b1c08cc08eabc58dda8b55887ff1622",
"version" : "0.4.4"
}
}
],
"version" : 3
}
```
## /Package.swift
```swift path="/Package.swift"
// swift-tools-version: 6.2
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let swiftSettings: [SwiftSetting] = [
.enableUpcomingFeature("NonisolatedNonsendingByDefault"),
.strictMemorySafety(),
]
let package = Package(
name: "AeroSpacePackage",
// Runtime support for parameterized protocol types is only available in macOS 13.0.0 or newer
// And it specifies deploymentTarget for CLI
platforms: [.macOS(.v13)],
// Products define the executables and libraries a package produces, making them visible to other packages.
products: [
.executable(name: "aerospace", targets: ["Cli"]),
// Don't use this build for release, use xcode instead
.executable(name: "AeroSpaceApp", targets: ["AeroSpaceApp"]),
// We only need to expose this as a product for xcode
.library(name: "AppBundle", targets: ["AppBundle"]),
],
dependencies: [
.package(url: "https://github.com/InerziaSoft/ISSoundAdditions.git", exact: "2.0.1"),
.package(url: "https://github.com/dduan/TOMLDecoder", exact: "0.4.4"),
.package(url: "https://github.com/apple/swift-collections.git", exact: "1.3.0"),
.package(url: "https://github.com/soffes/HotKey.git", exact: "0.2.1"),
],
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
targets: [
// Exposes the private _AXUIElementGetWindow function to swift
.target(
name: "PrivateApi",
path: "Sources/PrivateApi",
publicHeadersPath: "include",
),
.target(
name: "Common",
dependencies: [
.product(name: "Collections", package: "swift-collections"),
],
swiftSettings: swiftSettings,
),
.target(
name: "AppBundle",
dependencies: [
.product(name: "Collections", package: "swift-collections"),
.product(name: "HotKey", package: "HotKey"),
.product(name: "ISSoundAdditions", package: "ISSoundAdditions"),
.product(name: "TOMLDecoder", package: "TOMLDecoder"),
.target(name: "Common"),
.target(name: "PrivateApi"),
],
swiftSettings: swiftSettings,
),
.executableTarget(
name: "AeroSpaceApp",
dependencies: [
.target(name: "AppBundle"),
],
swiftSettings: swiftSettings,
),
.executableTarget(
name: "Cli",
dependencies: [
.target(name: "Common"),
],
swiftSettings: swiftSettings,
),
.testTarget(
name: "AppBundleTests",
dependencies: [
.target(name: "AppBundle"),
],
swiftSettings: swiftSettings,
),
],
)
```
## /README.md
# AeroSpace Beta [](https://github.com/nikitabobko/AeroSpace/actions/workflows/build.yml)
<img src="./resources/Assets.xcassets/AppIcon.appiconset/icon.png" width="40%" align="right">
AeroSpace is an i3-like tiling window manager for macOS
Videos:
- [YouTube 91 sec Demo](https://www.youtube.com/watch?v=UOl7ErqWbrk)
- [YouTube Guide by Josean Martinez](https://www.youtube.com/watch?v=-FoWClVHG5g)
Docs:
- [AeroSpace Guide](https://nikitabobko.github.io/AeroSpace/guide)
- [AeroSpace Commands](https://nikitabobko.github.io/AeroSpace/commands)
- [AeroSpace Goodies](https://nikitabobko.github.io/AeroSpace/goodies)
## Key features
- Tiling window manager based on a [tree paradigm](https://nikitabobko.github.io/AeroSpace/guide#tree)
- [i3](https://i3wm.org/) inspired
- Fast workspaces switching without animations and without the necessity to disable SIP
- AeroSpace employs its [own emulation of virtual workspaces](https://nikitabobko.github.io/AeroSpace/guide#emulation-of-virtual-workspaces) instead of relying on native macOS Spaces due to [their considerable limitations](https://nikitabobko.github.io/AeroSpace/guide#emulation-of-virtual-workspaces)
- Plain text configuration (dotfiles friendly). See: [default-config.toml](https://nikitabobko.github.io/AeroSpace/guide#default-config)
- CLI first (manpages and shell completion included)
- Doesn't require disabling SIP (System Integrity Protection)
- [Proper multi-monitor support](https://nikitabobko.github.io/AeroSpace/guide#multiple-monitors) (i3-like paradigm)
## Installation
Install via [Homebrew](https://brew.sh/) to get autoupdates (Preferred)
```
brew install --cask nikitabobko/tap/aerospace
```
In multi-monitor setup please make sure that monitors [are properly arranged](https://nikitabobko.github.io/AeroSpace/guide#proper-monitor-arrangement).
Other installation options: https://nikitabobko.github.io/AeroSpace/guide#installation
> [!NOTE]
> By using AeroSpace, you acknowledge that it's not [notarized](https://developer.apple.com/documentation/security/notarizing_macos_software_before_distribution).
>
> Notarization is a "security" feature by Apple.
> You send binaries to Apple, and they either approve them or not.
> In reality, notarization is about building binaries the way Apple likes it.
>
> I don't have anything against notarization as a concept.
> I specifically don't like the way Apple does notarization.
> I don't have time to deal with Apple.
>
> [Homebrew installation script](https://github.com/nikitabobko/homebrew-tap/blob/main/Casks/aerospace.rb) is configured to
> automatically delete `com.apple.quarantine` attribute, that's why the app should work out of the box, without any warnings that
> "Apple cannot check AeroSpace for malicious software"
## Community, discussions, issues
AeroSpace project doesn't accept Issues directly - we ask you to create a [Discussion](https://github.com/nikitabobko/AeroSpace/discussions) first.
Please read [CONTRIBUTING.md](./CONTRIBUTING.md) for more details.
Community discussions happen at GitHub Discussions.
There you can discuss bugs, propose new features, ask your questions, show off your setup, or just chat.
There are 7 channels:
- [#all](https://github.com/nikitabobko/AeroSpace/discussions).
[RSS](https://github.com/nikitabobko/AeroSpace/discussions.atom?discussions_q=sort%3Adate_created).
Feed with all discussions.
- [#announcements](https://github.com/nikitabobko/AeroSpace/discussions/categories/announcements).
[RSS](https://github.com/nikitabobko/AeroSpace/discussions/categories/announcements.atom?discussions_q=category%3Aannouncements+sort%3Adate_created).
Only maintainers can post here.
Highly moderated traffic.
- [#announcements-releases](https://github.com/nikitabobko/AeroSpace/discussions/categories/announcements-releases).
[RSS](https://github.com/nikitabobko/AeroSpace/discussions/categories/announcements-releases.atom?discussions_q=category%3Aannouncements-releases+sort%3Adate_created).
Announcements about non-patch releases.
Only maintainers can post here.
- [#feature-ideas](https://github.com/nikitabobko/AeroSpace/discussions/categories/feature-ideas).
[RSS](https://github.com/nikitabobko/AeroSpace/discussions/categories/feature-ideas.atom?discussions_q=category%3Afeature-ideas+sort%3Adate_created).
- [#general](https://github.com/nikitabobko/AeroSpace/discussions/categories/general).
[RSS](https://github.com/nikitabobko/AeroSpace/discussions/categories/general.atom?discussions_q=sort%3Adate_created+category%3Ageneral).
- [#potential-bugs](https://github.com/nikitabobko/AeroSpace/discussions/categories/potential-bugs).
[RSS](https://github.com/nikitabobko/AeroSpace/discussions/categories/potential-bugs.atom?discussions_q=category%3Apotential-bugs+sort%3Adate_created).
If you think that you have encountered a bug, you can discuss your bugs here.
- [#questions-and-answers](https://github.com/nikitabobko/AeroSpace/discussions/categories/questions-and-answers).
[RSS](https://github.com/nikitabobko/AeroSpace/discussions/categories/questions-and-answers.atom?discussions_q=category%3Aquestions-and-answers+sort%3Adate_created).
Everyone is welcome to ask questions.
Everyone is encouraged to answer other people's questions.
## Project status
Public Beta. AeroSpace can be used as a daily driver, but expect breaking changes until 1.0 is reached.
What stops us from 1.0 release:
- [x] https://github.com/nikitabobko/AeroSpace/issues/131 Performance. Implement thread-per-application to circumvent macOS blocking AX API.
- [ ] https://github.com/nikitabobko/AeroSpace/issues/1215 _Big refactoring_. Rewrite mutable double-linked core tree data structure to immutable single-linked persistent tree.
Important for: stability and potential performance
- [ ] https://github.com/nikitabobko/AeroSpace/issues/1216 The big refactoring will help us to fix stability issue that windows may randomly jump to the focused workspace
- [ ] https://github.com/nikitabobko/AeroSpace/issues/68 The big refactoring will help us to support macOS native tabs
- [x] https://github.com/nikitabobko/AeroSpace/issues/278 Implement shell-like combinators.
Ignore a lot of crazy fuss in the issue,
We are most probably going with the minimal approach to only introduce common shell-combinators: `||`, `&&`, `;` and `eval` command to send multiple commands in one go.
- [ ] https://github.com/nikitabobko/AeroSpace/issues/1012 Investigate a possibility to use `CGEvent.tapCreate` API for global hotkeys
- [ ] https://github.com/nikitabobko/AeroSpace/issues/28 Maybe it will allow to distinguish left and right modifiers. Maybe not
Big and important issues which will go after 1.0 release:
- [ ] https://github.com/nikitabobko/AeroSpace/issues/2 sticky windows
- [ ] https://github.com/nikitabobko/AeroSpace/issues/260 Dynamic TWM
## Development
A notes on how to setup the project, build it, how to run the tests, etc. can be found here: [dev-docs/development.md](./dev-docs/development.md)
## Project values
**Values**
- AeroSpace is targeted at advanced users and developers
- Keyboard centric
- Breaking changes (configuration files, CLI, behavior) are avoided as much as possible, but it must not let the software stagnate.
Thus breaking changes can happen, but with careful considerations and helpful message.
[Semver](https://semver.org/) major version is bumped in case of a breaking change (It's all guaranteed once AeroSpace reaches 1.0 version, until then breaking changes just happen)
- AeroSpace doesn't use GUI, unless necessarily
- AeroSpace will never provide a GUI for configuration.
For advanced users, it's easier to edit a configuration file in text editor rather than navigating through checkboxes in GUI.
- Status menu icon is ok, because visual feedback is needed
- Provide _practical_ features. Fancy appearance features are not _practical_ (e.g. window borders, transparency, animations, etc.)
- "dark magic" (aka "private APIs", "code injections", etc.) must be avoided as much as possible
- Right now, AeroSpace uses only a single private API to get window ID of accessibility object `_AXUIElementGetWindow`.
Everything else is [macOS public accessibility API](https://developer.apple.com/documentation/applicationservices/axuielement_h).
- AeroSpace will never require you to disable SIP (System Integrity Protection).
- The goal is to make AeroSpace easily maintainable, and resistant to macOS updates.
**Non Values**
- Play nicely with existing macOS features.
If limitations are imposed then AeroSpace won't play nicely with existing macOS features
(For example, AeroSpace doesn't acknowledge the existence of macOS Spaces, and it uses [emulation of its own workspaces](https://nikitabobko.github.io/AeroSpace/guide#emulation-of-virtual-workspaces))
- Ricing.
AeroSpace provides only a very minimal support for ricing - gaps and a few callbacks for integrations with bars.
The current maintainer doesn't care about ricing.
Ricing issues are not a priority, and they are mostly ignored.
The ricing stance can change only with the appearance of more maintainers.
## macOS compatibility table
* AeroSpace binary runs on: macOS 13+
* AeroSpace debug build from sources is supported on: macOS 14+
* AeroSpace release build from sources is supported on: macOS 15+ (Requires Xcode 26+)
## Sponsorship
AeroSpace is developed and maintained in my free time.
If you find it useful, [consider sponsoring](https://github.com/sponsors/nikitabobko#sponsors).
## People who have write access
In alphabetical order:
- [@mobile-ar](https://github.com/mobile-ar)
- [@nikitabobko](https://github.com/nikitabobko)
- [@rickyz](https://github.com/rickyz)
## Tip of the day
```bash
defaults write -g NSWindowShouldDragOnGesture -bool true
```
Now, you can move windows by holding `ctrl`+`cmd` and dragging any part of the window (not necessarily the window title)
Source: [reddit](https://www.reddit.com/r/MacOS/comments/k6hiwk/keyboard_modifier_to_simplify_click_drag_of/)
## Related projects
- [Amethyst](https://github.com/ianyh/Amethyst) - tiling window manager à la xmonad
- [InstantSpaceSwitcher](https://github.com/jurplel/InstantSpaceSwitcher) - Instant space switching by synthesizing trackpad gesture with an artificially high velocity
- [yabai](https://github.com/koekeishiya/yabai) - a tiling window manager for macOS based on binary space partitioning
## /Sources/AeroSpaceApp/AeroSpaceApp.swift
```swift path="/Sources/AeroSpaceApp/AeroSpaceApp.swift"
import AppBundle
import SwiftUI
// This file is shared between SPM and xcode project
@main
struct AeroSpaceApp: App {
@StateObject var viewModel = TrayMenuModel.shared
@StateObject var messageModel = MessageModel.shared
@Environment(\.openWindow) var openWindow: OpenWindowAction
init() {
initAppBundle()
}
var body: some Scene {
menuBar(viewModel: viewModel)
getMessageWindow(messageModel: messageModel)
.onChange(of: messageModel.message) { message in
if message != nil {
openWindow(id: messageWindowId)
}
}
}
}
```
## /Sources/AppBundle/GlobalObserver.swift
```swift path="/Sources/AppBundle/GlobalObserver.swift"
import AppKit
import Common
enum GlobalObserver {
private static func onNotif(_ notification: Notification) {
// Third line of defence against lock screen window. See: closedWindowsCache
// Second and third lines of defence are technically needed only to avoid potential flickering
if (notification.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication)?.bundleIdentifier == lockScreenAppBundleId {
return
}
let notifName = notification.name.rawValue
Task.startUnstructured { @MainActor in
if !TrayMenuModel.shared.isEnabled { return }
if notifName == NSWorkspace.didActivateApplicationNotification.rawValue {
scheduleCancellableCompleteRefreshSession(.globalObserver(notifName), optimisticallyPreLayoutWorkspaces: true)
} else {
scheduleCancellableCompleteRefreshSession(.globalObserver(notifName))
}
}
}
private static func onHideApp(_ notification: Notification) {
let notifName = notification.name.rawValue
Task.startUnstructured { @MainActor in
guard let token: RunSessionGuard = .isServerEnabled else { return }
try await runLightSession(.globalObserver(notifName), token) {
if config.automaticallyUnhideMacosHiddenApps {
if let w = prevFocus?.windowOrNil,
w.macAppUnsafe.nsApp.isHidden,
// "Hide others" (cmd-alt-h) -> don't force focus
// "Hide app" (cmd-h) -> force focus
MacApp.allAppsMap.values.count(where: { $0.nsApp.isHidden }) == 1
{
// Force focus
_ = w.focusWindow()
w.nativeFocus()
}
for app in MacApp.allAppsMap.values {
app.nsApp.unhide()
}
}
}
}
}
@MainActor
static func initObserver() {
let nc = NSWorkspace.shared.notificationCenter
nc.addObserver(forName: NSWorkspace.didLaunchApplicationNotification, object: nil, queue: .main, using: onNotif)
nc.addObserver(forName: NSWorkspace.didActivateApplicationNotification, object: nil, queue: .main, using: onNotif)
nc.addObserver(forName: NSWorkspace.didHideApplicationNotification, object: nil, queue: .main, using: onHideApp)
nc.addObserver(forName: NSWorkspace.didUnhideApplicationNotification, object: nil, queue: .main, using: onNotif)
nc.addObserver(forName: NSWorkspace.activeSpaceDidChangeNotification, object: nil, queue: .main, using: onNotif)
nc.addObserver(forName: NSWorkspace.didTerminateApplicationNotification, object: nil, queue: .main, using: onNotif)
NSEvent.addGlobalMonitorForEvents(matching: .leftMouseUp) { _ in
// todo reduce number of refreshSession in the callback
// resetManipulatedWithMouseIfPossible might call its own refreshSession
// The end of the callback calls refreshSession
Task.startUnstructured { @MainActor in
guard let token: RunSessionGuard = .isServerEnabled else { return }
try await resetManipulatedWithMouseIfPossible()
let mouseLocation = mouseLocation
let clickedMonitor = mouseLocation.monitorApproximation
switch true {
// Detect clicks on desktop of different monitors
case clickedMonitor.visibleRect.contains(mouseLocation) && clickedMonitor.activeWorkspace != focus.workspace:
_ = try await runLightSession(.globalObserverLeftMouseUp, token) {
clickedMonitor.activeWorkspace.focusWorkspace()
}
// Detect close button clicks for unfocused windows. Yes, kAXUIElementDestroyedNotification is that unreliable
// And trigger new window detection that could be delayed due to mouseDown event
default:
scheduleCancellableCompleteRefreshSession(.globalObserverLeftMouseUp)
}
}
}
}
}
```
## /Sources/AppBundle/command/CmdEnv.swift
```swift path="/Sources/AppBundle/command/CmdEnv.swift"
import Common
struct CmdEnv {
var windowId: UInt32? = nil
var workspaceName: String? = nil
static let defaultEnv: CmdEnv = .init()
consuming func withFocus(_ focus: LiveFocus) -> Self {
return switch focus.asLeaf {
case .window(let wd): withWindowId(wd.windowId)
case .emptyWorkspace(let ws): withWorkspaceName(ws.name)
}
}
consuming func withWindowId(_ windowId: UInt32) -> CmdEnv {
self.windowId = windowId
self.workspaceName = nil
return self
}
consuming func withWorkspaceName(_ workspaceName: String) -> CmdEnv {
self.windowId = nil
self.workspaceName = workspaceName
return self
}
var asMap: [String: String] {
var result = [String: String]()
if let windowId {
result[AEROSPACE_WINDOW_ID] = windowId.description
}
if let workspaceName {
result[AEROSPACE_WORKSPACE] = workspaceName.description
}
return result
}
}
```
## /Sources/AppBundle/command/CmdIo.swift
```swift path="/Sources/AppBundle/command/CmdIo.swift"
import Common
struct CmdStdin: ~Copyable {
private var input: String = ""
init(_ input: String) {
self.input = input
}
static var emptyStdin: CmdStdin { .init("") }
mutating func readAll() -> String {
let result = input
input = ""
return result
}
}
protocol CmdIo: AnyObject {
var stdout: [String] { get set }
var stderr: [String] { get set }
func readStdin() -> String
}
extension CmdIo {
@discardableResult func out(_ msg: String) -> IoSideEffect { stdout.append(msg); return .instance }
@discardableResult func err(_ msg: String) -> IoSideEffect { stderr.append(msg); return .instance }
@discardableResult func out(_ msg: [String]) -> IoSideEffect { stdout += msg; return .instance }
// periphery:ignore
@discardableResult func err(_ msg: [String]) -> IoSideEffect { stderr += msg; return .instance }
}
final class CmdIoImpl: CmdIo {
private var stdin: CmdStdin
var stdout: [String] = []
var stderr: [String] = []
init(stdin: consuming CmdStdin) { self.stdin = stdin }
func readStdin() -> String { stdin.readAll() }
static var emptyStdinIgnoringOut: CmdIo { CmdIoImpl(stdin: .emptyStdin) }
}
final class CmdIoForwardingStdin: CmdIo {
private var stdin: CmdIo
var stdout: [String] = []
var stderr: [String] = []
init(stdin: CmdIo) { self.stdin = stdin }
func readStdin() -> String { stdin.readStdin() }
}
struct CmdResult: Equatable {
let stdout: [String]
let stderr: [String]
let exitCode: Int32ExitCode
}
```
## /Sources/AppBundle/command/Command.swift
```swift path="/Sources/AppBundle/command/Command.swift"
import AppKit
import Common
protocol Command: AeroAny, Equatable, Sendable {
associatedtype T: CmdArgs
var args: T { get }
@MainActor
func run(_ env: CmdEnv, _ io: CmdIo) async -> T.ExitCodeType
/// We should reset closedWindowsCache when the command can potentially change the tree
var shouldResetClosedWindowsCache: Bool { get }
}
extension Command {
static func == (lhs: Self, rhs: Self) -> Bool {
return lhs.args.equals(rhs.args)
}
nonisolated func equals(_ other: any Command) -> Bool {
(other as? Self).flatMap { self == $0 } ?? false
}
}
extension Command {
var info: CmdStaticInfo { T.info }
}
extension Command {
@MainActor
@discardableResult
func run(_ env: CmdEnv, _ stdin: consuming CmdStdin) async -> CmdResult {
return await Shell.cmd(self).run(env, stdin)
}
}
```
## /Sources/AppBundle/command/cmdManifest.swift
```swift path="/Sources/AppBundle/command/cmdManifest.swift"
import Common
extension CmdArgs {
func toCommand() -> ParsedCmd<any Command> {
let command: any Command
switch Self.info.kind {
case ._false:
command = FalseCommand(args: self as! FalseCmdArgs)
case ._true:
command = TrueCommand(args: self as! TrueCmdArgs)
case .balanceSizes:
command = BalanceSizesCommand(args: self as! BalanceSizesCmdArgs)
case .close:
command = CloseCommand(args: self as! CloseCmdArgs)
case .closeAllWindowsButCurrent:
command = CloseAllWindowsButCurrentCommand(args: self as! CloseAllWindowsButCurrentCmdArgs)
case .config:
command = ConfigCommand(args: self as! ConfigCmdArgs)
case .debugWindows:
command = DebugWindowsCommand(args: self as! DebugWindowsCmdArgs)
case .echo:
command = EchoCommand(args: self as! EchoCmdArgs)
case .enable:
command = EnableCommand(args: self as! EnableCmdArgs)
case .eval:
command = EvalCommand(args: self as! EvalCmdArgs)
case .execAndForget:
return .failure("exec-and-forget is not a real command and it's NOT allowed in the shell", EXIT_CODE_TWO)
case .flattenWorkspaceTree:
command = FlattenWorkspaceTreeCommand(args: self as! FlattenWorkspaceTreeCmdArgs)
case .focus:
command = FocusCommand(args: self as! FocusCmdArgs)
case .focusBackAndForth:
command = FocusBackAndForthCommand(args: self as! FocusBackAndForthCmdArgs)
case .focusMonitor:
command = FocusMonitorCommand(args: self as! FocusMonitorCmdArgs)
case .fullscreen:
command = FullscreenCommand(args: self as! FullscreenCmdArgs)
case .joinWith:
command = JoinWithCommand(args: self as! JoinWithCmdArgs)
case .layout:
command = LayoutCommand(args: self as! LayoutCmdArgs)
case .listApps:
command = ListAppsCommand(args: self as! ListAppsCmdArgs)
case .listExecEnvVars:
command = ListExecEnvVarsCommand(args: self as! ListExecEnvVarsCmdArgs)
case .listModes:
command = ListModesCommand(args: self as! ListModesCmdArgs)
case .listMonitors:
command = ListMonitorsCommand(args: self as! ListMonitorsCmdArgs)
case .listWindows:
command = ListWindowsCommand(args: self as! ListWindowsCmdArgs)
case .listWorkspaces:
command = ListWorkspacesCommand(args: self as! ListWorkspacesCmdArgs)
case .macosNativeFullscreen:
command = MacosNativeFullscreenCommand(args: self as! MacosNativeFullscreenCmdArgs)
case .macosNativeMinimize:
command = MacosNativeMinimizeCommand(args: self as! MacosNativeMinimizeCmdArgs)
case .mode:
command = ModeCommand(args: self as! ModeCmdArgs)
case .move:
command = MoveCommand(args: self as! MoveCmdArgs)
case .moveMouse:
command = MoveMouseCommand(args: self as! MoveMouseCmdArgs)
case .moveNodeToMonitor:
command = MoveNodeToMonitorCommand(args: self as! MoveNodeToMonitorCmdArgs)
case .moveNodeToWorkspace:
command = MoveNodeToWorkspaceCommand(args: self as! MoveNodeToWorkspaceCmdArgs)
case .moveWorkspaceToMonitor:
command = MoveWorkspaceToMonitorCommand(args: self as! MoveWorkspaceToMonitorCmdArgs)
case .reloadConfig:
command = ReloadConfigCommand(args: self as! ReloadConfigCmdArgs)
case .resize:
command = ResizeCommand(args: self as! ResizeCmdArgs)
case .runCallback:
command = RunCallbackCommand(args: self as! RunCallbackCmdArgs)
case .split:
command = SplitCommand(args: self as! SplitCmdArgs)
case .subscribe:
return .failure("subscribe is not supported in the eval", EXIT_CODE_TWO)
case .summonWorkspace:
command = SummonWorkspaceCommand(args: self as! SummonWorkspaceCmdArgs)
case .swap:
command = SwapCommand(args: self as! SwapCmdArgs)
case .test:
command = TestCommand(args: self as! TestCmdArgs)
case .testNot:
command = TestNotCommand(args: self as! TestNotCmdArgs)
case .triggerBinding:
command = TriggerBindingCommand(args: self as! TriggerBindingCmdArgs)
case .volume:
command = VolumeCommand(args: self as! VolumeCmdArgs)
case .workspace:
command = WorkspaceCommand(args: self as! WorkspaceCmdArgs)
case .workspaceBackAndForth:
command = WorkspaceBackAndForthCommand(args: self as! WorkspaceBackAndForthCmdArgs)
}
check(command.info == Self.info)
return .cmd(command)
}
}
```
## /Sources/AppBundle/command/cmdResolveTargetOrReportError.swift
```swift path="/Sources/AppBundle/command/cmdResolveTargetOrReportError.swift"
import Common
extension CmdArgs {
@MainActor
var workspace: Workspace? {
if let workspaceName { Workspace.get(byName: workspaceName.raw) } else { nil }
}
@MainActor
func resolveTargetOrReportError(_ env: CmdEnv, _ io: CmdIo) -> LiveFocus? {
// Flags
if let windowId {
if let wi = Window.get(byId: windowId) {
return wi.toLiveFocusOrReportError(io)
} else {
io.err("Invalid <window-id> \(windowId) passed to --window-id")
return nil
}
}
if let workspace {
return workspace.toLiveFocus()
}
// Env
if let windowId = env.windowId {
if let wi = Window.get(byId: windowId) {
return wi.toLiveFocusOrReportError(io)
} else {
io.err("Invalid <window-id> \(windowId) specified in \(AEROSPACE_WINDOW_ID) env variable")
return nil
}
}
if let wsName = env.workspaceName {
return Workspace.get(byName: wsName).toLiveFocus()
}
// Real Focus
return focus
}
}
extension Window {
@MainActor
func toLiveFocusOrReportError(_ io: CmdIo) -> LiveFocus? {
if let result = toLiveFocusOrNil() {
return result
} else {
io.err("Window \(windowId) doesn't belong to any monitor. And thus can't even define a focused workspace")
return nil
}
}
}
```
## /Sources/AppBundle/command/format.swift
```swift path="/Sources/AppBundle/command/format.swift"
import Common
import Foundation
struct WindowWithPrefetchedTitle {
let window: Window
let title: String?
private init(window: Window, title: String?) {
self.window = window
self.title = title
}
static func resolveWindow(_ window: Window, for formatVar: FormatVar, _ cm: CancellationMode) async throws -> Self {
try await resolveWindow(window, needsTitle: formatVar == .window(.windowTitle), cm)
}
static func resolveWindow(_ window: Window, for format: [InterToken<InterVar>], _ cm: CancellationMode) async throws -> Self {
let needsTitle = format.contains { $0 == .interVar(.formatVar(.window(.windowTitle))) }
return try await resolveWindow(window, needsTitle: needsTitle, cm)
}
private static func resolveWindow(_ window: Window, needsTitle: Bool, _ cm: CancellationMode) async throws -> Self {
let title = needsTitle ? try await window.getTitle(cm) : nil
return .init(window: window, title: title)
}
static func forTest(window: Window, title: String?) -> Self {
.init(window: window, title: title)
}
}
enum AeroObj {
case window(WindowWithPrefetchedTitle)
case workspace(Workspace)
case app(any AbstractApp)
case monitor(MonitorInfo)
var kind: AeroObjKind {
switch self {
case .window: .window
case .workspace: .workspace
case .app: .app
case .monitor: .monitor
}
}
}
extension [AeroObj] {
@MainActor
func format(_ format: [InterToken<InterVar>]) -> Result<[String], [InterVarExpansionError]> {
var cellTable: [[Cell<String>]] = []
for obj in self {
var line: [Cell<String>] = []
var curCell: String = ""
var errors = [InterVarExpansionError]()
for token in format {
switch token {
case .interVar(.plainInterVar(.rightPadding)):
line.append(Cell(value: curCell, rightPadding: true))
curCell = ""
case .literal(let literal):
curCell += literal
case .interVar(let value):
switch value.expandFormatVar(obj: obj) {
case .success(let expanded): curCell += expanded.toString()
case .failure(let error): errors.append(error)
}
}
}
if !errors.isEmpty { return .failure(errors) }
line.append(Cell(value: curCell, rightPadding: false))
cellTable.append(line)
}
let result = cellTable
.transposed()
.map { column in
let columndWidth = column.map { $0.value.count }.max().orDie()
return column.map {
$0.rightPadding
? $0.value + String(repeating: " ", count: columndWidth - $0.value.count)
: $0.value
}
}
.transposed()
.map { line in line.joined(separator: "") }
return .success(result)
}
}
enum Primitive: Encodable {
case bool(Bool)
case int(Int64)
case string(String)
enum Kind: String {
case bool = "Bool"
case int = "Int"
case string = "String"
}
var kind: Kind {
switch self {
case .bool: .bool
case .int: .int
case .string: .string
}
}
func toString() -> String {
switch self {
case .bool(let x): x.description
case .int(let x): x.description
case .string(let x): x
}
}
func encode(to encoder: any Encoder) throws {
let value: Encodable = switch self {
case .bool(let x): x
case .int(let x): x
case .string(let x): x
}
var container = encoder.singleValueContainer()
try container.encode(value)
}
public static func int(_ int: UInt32) -> Self { .int(Int64(exactly: int).orDie()) }
public static func int(_ int: Int32) -> Self { .int(Int64(exactly: int).orDie()) }
public static func int(_ int: Int) -> Self { .int(Int64(exactly: int).orDie()) }
}
private struct Cell<T> {
let value: T
let rightPadding: Bool
}
extension FormatVar {
@MainActor func expandFormatVar(obj: AeroObj) -> Result<Primitive, InterVarExpansionError> {
switch (obj, self) {
case (.window(let w), .workspace):
return w.window.nodeWorkspace.flatMap(AeroObj.workspace).map(expandFormatVar) ?? .success(.string("NULL-WORKSPACE"))
case (.window(let w), .monitor):
return w.window.nodeMonitor.flatMap(AeroObj.monitor).map(expandFormatVar) ?? .success(.string("NULL-MONITOR"))
case (.window(let w), .app):
return expandFormatVar(obj: .app(w.window.app))
case (.window(_), .window): break
case (.workspace(let ws), .monitor):
return expandFormatVar(obj: AeroObj.monitor(ws.workspaceMonitor))
case (.workspace, _): break
case (.app(_), _): break
case (.monitor(_), _): break
}
switch (obj, self) {
case (.window(let w), .window(let f)):
return switch f {
case .windowId: .success(.int(w.window.windowId))
case .windowIsFullscreen: .success(.bool(w.window.isFullscreen))
case .windowTitle: .success(.string(w.title.orDie("Title wasn't prefetched")))
case .windowLayout, .windowParentContainerLayout: toLayoutResult(w: w.window)
}
case (.workspace(let w), .workspace(let f)):
return switch f {
case .workspaceName: .success(.string(w.name))
case .workspaceVisible: .success(.bool(w.isVisible))
case .workspaceFocused: .success(.bool(focus.workspace == w))
case .workspaceRootContainerLayout: .success(.string(toLayoutString(tc: w.rootTilingContainer)))
}
case (.monitor(let m), .monitor(let f)):
return switch f {
case .monitorId_oneBased: .success(m.monitorId_oneBased.map { .int($0) } ?? .string("NULL-MONITOR-ID"))
case .monitorAppKitNsScreenScreensId: .success(.int(m.monitorAppKitNsScreenScreensId))
case .monitorName: .success(.string(m.name))
case .monitorIsMain: .success(.bool(m.isMain))
}
case (.app(let a), .app(let f)):
return switch f {
case .appBundleId: .success(.string(a.rawAppBundleId ?? "NULL-APP-BUNDLE-ID"))
case .appName: .success(.string(a.name ?? "NULL-APP-NAME"))
case .appPid: .success(.int(a.pid))
case .appExecPath: .success(.string(a.execPath ?? "NULL-APP-EXEC-PATH"))
case .appBundlePath: .success(.string(a.bundlePath ?? "NULL-APP-BUNDLE-PATH"))
}
default: break
}
return .failure(.unknownInterpolationVariable(unknownInterpolationVariable(variable: rawValue, obj)))
}
}
enum InterVarExpansionError: LocalizedError, CustomStringConvertible {
case unknownInterpolationVariable(String)
case nullParent(String)
case notPossible(String)
case windowParentIllegalRelation(String)
case rightPaddingCannotBeExpanded(String)
public var description: String {
switch self {
case .unknownInterpolationVariable(let msg): msg
case .nullParent(let msg): msg
case .notPossible(let msg): msg
case .windowParentIllegalRelation(let msg): msg
case .rightPaddingCannotBeExpanded(let msg): msg
}
}
public var errorDescription: String? { description }
}
extension PlainInterVar {
@MainActor func expandFormatVar() -> Result<Primitive, InterVarExpansionError> {
switch self {
case .newline: .success(.string("\n"))
case .tab: .success(.string("\t"))
case .rightPadding:
.failure(.rightPaddingCannotBeExpanded("\(PlainInterVar.rightPadding.rawValue.singleQuoted) interpolation variable cannot be expanded"))
}
}
}
extension InterVar {
@MainActor func expandFormatVar(obj: AeroObj) -> Result<Primitive, InterVarExpansionError> {
switch self {
case .formatVar(let it): it.expandFormatVar(obj: obj)
case .plainInterVar(let it): it.expandFormatVar()
}
}
}
func unknownInterpolationVariable(variable: String, _ obj: AeroObj) -> String {
"Unknown interpolation variable '\(variable)'. " +
"Possible values:\n\(getAvailableInterVars(for: obj.kind).joined(separator: "\n").prependLines(" "))"
}
private func toLayoutString(tc: TilingContainer) -> String {
switch (tc.layout, tc.orientation) {
case (.tiles, .h): return LayoutCmdArgs.LayoutDescription.h_tiles.rawValue
case (.tiles, .v): return LayoutCmdArgs.LayoutDescription.v_tiles.rawValue
case (.accordion, .h): return LayoutCmdArgs.LayoutDescription.h_accordion.rawValue
case (.accordion, .v): return LayoutCmdArgs.LayoutDescription.v_accordion.rawValue
}
}
private func toLayoutResult(w: Window) -> Result<Primitive, InterVarExpansionError> {
guard let parent = w.parent else { return .failure(.nullParent("NULL-PARENT")) }
return switch getChildParentRelation(child: w, parent: parent) {
case .tiling(let tc): .success(.string(toLayoutString(tc: tc)))
case .floatingWindow: .success(.string(LayoutCmdArgs.LayoutDescription.floating.rawValue))
case .macosNativeFullscreenWindow: .success(.string("macos_native_fullscreen"))
case .macosNativeHiddenAppWindow: .success(.string("macos_native_window_of_hidden_app"))
case .macosNativeMinimizedWindow: .success(.string("macos_native_minimized"))
case .macosPopupWindow: .success(.string("NULL-WINDOW-LAYOUT"))
case .rootTilingContainer: .failure(.notPossible("Not possible"))
case .shimContainerRelation: .failure(.windowParentIllegalRelation("Window cannot have a shim container relation"))
}
}
```
## /Sources/AppBundle/command/formatToJson.swift
```swift path="/Sources/AppBundle/command/formatToJson.swift"
import Common
import Foundation
extension [AeroObj] {
@MainActor
func formatToJson(_ format: [InterToken<InterVar>], ignoreRightPaddingVar: Bool) -> Result<String, String> {
var list: [[String: Primitive]] = []
for richObj in self {
var rawObj: [String: Primitive] = [:]
for token in format {
switch token {
case .interVar(.plainInterVar(.rightPadding)) where ignoreRightPaddingVar:
break
case .literal:
break // should be spaces
case .interVar(let varName):
switch varName.expandFormatVar(obj: richObj) {
case .success(let expanded): rawObj[varName.rawValue] = expanded
case .failure(let error): return .failure(error.description)
}
}
}
list.append(rawObj)
}
return JSONEncoder.aeroSpaceDefault.encodeToString(list).toResult("Can't encode '\(list)' to JSON")
}
}
```
## /Sources/AppBundle/command/impl/BalanceSizesCommand.swift
```swift path="/Sources/AppBundle/command/impl/BalanceSizesCommand.swift"
import AppKit
import Common
import Foundation
struct BalanceSizesCommand: Command {
let args: BalanceSizesCmdArgs
/*conforms*/ let shouldResetClosedWindowsCache = false
func run(_ env: CmdEnv, _ io: CmdIo) -> BinaryExitCode {
guard let target = args.resolveTargetOrReportError(env, io) else { return .fail }
balance(target.workspace.rootTilingContainer)
return .succ
}
}
@MainActor
private func balance(_ parent: TilingContainer) {
for child in parent.children {
switch parent.layout {
case .tiles: child.setWeight(parent.orientation, 1)
case .accordion: break // Do nothing
}
if let child = child as? TilingContainer {
balance(child)
}
}
}
```
## /Sources/AppBundle/command/impl/CloseAllWindowsButCurrentCommand.swift
```swift path="/Sources/AppBundle/command/impl/CloseAllWindowsButCurrentCommand.swift"
import AppKit
import Common
struct CloseAllWindowsButCurrentCommand: Command {
let args: CloseAllWindowsButCurrentCmdArgs
/*conforms*/ let shouldResetClosedWindowsCache = false
func run(_ env: CmdEnv, _ io: CmdIo) async -> BinaryExitCode {
guard let target = args.resolveTargetOrReportError(env, io) else { return .fail }
guard let focused = target.windowOrNil else {
return .fail(io.err("Empty workspace"))
}
guard let workspace = focused.nodeWorkspace else {
return .fail(io.err("Focused window '\(focused.windowId)' doesn't belong to workspace"))
}
var result = BinaryExitCode.succ
for window in workspace.allLeafWindowsRecursive where window != focused {
result = await CloseCommand(args: args.closeArgs).run(env.withWindowId(window.windowId), io).and(result)
}
return result
}
}
```
## /Sources/AppBundle/command/impl/CloseCommand.swift
```swift path="/Sources/AppBundle/command/impl/CloseCommand.swift"
import AppKit
import Common
struct CloseCommand: Command {
let args: CloseCmdArgs
/*conforms*/ let shouldResetClosedWindowsCache = false
func run(_ env: CmdEnv, _ io: CmdIo) async -> BinaryExitCode {
guard let target = args.resolveTargetOrReportError(env, io) else { return .fail }
guard let window = target.windowOrNil else {
return .fail(io.err("Empty workspace"))
}
// Access ax directly. Not cool :(
if await args.quitIfLastWindow.andAsync({ @MainActor @Sendable in (try? await window.macAppUnsafe.getAxWindowsCount(.nonCancellable)) == 1 }) {
let app = window.macAppUnsafe
if app.nsApp.terminate() {
for workspace in Workspace.all {
for window in workspace.allLeafWindowsRecursive where window.app.pid == app.pid {
(window as! MacWindow).garbageCollect(skipClosedWindowsCache: true)
}
}
return .succ
} else {
return .fail(io.err("Failed to quit '\(window.app.name ?? "Unknown app")'"))
}
} else {
window.closeAxWindow()
return .succ
}
}
}
```
## /Sources/AppBundle/command/impl/ConfigCommand.swift
```swift path="/Sources/AppBundle/command/impl/ConfigCommand.swift"
import AppKit
import Common
struct ConfigCommand: Command {
let args: ConfigCmdArgs
/*conforms*/ let shouldResetClosedWindowsCache = false
func run(_ env: CmdEnv, _ io: CmdIo) -> BinaryExitCode {
switch args.mode {
case .getKey(let key):
return getKey(io, args: args, key: key)
case .majorKeys:
let out = """
.
mode
\(config.modes.keys.map { "mode.\($0).binding" }.joined(separator: "\n"))
"""
return .succ(io.out(out))
case .allKeys:
let configMap = buildConfigMap()
var allKeys: [String] = []
configMap.dumpAllKeysRecursive(path: ".", result: &allKeys)
return .succ(io.out(allKeys.joined(separator: "\n")))
case .configPath:
return .succ(io.out(configUrl.absoluteURL.path))
}
}
}
extension String {
fileprivate func toKeyPath() -> Result<[String], String> {
if self == "." { return .success([]) }
if isEmpty { return .failure("Invalid empty key") }
if self.contains("..") { return .failure("Invalid key '\(self)'") }
if self.hasSuffix(".") { return .failure("Invalid key '\(self)'") }
return .success(self.split(separator: ".", omittingEmptySubsequences: false).map(String.init))
}
}
@MainActor private func getKey(_ io: CmdIo, args: ConfigCmdArgs, key: String) -> BinaryExitCode {
guard let keyPath = key.toKeyPath().getOrNil(appendErrorTo: &io.stderr) else { return .fail }
guard var configMap = buildConfigMap().find(keyPath: keyPath.slice).getOrNil(appendErrorTo: &io.stderr) else { return .fail }
if args.keys {
switch configMap {
case .scalar(let scalar):
return .fail(io.err("--keys flag cannot be applied to scalar object '\(scalar)'"))
case .map(let map):
configMap = .array(map.keys.map { .scalar(.string($0)) })
case .array(let array):
configMap = .array((0 ..< array.count).map { .scalar(.int($0)) })
}
}
if args.json {
return switch JSONEncoder.aeroSpaceDefault.encodeToString(configMap) {
case let json?: .succ(io.out(json))
case nil: .fail(io.err("Can't convert json Data to String"))
}
} else {
switch configMap {
case .scalar(let scalar):
return .succ(io.out(scalar.describe))
case .map:
let msg = "Complicated objects can be printed only with --json flag. Alternatively, you can try to inspect keys of the object with --keys flag"
return .fail(io.err(msg))
case .array(let array):
let plainArray: Result<[String], String> = array.mapAllOrFailure {
switch $0 {
case .scalar(let scalar): .success(scalar.describe)
default: .failure("Printing array of non-string objects is supported only with --json flag." +
"Alternatively, you can try to inspect keys of the object with --keys flag")
}
}
return switch plainArray {
case .success(let array): .succ(io.out(array.sorted().joined(separator: "\n")))
case .failure(let error): .fail(io.err(error))
}
}
}
}
extension ConfigMapValue {
func find(keyPath: StrArrSlice) -> Result<ConfigMapValue, String> {
if let key = keyPath.first {
switch self {
case .scalar(let scalar):
return .failure("Can't dereference scalar value '\(scalar.describe)'")
case .map(let map):
return switch map[key] {
case let child?: child.find(keyPath: keyPath.slice(1...).orDie())
case nil: .failure("No value at key token '\(key)'")
}
case .array(let array):
if let key = Int(key) {
return switch array.getOrNil(atIndex: key) {
case let child?: child.find(keyPath: keyPath.slice(1...).orDie())
case nil: .failure("Index out of bounds. Index: \(key), Size: \(array.count)")
}
} else {
return .failure("Can't convert key token '\(key)' to Int")
}
}
} else {
return .success(self)
}
}
func dumpAllKeysRecursive(path: String, result: inout [String]) {
result.append(path)
switch self {
case .scalar: break
case .map(let map):
for (key, value) in map {
let path = path == "." ? key : path + "." + key
value.dumpAllKeysRecursive(path: path, result: &result)
}
case .array(let array):
for (index, value) in array.enumerated() {
let path = path == "." ? String(index) : path + "." + String(index)
value.dumpAllKeysRecursive(path: path, result: &result)
}
}
}
}
extension Shell<any Command> {
var shellOfCommandsDescription: String { buildDescription { $0.args.description } }
}
@MainActor func buildConfigMap() -> ConfigMapValue {
let mode = config.modes.mapValues { (mode: Mode) -> ConfigMapValue in
var keyNotationToScript: [String: ConfigMapValue] = [:]
for binding in mode.bindings.values {
keyNotationToScript[binding.descriptionWithKeyNotation] =
.scalar(.string(binding.commands.shellOfCommandsDescription))
}
return .map(["binding": .map(keyNotationToScript)])
}
return .map(["mode": .map(mode)])
}
enum ConfigScalarValue: Encodable {
case string(String)
case int(Int)
var describe: String {
return switch self {
case .string(let string): string
case .int(let int): String(int)
}
}
func encode(to encoder: Encoder) throws {
let value: Encodable = switch self {
case .string(let string): string
case .int(let int): int
}
var container = encoder.singleValueContainer()
try container.encode(value)
}
}
enum ConfigMapValue: Encodable {
case scalar(ConfigScalarValue)
case map([String: ConfigMapValue])
case array([ConfigMapValue])
func encode(to encoder: Encoder) throws {
let value: Encodable = switch self {
case .scalar(let scalar): scalar
case .map(let map): map
case .array(let array): array
}
var container = encoder.singleValueContainer()
try container.encode(value)
}
}
```
## /Sources/AppBundle/command/impl/DebugWindowsCommand.swift
```swift path="/Sources/AppBundle/command/impl/DebugWindowsCommand.swift"
import AppKit
import Common
import OrderedCollections
private let disclaimer =
"""
!!! DISCLAIMER !!!
!!! 'debug-windows' command is not stable API. Please don't rely on the command existence and output format !!!
!!! The only intended use case is to report bugs about incorrect windows handling !!!
"""
@MainActor private var debugWindowsState: DebugWindowsState = .notRecording
@MainActor private var debugWindowsLog: OrderedDictionary<UInt32, String> = [:]
private let debugWindowsLimit = 10
enum DebugWindowsState {
case recording
case notRecording
case recordingAborted
}
struct DebugWindowsCommand: Command {
let args: DebugWindowsCmdArgs
/*conforms*/ let shouldResetClosedWindowsCache = false
func run(_ env: CmdEnv, _ io: CmdIo) async -> BinaryExitCode {
if let windowId = args.windowId {
guard let window = Window.get(byId: windowId) else {
return .fail(io.err("Can't find window with the specified window-id: \(windowId)"))
}
guard let a = try? await dumpWindowDebugInfo(window, .nonCancellable) else { return .fail(io.err(bugPrompt())) }
io.out(a + "\n")
io.out(disclaimer)
return .succ
}
switch debugWindowsState {
case .recording:
debugWindowsState = .notRecording
io.out(debugWindowsLog.values.joined(separator: "\n\n"))
io.out("\n" + disclaimer + "\n")
io.out("Debug session finished" + "\n")
debugWindowsLog = [:]
return .succ
case .notRecording:
debugWindowsState = .recording
debugWindowsLog = [:]
io.out(
"""
Debug windows session has started
1. Focus the problematic window
2. Run 'aerospace debug-windows' once again to finish the session and get the results
""",
)
// Make sure that the Terminal window that started the recording is recorded first
guard let target = args.resolveTargetOrReportError(env, io) else { return .fail }
if let window = target.windowOrNil {
do {
try await debugWindowsIfRecording(window, .nonCancellable)
} catch {
return .fail(io.err(bugPrompt(String(describing: error))))
}
}
return .succ
case .recordingAborted:
io.out(
"""
Recording of the previous session was aborted after \(debugWindowsLimit) windows has been focused
Run the command one more time to start new debug session
""",
)
debugWindowsState = .notRecording
debugWindowsLog = [:]
return .fail
}
}
}
@MainActor
private func dumpWindowDebugInfo(_ window: Window, _ cm: CancellationMode) async throws -> String {
let window = window as! MacWindow
let appInfoDic = window.macApp.nsApp.bundleURL.flatMap { Bundle.init(url: $0) }?.infoDictionary ?? [:]
var result: [String: Json] = try await window.dumpAxInfo(cm)
let windowLevel = getWindowLevel(for: window.windowId)
let windowLevelJson = windowLevel?.toJson() ?? .null
result["Aero.windowLevel"] = windowLevelJson
result["Aero.axWindowId"] = .int(window.windowId)
result["Aero.workspace"] = .stringOrNull(window.nodeWorkspace?.name)
result["Aero.treeNodeParent"] = .string(String(describing: window.parent))
result["Aero.macOS.version"] = .string(ProcessInfo().operatingSystemVersionString) // because built-in apps might behave differently depending on the OS version
result["Aero.App.appBundleId"] = .stringOrNull(window.app.rawAppBundleId)
result["Aero.App.pid"] = .int(Int(window.app.pid))
result["Aero.App.versionShort"] = .stringOrNull(appInfoDic["CFBundleShortVersionString"] as? String)
result["Aero.App.version"] = .stringOrNull(appInfoDic["CFBundleVersion"] as? String)
result["Aero.App.nsApp.activationPolicy"] = .string(window.macApp.nsApp.activationPolicy.prettyDescription)
result["Aero.App.nsApp.execPath"] = .stringOrNull(window.macApp.nsApp.executableURL?.description)
result["Aero.App.nsApp.appBundlePath"] = .stringOrNull(window.macApp.nsApp.bundleURL?.description)
result["Aero.AXApp"] = .dict(try await window.macApp.dumpAppAxInfo(cm))
let isDialog = try await window.isDialogHeuristic(windowLevel, cm)
let isWindow = try await window.isWindowHeuristic(windowLevel, cm)
result["Aero.AxUiElementWindowType"] = .string(AxUiElementWindowType.new(isWindow: isWindow, isDialog: { isDialog }).rawValue)
result["Aero.AxUiElementWindowType_isDialogHeuristic"] = .bool(isDialog)
var matchingCallbacks: [Json] = []
for callback in config.onWindowDetected where await callback.matches(window) {
matchingCallbacks.append(callback.debugJson)
}
result["Aero.on-window-detected"] = .array(matchingCallbacks)
return JSONEncoder.aeroSpaceDefault.encodeToString(result).prettyDescription
.prefixLines(with: "\(window.app.rawAppBundleId ?? "nil-bundle-id").\(window.windowId) ||| ")
}
@MainActor
func debugWindowsIfRecording(_ window: Window, _ cm: CancellationMode) async throws {
switch debugWindowsState {
case .recording: break
case .notRecording, .recordingAborted: return
}
if debugWindowsLog.count > debugWindowsLimit {
debugWindowsState = .recordingAborted
debugWindowsLog = [:]
}
if debugWindowsLog.keys.contains(window.windowId) {
return
}
debugWindowsLog[window.windowId] = try await dumpWindowDebugInfo(window, cm)
}
```
## /Sources/AppBundle/command/impl/EchoCommand.swift
```swift path="/Sources/AppBundle/command/impl/EchoCommand.swift"
import Common
struct EchoCommand: Command {
let args: EchoCmdArgs
/*conforms*/ let shouldResetClosedWindowsCache = false
func run(_ env: CmdEnv, _ io: CmdIo) async -> BinaryExitCode {
guard let target = args.resolveTargetOrReportError(env, io) else { return .fail }
var obj = [AeroObj]()
if let window = target.windowOrNil {
guard let a: WindowWithPrefetchedTitle = try? await .resolveWindow(window, for: args.args.val.flatMap { $0 }, .nonCancellable) else { return .fail(io.err(bugPrompt())) }
obj.append(AeroObj.window(a))
} else {
obj.append(AeroObj.workspace(target.workspace))
}
for argWithInterVars in args.args.val {
guard let strs = obj.format(argWithInterVars).getOrNil(onFailure: { errs in
for err in errs {
switch err {
case .unknownInterpolationVariable: io.err(noWindowIsFocused)
case .notPossible, .nullParent,
.rightPaddingCannotBeExpanded, .windowParentIllegalRelation: io.err(err.description)
}
}
}) else { return .fail }
for str in strs {
switch args.isStderr {
case true: io.err(str)
case false: io.out(str)
}
}
}
return .succ
}
}
```
## /Sources/AppBundle/command/impl/EnableCommand.swift
```swift path="/Sources/AppBundle/command/impl/EnableCommand.swift"
import AppKit
import Common
struct EnableCommand: Command {
let args: EnableCmdArgs
/*conforms*/ let shouldResetClosedWindowsCache = false
func run(_ env: CmdEnv, _ io: CmdIo) async -> BinaryExitCode {
let prevState = TrayMenuModel.shared.isEnabled
let newState: Bool = switch args.targetState.val {
case .on: true
case .off: false
case .toggle: !TrayMenuModel.shared.isEnabled
}
if newState == prevState {
switch args.failIfNoop {
case true: return .fail
case false:
let msg = newState
? "Already enabled. Tip: use --fail-if-noop to exit with non-zero code"
: "Already disabled. Tip: use --fail-if-noop to exit with non-zero code"
return .succ(io.err(msg))
}
}
TrayMenuModel.shared.isEnabled = newState
if newState {
for workspace in Workspace.all {
for window in workspace.allLeafWindowsRecursive where window.isFloating {
window.lastFloatingSize = (try? await window.getAxSize(.nonCancellable)) ?? window.lastFloatingSize
}
}
await activateMode_nonCancellable(mainModeId)
} else {
await activateMode_nonCancellable(nil)
}
return .succ
}
}
```
## /Sources/AppBundle/command/impl/EvalCommand.swift
```swift path="/Sources/AppBundle/command/impl/EvalCommand.swift"
import Common
struct EvalCommand: Command {
let args: EvalCmdArgs
/*conforms*/ let shouldResetClosedWindowsCache = true
func run(_ env: CmdEnv, _ io: CmdIo) async -> Int32ExitCode {
switch parseCommand(args.shellExpr.val, allowExecAndForget: false, allowEval: false) {
case .failure(let cmdParsingFailure):
io.err(cmdParsingFailure.msg)
return Int32ExitCode(rawValue: cmdParsingFailure.exitCode)
case .help:
io.err("--help is not supported inside eval command")
return .init(rawValue: EXIT_CODE_TWO)
case .cmd(let cmd):
return await cmd.run(env, io)
}
}
}
```
## /Sources/AppBundle/command/impl/ExecAndForgetCommand.swift
```swift path="/Sources/AppBundle/command/impl/ExecAndForgetCommand.swift"
import AppKit
import Common
struct ExecAndForgetCommand: Command {
let args: ExecAndForgetCmdArgs
/*conforms*/ let shouldResetClosedWindowsCache = false
func run(_ env: CmdEnv, _ io: CmdIo) -> BinaryExitCode {
// todo shall exec-and-forget fork exec session?
// It doesn't throw if exit code is non-zero
let process = Process()
process.environment = config.execConfig.envVariables + env.asMap
process.executableURL = URL(filePath: "/bin/bash")
process.arguments = ["-c", args.bashScript]
return .from(bool: Result { try process.run() }.isSuccess)
}
}
```
## /Sources/AppBundle/command/impl/FalseCommand.swift
```swift path="/Sources/AppBundle/command/impl/FalseCommand.swift"
import AppKit
import Common
struct FalseCommand: Command {
let args: FalseCmdArgs
/*conforms*/ let shouldResetClosedWindowsCache: Bool = false
func run(_ env: CmdEnv, _ io: CmdIo) async -> ConditionalExitCode { ._false }
}
```
## /Sources/AppBundle/command/impl/FlattenWorkspaceTreeCommand.swift
```swift path="/Sources/AppBundle/command/impl/FlattenWorkspaceTreeCommand.swift"
import AppKit
import Common
struct FlattenWorkspaceTreeCommand: Command {
let args: FlattenWorkspaceTreeCmdArgs
/*conforms*/ let shouldResetClosedWindowsCache: Bool = true
func run(_ env: CmdEnv, _ io: CmdIo) -> BinaryExitCode {
guard let target = args.resolveTargetOrReportError(env, io) else { return .fail }
let workspace = target.workspace
let windows = workspace.rootTilingContainer.allLeafWindowsRecursive
for window in windows {
window.bind(to: workspace.rootTilingContainer, adaptiveWeight: 1, index: INDEX_BIND_LAST)
}
return .succ
}
}
```
## /Sources/AppBundle/command/impl/FocusBackAndForthCommand.swift
```swift path="/Sources/AppBundle/command/impl/FocusBackAndForthCommand.swift"
import AppKit
import Common
struct FocusBackAndForthCommand: Command {
let args: FocusBackAndForthCmdArgs
/*conforms*/ let shouldResetClosedWindowsCache = false
func run(_ env: CmdEnv, _ io: CmdIo) -> BinaryExitCode {
switch prevFocus {
case let prevFocus?: .from(bool: setFocus(to: prevFocus))
case nil: .fail(io.err("Prev window has been closed"))
}
}
}
```
## /Sources/AppBundle/command/impl/FocusCommand.swift
```swift path="/Sources/AppBundle/command/impl/FocusCommand.swift"
import AppKit
import Common
struct FocusCommand: Command {
let args: FocusCmdArgs
/*conforms*/ let shouldResetClosedWindowsCache = false
func run(_ env: CmdEnv, _ io: CmdIo) async -> BinaryExitCode {
guard let target = args.resolveTargetOrReportError(env, io) else { return .fail }
if let window = target.windowOrNil, await shouldFailBecauseFullscreen_nonCancellable(
window: window,
failIfFullscreen: args.failIfFullscreen,
failIfMacosNativeFullscreen: args.failIfMacosNativeFullscreen,
) {
return .fail
}
// todo bug: floating windows break mru
let floatingWindows = args.floatingAsTiling ? await makeFloatingWindowsSeenAsTiling(workspace: target.workspace) : []
defer {
if args.floatingAsTiling {
restoreFloatingWindows(floatingWindows: floatingWindows, workspace: target.workspace)
}
}
switch args.target {
case .direction(let direction):
let window = target.windowOrNil
if let (parent, ownIndex) = window?.closestParent(hasChildrenInDirection: direction, withLayout: nil) {
guard let windowToFocus = parent.children[ownIndex + direction.focusOffset]
.findLeafWindowRecursive(snappedTo: direction.opposite) else { return .fail(io.err(bugPrompt())) }
return .from(bool: windowToFocus.focusWindow())
} else {
return hitWorkspaceBoundaries(target, io, args, direction)
}
case .windowId(let windowId):
if let windowToFocus = Window.get(byId: windowId) {
return .from(bool: windowToFocus.focusWindow())
} else {
return .fail(io.err("Can't find window with ID \(windowId)"))
}
case .dfsIndex(let dfsIndex):
if let windowToFocus = target.workspace.rootTilingContainer.allLeafWindowsRecursive.getOrNil(atIndex: Int(dfsIndex)) {
return .from(bool: windowToFocus.focusWindow())
} else {
return .fail(io.err("Can't find window with DFS index \(dfsIndex)"))
}
case .dfsRelative(let nextPrev):
let windows = target.workspace.rootTilingContainer.allLeafWindowsRecursive
guard let currentIndex = windows.firstIndex(where: { $0 == target.windowOrNil }) else {
return .fail
}
var targetIndex = switch nextPrev {
case .dfsNext: currentIndex + 1
case .dfsPrev: currentIndex - 1
}
if !(0 ..< windows.count).contains(targetIndex) {
switch args.boundariesAction {
case .stop: return .succ
case .fail: return .fail
case .wrapAroundTheWorkspace: targetIndex = (targetIndex + windows.count) % windows.count
case .wrapAroundAllMonitors: return .fail(io.err(bugPrompt("Must be discarded by args parser")))
}
}
return .from(bool: windows[targetIndex].focusWindow())
}
}
}
@MainActor private func hitWorkspaceBoundaries(
_ target: LiveFocus,
_ io: CmdIo,
_ args: FocusCmdArgs,
_ direction: CardinalDirection,
) -> BinaryExitCode {
switch args.boundaries {
case .workspace:
return switch args.boundariesAction {
case .stop: .succ
case .fail: .fail
case .wrapAroundTheWorkspace: wrapAroundTheWorkspace(target, io, direction)
case .wrapAroundAllMonitors: .fail(io.err("Must be discarded by args parser"))
}
case .allMonitorsOuterFrame:
let currentMonitor = target.workspace.workspaceMonitor
guard let (monitors, index) = currentMonitor.findRelativeMonitor(inDirection: direction) else {
return .fail(io.err(bugPrompt("Should never happen. Can't find the current monitor")))
}
if let targetMonitor = monitors.getOrNil(atIndex: index) {
return .from(bool: targetMonitor.activeWorkspace.focusWorkspace())
} else {
guard let wrapped = monitors.get(wrappingIndex: index) else { return .fail(io.err(bugPrompt("\(index) \(monitors)"))) }
return hitAllMonitorsOuterFrameBoundaries(target, io, args, direction, wrapped)
}
}
}
@MainActor private func hitAllMonitorsOuterFrameBoundaries(
_ target: LiveFocus,
_ io: CmdIo,
_ args: FocusCmdArgs,
_ direction: CardinalDirection,
_ wrappedMonitor: MonitorInfo,
) -> BinaryExitCode {
switch args.boundariesAction {
case .stop:
return .succ
case .fail:
return .fail
case .wrapAroundTheWorkspace:
return wrapAroundTheWorkspace(target, io, direction)
case .wrapAroundAllMonitors:
wrappedMonitor.activeWorkspace.findLeafWindowRecursive(snappedTo: direction.opposite)?.markAsMostRecentChild()
return .from(bool: wrappedMonitor.activeWorkspace.focusWorkspace())
}
}
@MainActor private func wrapAroundTheWorkspace(_ target: LiveFocus, _ io: CmdIo, _ direction: CardinalDirection) -> BinaryExitCode {
guard let windowToFocus = target.workspace.findLeafWindowRecursive(snappedTo: direction.opposite) else {
return .fail(io.err(noWindowIsFocused))
}
return .from(bool: windowToFocus.focusWindow())
}
@MainActor private func makeFloatingWindowsSeenAsTiling(workspace: Workspace) async -> [FloatingWindowData] {
let mruBefore = workspace.mostRecentWindowRecursive
defer {
mruBefore?.markAsMostRecentChild()
}
var _floatingWindows: [FloatingWindowData] = []
for window in workspace.floatingWindows {
// todo bug: we shouldn't access ax api here. What if the window was moved but it wasn't committed to ax yet?
guard let center = try? await window.getCenter(.nonCancellable) else { continue }
let tilingParent: TilingContainer
let index: Int
if let target = center.coerce(in: workspace.workspaceMonitor.visibleRectPaddedByOuterGaps)?
.findWindowRecursively(in: workspace.rootTilingContainer, virtual: true, fullscreenCoversAll: false)
{
guard let targetCenter = try? await target.getCenter(.nonCancellable) else { continue }
guard let _tilingParent = target.parent as? TilingContainer else { continue }
tilingParent = _tilingParent
index = switch tilingParent.layout {
case .tiles:
center.getProjection(tilingParent.orientation) >= targetCenter.getProjection(tilingParent.orientation)
? target.ownIndex.orDie() + 1
: target.ownIndex.orDie()
case .accordion:
center.getProjection(tilingParent.orientation) >= targetCenter.getProjection(tilingParent.orientation)
? tilingParent.children.count
: 0
}
} else {
index = 0
tilingParent = workspace.rootTilingContainer
}
let data = window.unbindFromParent()
let floatingWindowData = FloatingWindowData(
window: window,
center: center,
tilingParent: tilingParent,
adaptiveWeight: data.adaptiveWeight,
index: index,
)
_floatingWindows.append(floatingWindowData)
}
let floatingWindows: [FloatingWindowData] = _floatingWindows.sortedBy { $0.center.getProjection($0.tilingParent.orientation) }.reversed()
for floating in floatingWindows { // Make floating windows be seen as tiling
floating.window.bind(to: floating.tilingParent, adaptiveWeight: 1, index: floating.index)
}
return floatingWindows
}
@MainActor private func restoreFloatingWindows(floatingWindows: [FloatingWindowData], workspace: Workspace) {
let mruBefore = workspace.mostRecentWindowRecursive
defer {
mruBefore?.markAsMostRecentChild()
}
for floating in floatingWindows {
floating.window.bind(to: workspace.floatingWindowsContainer, adaptiveWeight: floating.adaptiveWeight, index: INDEX_BIND_LAST)
}
}
private struct FloatingWindowData {
let window: Window
let center: CGPoint
let tilingParent: TilingContainer
let adaptiveWeight: CGFloat
let index: Int
}
extension TreeNode {
@MainActor
func findLeafWindowRecursive(snappedTo direction: CardinalDirection) -> Window? {
switch nodeCases {
case .workspace(let workspace):
return workspace.rootTilingContainer.findLeafWindowRecursive(snappedTo: direction)
case .window(let window):
return window
case .tilingContainer(let container):
if direction.orientation == container.orientation {
return (direction.isPositive ? container.children.last : container.children.first)?
.findLeafWindowRecursive(snappedTo: direction)
} else {
return mostRecentChild?.findLeafWindowRecursive(snappedTo: direction)
}
case .macosMinimizedWindowsContainer, .macosFullscreenWindowsContainer,
.macosPopupWindowsContainer, .macosHiddenAppsWindowsContainer,
.floatingWindowsContainer:
die("Impossible")
}
}
}
```
## /Sources/AppBundle/command/impl/FocusMonitorCommand.swift
```swift path="/Sources/AppBundle/command/impl/FocusMonitorCommand.swift"
import AppKit
import Common
struct FocusMonitorCommand: Command {
let args: FocusMonitorCmdArgs
/*conforms*/ let shouldResetClosedWindowsCache = false
func run(_ env: CmdEnv, _ io: CmdIo) -> BinaryExitCode {
guard let target = args.resolveTargetOrReportError(env, io) else { return .fail }
return switch args.target.val.resolve(target.workspace.workspaceMonitor, wrapAround: args.wrapAround) {
case .success(let targetMonitor): .from(bool: targetMonitor.activeWorkspace.focusWorkspace())
case .failure(let msg): .fail(io.err(msg))
}
}
}
extension MonitorTarget {
@MainActor func resolve(_ currentMonitor: MonitorInfo, wrapAround: Bool) -> Result<MonitorInfo, String> {
switch self {
case .direction(let direction):
guard let (monitorsInDirection, index) = currentMonitor.findRelativeMonitor(inDirection: direction) else {
return .failure("Should never happen. Can't find the current monitor")
}
let targetMonitor = wrapAround ? monitorsInDirection.get(wrappingIndex: index) : monitorsInDirection.getOrNil(atIndex: index)
guard let targetMonitor else {
return .failure("No monitors in direction \(direction)")
}
return .success(targetMonitor)
case .relative(let nextPrev):
let monitors = sortedMonitorInfos
guard let curIndex = monitors.firstIndex(where: { $0.rect.topLeftCorner == currentMonitor.rect.topLeftCorner }) else {
return .failure("Can't find current monitor")
}
let targetIndex = nextPrev == .next ? curIndex + 1 : curIndex - 1
let targetMonitor = wrapAround ? monitors.get(wrappingIndex: targetIndex) : monitors.getOrNil(atIndex: targetIndex)
guard let targetMonitor else {
return .failure("Can't find target monitor")
}
return .success(targetMonitor)
case .patterns(let patterns):
let monitors = sortedMonitorInfos
guard let targetMonitor = patterns.lazy.compactMap({ $0.resolveMonitor(sortedMonitors: monitors) }).first else {
return .failure("None of the monitors match the pattern(s)")
}
return .success(targetMonitor)
}
}
}
extension MonitorInfo {
func relation(to monitor: MonitorInfo) -> Orientation {
guard let otherYRange = monitor.rect.minY.until(excl: monitor.rect.maxY) else { return .h }
guard let myYRange = rect.minY.until(excl: rect.maxY) else { return .h }
return myYRange.overlaps(otherYRange) ? .h : .v
}
func findRelativeMonitor(inDirection direction: CardinalDirection) -> (monitorsInDirection: [MonitorInfo], index: Int)? {
let currentMonitor = self
let monitors = sortedMonitorInfos.filter {
currentMonitor.rect.topLeftCorner == $0.rect.topLeftCorner ||
$0.relation(to: currentMonitor) == direction.orientation
}
guard let index = monitors.firstIndex(where: { $0.rect.topLeftCorner == currentMonitor.rect.topLeftCorner }) else { return nil }
return (monitors, index + direction.focusOffset)
}
}
```
## /Sources/AppBundle/command/impl/FullscreenCommand.swift
```swift path="/Sources/AppBundle/command/impl/FullscreenCommand.swift"
import AppKit
import Common
struct FullscreenCommand: Command {
let args: FullscreenCmdArgs
/*conforms*/ let shouldResetClosedWindowsCache = false
func run(_ env: CmdEnv, _ io: CmdIo) -> BinaryExitCode {
guard let target = args.resolveTargetOrReportError(env, io) else { return .fail }
guard let window = target.windowOrNil else {
return .fail(io.err(noWindowIsFocused))
}
let newState: Bool = switch args.toggle {
case .on: true
case .off: false
case .toggle: !window.isFullscreen
}
if newState == window.isFullscreen {
switch args.failIfNoop {
case true: return .fail
case false:
let msg = newState
? "Already fullscreen. Tip: use --fail-if-noop to exit with non-zero code"
: "Already not fullscreen. Tip: use --fail-if-noop to exit with non-zero code"
return .succ(io.err(msg))
}
}
window.isFullscreen = newState
window.noOuterGapsInFullscreen = args.noOuterGaps
// Focus on its own workspace
window.markAsMostRecentChild()
return .succ
}
}
let noWindowIsFocused = "No window is focused"
```
## /Sources/AppBundle/command/impl/JoinWithCommand.swift
```swift path="/Sources/AppBundle/command/impl/JoinWithCommand.swift"
import AppKit
import Common
struct JoinWithCommand: Command {
let args: JoinWithCmdArgs
/*conforms*/ let shouldResetClosedWindowsCache = true
func run(_ env: CmdEnv, _ io: CmdIo) -> BinaryExitCode {
let direction = args.direction.val
guard let target = args.resolveTargetOrReportError(env, io) else { return .fail }
guard let currentWindow = target.windowOrNil else {
return .fail(io.err(noWindowIsFocused))
}
guard let (parent, ownIndex) = currentWindow.closestParent(hasChildrenInDirection: direction, withLayout: nil) else {
return .fail(io.err("No windows in the specified direction"))
}
let joinWithTarget = parent.children[ownIndex + direction.focusOffset]
let prevBinding = joinWithTarget.unbindFromParent()
let newParent = TilingContainer(
parent: parent,
adaptiveWeight: prevBinding.adaptiveWeight,
parent.orientation.opposite,
.tiles,
index: prevBinding.index,
)
currentWindow.unbindFromParent()
joinWithTarget.bind(to: newParent, adaptiveWeight: WEIGHT_AUTO, index: 0)
currentWindow.bind(to: newParent, adaptiveWeight: WEIGHT_AUTO, index: direction.isPositive ? 0 : INDEX_BIND_LAST)
return .succ
}
}
```
## /Sources/AppBundle/command/impl/LayoutCommand.swift
```swift path="/Sources/AppBundle/command/impl/LayoutCommand.swift"
import AppKit
import Common
struct LayoutCommand: Command {
let args: LayoutCmdArgs
/*conforms*/ let shouldResetClosedWindowsCache = true
func run(_ env: CmdEnv, _ io: CmdIo) async -> BinaryExitCode {
guard let target = args.resolveTargetOrReportError(env, io) else { return .fail }
let node: ConventionalWindowParentCases
switch args.root ? nil : target.windowOrNil {
case let window?:
switch window.windowParentCases {
case .floatingWindowsContainer(let it):
node = .floatingWindowsContainer(it)
case .tilingContainer(let it):
node = .tilingContainer(it)
case .macosFullscreenWindowsContainer,
.macosHiddenAppsWindowsContainer,
.macosMinimizedWindowsContainer:
let msg = "Can't change layout for macOS minimized, fullscreen windows or windows or hidden apps. " +
"This behavior is subject to change"
return .fail(io.err(msg))
case .unbound, .macosPopupWindowsContainer:
return .fail(io.err(bugPrompt()))
}
case nil:
node = .tilingContainer(target.workspace.rootTilingContainer)
}
let targetDescription = args.toggleBetween.val.first(where: { !node.matchesDescription($0) })
?? args.toggleBetween.val.first.orDie()
if node.matchesDescription(targetDescription) {
switch args.failIfNoop {
case true: return .fail
case false:
let msg = "Already in the requested \(targetDescription.rawValue) mode. " +
"Tip: use --fail-if-noop to exit with non-zero exit code"
return .succ(io.err(msg))
}
}
switch targetDescription {
case .h_accordion:
return changeTilingLayout(io, targetLayout: .accordion, targetOrientation: .h, node: node)
case .v_accordion:
return changeTilingLayout(io, targetLayout: .accordion, targetOrientation: .v, node: node)
case .h_tiles:
return changeTilingLayout(io, targetLayout: .tiles, targetOrientation: .h, node: node)
case .v_tiles:
return changeTilingLayout(io, targetLayout: .tiles, targetOrientation: .v, node: node)
case .accordion:
return changeTilingLayout(io, targetLayout: .accordion, targetOrientation: nil, node: node)
case .tiles:
return changeTilingLayout(io, targetLayout: .tiles, targetOrientation: nil, node: node)
case .horizontal:
return changeTilingLayout(io, targetLayout: nil, targetOrientation: .h, node: node)
case .vertical:
return changeTilingLayout(io, targetLayout: nil, targetOrientation: .v, node: node)
case .tiling:
guard let window = target.windowOrNil else { return .fail(io.err(noWindowIsFocused)) }
switch node {
case .tilingContainer:
return .succ // Nothing to do
case .floatingWindowsContainer(let container):
window.lastFloatingSize = (try? await window.getAxSize(.nonCancellable)) ?? window.lastFloatingSize
guard let workspace = container.nodeWorkspace else { return .fail(io.err(bugPrompt())) }
do {
try await window.relayoutWindow(on: workspace, .nonCancellable, forceTile: true)
} catch {
return .fail(io.err(bugPrompt()))
}
return .succ
}
case .floating:
guard let window = target.windowOrNil else { return .fail(io.err(noWindowIsFocused)) }
let workspace = target.workspace
window.bindAsFloatingWindow(to: workspace)
if let size = window.lastFloatingSize { window.setAxFrame(nil, size) }
return .succ
}
}
}
@MainActor private func changeTilingLayout(
_ io: CmdIo,
targetLayout: Layout?,
targetOrientation: Orientation?,
node: ConventionalWindowParentCases,
) -> BinaryExitCode {
switch node {
case .floatingWindowsContainer:
return .fail(io.err("The window is non-tiling"))
case .tilingContainer(let parent):
let targetOrientation = targetOrientation ?? parent.orientation
let targetLayout = targetLayout ?? parent.layout
parent.layout = targetLayout
parent.changeOrientation(targetOrientation)
return .succ
}
}
extension ConventionalWindowParentCases {
fileprivate func matchesDescription(_ layout: LayoutCmdArgs.LayoutDescription) -> Bool {
return switch layout {
case .accordion: tilingContainerOrNil?.layout == .accordion
case .tiles: tilingContainerOrNil?.layout == .tiles
case .horizontal: tilingContainerOrNil?.orientation == .h
case .vertical: tilingContainerOrNil?.orientation == .v
case .h_accordion: tilingContainerOrNil.map { $0.layout == .accordion && $0.orientation == .h } == true
case .v_accordion: tilingContainerOrNil.map { $0.layout == .accordion && $0.orientation == .v } == true
case .h_tiles: tilingContainerOrNil.map { $0.layout == .tiles && $0.orientation == .h } == true
case .v_tiles: tilingContainerOrNil.map { $0.layout == .tiles && $0.orientation == .v } == true
case .tiling: tilingContainerOrNil != nil
case .floating: floatingWindowsContainerOrNil != nil
}
}
}
```
## /Sources/AppBundle/command/impl/ListAppsCommand.swift
```swift path="/Sources/AppBundle/command/impl/ListAppsCommand.swift"
import AppKit
import Common
struct ListAppsCommand: Command {
let args: ListAppsCmdArgs
/*conforms*/ let shouldResetClosedWindowsCache = false
func run(_ env: CmdEnv, _ io: CmdIo) -> BinaryExitCode {
var result = Array(MacApp.allAppsMap.values)
if let hidden = args.macosHidden {
result = result.filter { $0.nsApp.isHidden == hidden }
}
lazy var list = result.map(AeroObj.app)
return switch true {
case args.outputOnlyCount:
.succ(io.out("\(result.count)"))
case args.json:
switch list.formatToJson(args.format, ignoreRightPaddingVar: args._format.isEmpty) {
case .success(let json): .succ(io.out(json))
case .failure(let msg): .fail(io.err(msg))
}
default:
switch list.format(args.format) {
case .success(let lines): .succ(io.out(lines))
case .failure(let msg): .fail(io.err(msg.map(\.description).joinErrors()))
}
}
}
}
```
## /Sources/AppBundle/command/impl/ListExecEnvVarsCommand.swift
```swift path="/Sources/AppBundle/command/impl/ListExecEnvVarsCommand.swift"
import AppKit
import Common
struct ListExecEnvVarsCommand: Command {
let args: ListExecEnvVarsCmdArgs
/*conforms*/ let shouldResetClosedWindowsCache = false
func run(_ env: CmdEnv, _ io: CmdIo) -> BinaryExitCode {
for (key, value) in config.execConfig.envVariables {
io.out("\(key)=\(value)")
}
return .succ
}
}
```
## /Sources/AppBundle/command/impl/ListModesCommand.swift
```swift path="/Sources/AppBundle/command/impl/ListModesCommand.swift"
import AppKit
import Common
struct ListModesCommand: Command {
let args: ListModesCmdArgs
/*conforms*/ let shouldResetClosedWindowsCache = false
func run(_ env: CmdEnv, _ io: CmdIo) -> BinaryExitCode {
let modes: [String] = args.current ? [activeMode ?? mainModeId] : config.modes.keys.sorted()
return switch true {
case args.outputOnlyCount:
.succ(io.out("\(modes.count)"))
case args.json:
JSONEncoder.aeroSpaceDefault.encodeToString(modes.map { ["mode-id": $0] }).map { .succ(io.out($0)) }
?? .fail(io.err("Failed to encode JSON"))
default:
.succ(io.out(modes))
}
}
}
```
## /Sources/AppBundle/command/impl/ListMonitorsCommand.swift
```swift path="/Sources/AppBundle/command/impl/ListMonitorsCommand.swift"
import AppKit
import Common
struct ListMonitorsCommand: Command {
let args: ListMonitorsCmdArgs
/*conforms*/ let shouldResetClosedWindowsCache = false
func run(_ env: CmdEnv, _ io: CmdIo) -> BinaryExitCode {
let focus = focus
var result = sortedMonitorInfos
if let focused = args.focused {
result = result.filter { (monitor) in (monitor.activeWorkspace == focus.workspace) == focused }
}
if let mouse = args.mouse {
let mouseWorkspace = mouseLocation.monitorApproximation.activeWorkspace
result = result.filter { (monitor) in (monitor.activeWorkspace == mouseWorkspace) == mouse }
}
lazy var list = result.map(AeroObj.monitor)
return switch true {
case args.outputOnlyCount:
.succ(io.out("\(result.count)"))
case args.json:
switch list.formatToJson(args.format, ignoreRightPaddingVar: args._format.isEmpty) {
case .success(let json): .succ(io.out(json))
case .failure(let msg): .fail(io.err(msg))
}
default:
switch list.format(args.format) {
case .success(let lines): .succ(io.out(lines))
case .failure(let msg): .fail(io.err(msg.map(\.description).joinErrors()))
}
}
}
}
```
## /Sources/AppBundle/command/impl/ListWindowsCommand.swift
```swift path="/Sources/AppBundle/command/impl/ListWindowsCommand.swift"
import AppKit
import Common
struct ListWindowsCommand: Command {
let args: ListWindowsCmdArgs
/*conforms*/ let shouldResetClosedWindowsCache = false
func run(_ env: CmdEnv, _ io: CmdIo) async -> BinaryExitCode {
let focus = focus
var windows: [Window] = []
if args.filteringOptions.focused {
switch focus.windowOrNil {
case let window?: windows = [window]
case nil: return .fail(io.err(noWindowIsFocused))
}
} else {
var workspaces: Set<Workspace> = args.filteringOptions.workspaces.isEmpty
? Workspace.all.toSet()
: args.filteringOptions.workspaces
.flatMap { filter in
switch filter {
case .focused: [focus.workspace]
case .visible: Workspace.all.filter(\.isVisible)
case .name(let name): [Workspace.get(byName: name.raw)]
}
}
.toSet()
if !args.filteringOptions.monitors.isEmpty {
let monitors: Set<CGPoint> = args.filteringOptions.monitors.resolveMonitors(io)
if monitors.isEmpty { return .fail }
workspaces = workspaces.filter { monitors.contains($0.workspaceMonitor.rect.topLeftCorner) }
}
windows = workspaces.flatMap(\.allLeafWindowsRecursive)
if let pid = args.filteringOptions.pidFilter {
windows = windows.filter { $0.app.pid == pid }
}
if let appId = args.filteringOptions.appIdFilter {
windows = windows.filter { $0.app.rawAppBundleId == appId }
}
}
if args.outputOnlyCount {
return .succ(io.out("\(windows.count)"))
} else {
var _list: [WindowWithPrefetchedTitle] = [] // todo cleanup
for window in windows {
guard let window = try? await WindowWithPrefetchedTitle.resolveWindow(window, for: args.format, .nonCancellable) else { return .fail(io.err(bugPrompt())) }
_list.append(window)
}
_list = _list.filter { $0.window.isBound }
_list = _list.sortedBy([{ $0.window.app.name ?? "" }, { $0.title ?? "" }])
let list = _list.map { AeroObj.window($0) }
if args.json {
return switch list.formatToJson(args.format, ignoreRightPaddingVar: args._format.isEmpty) {
case .success(let json): .succ(io.out(json))
case .failure(let msg): .fail(io.err(msg))
}
} else {
return switch list.format(args.format) {
case .success(let lines): .succ(io.out(lines))
case .failure(let msg): .fail(io.err(msg.map(\.description).joinErrors()))
}
}
}
}
}
```
## /Sources/AppBundle/command/impl/ListWorkspacesCommand.swift
```swift path="/Sources/AppBundle/command/impl/ListWorkspacesCommand.swift"
import AppKit
import Common
struct ListWorkspacesCommand: Command {
let args: ListWorkspacesCmdArgs
/*conforms*/ let shouldResetClosedWindowsCache = false
func run(_ env: CmdEnv, _ io: CmdIo) -> BinaryExitCode {
var result: [Workspace] = Workspace.all
if let visible = args.filteringOptions.visible {
result = result.filter { $0.isVisible == visible }
}
if !args.filteringOptions.onMonitors.isEmpty {
let monitors: Set<CGPoint> = args.filteringOptions.onMonitors.resolveMonitors(io)
if monitors.isEmpty { return .fail }
result = result.filter { monitors.contains($0.workspaceMonitor.rect.topLeftCorner) }
}
if let empty = args.filteringOptions.empty {
result = result.filter { $0.isEffectivelyEmpty == empty }
}
lazy var list = result.map() { AeroObj.workspace($0) }
return switch true {
case args.outputOnlyCount:
.succ(io.out("\(result.count)"))
case args.json:
switch list.formatToJson(args.format, ignoreRightPaddingVar: args._format.isEmpty) {
case .success(let json): .succ(io.out(json))
case .failure(let msg): .fail(io.err(msg))
}
default:
switch list.format(args.format) {
case .success(let lines): .succ(io.out(lines))
case .failure(let msg): .fail(io.err(msg.map(\.description).joinErrors()))
}
}
}
}
extension [MonitorId] {
@MainActor func resolveMonitors(_ io: CmdIo) -> Set<CGPoint> {
var requested: Set<CGPoint> = []
let sortedMonitors = sortedMonitorInfos
for id in self {
let resolved = id.resolve(io, sortedMonitors: sortedMonitors)
if resolved.isEmpty {
return []
}
for monitor in resolved {
requested.insert(monitor.rect.topLeftCorner)
}
}
return requested
}
}
extension MonitorId {
@MainActor func resolve(_ io: CmdIo, sortedMonitors: [MonitorInfo]) -> [MonitorInfo] {
switch self {
case .focused:
return [focus.workspace.workspaceMonitor]
case .mouse:
return [mouseLocation.monitorApproximation]
case .all:
return monitorInfos
case .index(let index):
if let monitor = sortedMonitors.getOrNil(atIndex: index) {
return [monitor]
} else {
io.err("Invalid monitor ID: \(index + 1)")
return []
}
}
}
}
```
## /Sources/AppBundle/command/impl/MacosNativeFullscreenCommand.swift
```swift path="/Sources/AppBundle/command/impl/MacosNativeFullscreenCommand.swift"
import AppKit
import Common
/// Problem ID-B6E178F2: It's not first-class citizen command in AeroSpace model, since it interacts with macOS API directly.
/// Consecutive macos-native-fullscreen commands may not works as expected (because macOS may report correct state with a
/// delay), or may flicker
///
/// The same applies to macos-native-minimize command
struct MacosNativeFullscreenCommand: Command {
let args: MacosNativeFullscreenCmdArgs
/*conforms*/ let shouldResetClosedWindowsCache = false
func run(_ env: CmdEnv, _ io: CmdIo) async -> BinaryExitCode {
guard let target = args.resolveTargetOrReportError(env, io) else { return .fail }
guard let window = target.windowOrNil else {
return .fail(io.err(noWindowIsFocused))
}
guard let prevState = try? await window.isMacosFullscreen(.nonCancellable) else { return .fail(io.err(bugPrompt())) }
let newState: Bool = switch args.toggle {
case .on: true
case .off: false
case .toggle: !prevState
}
if newState == prevState {
return switch args.failIfNoop {
case true: .fail
case false:
.succ(io.err((newState ? "Already fullscreen. " : "Already not fullscreen. ") +
"Tip: use --fail-if-noop to exit with non-zero exit code"))
}
}
window.asMacWindow().setNativeFullscreen(newState)
guard let workspace = window.visualWorkspace else {
return .fail(io.err(windowIsntPartOfTree(window)))
}
if newState { // Enter fullscreen
window.bind(to: workspace.macOsNativeFullscreenWindowsContainer, adaptiveWeight: 1, index: INDEX_BIND_LAST)
} else { // Exit fullscreen
switch window.layoutReason {
case .macos(let prevParentKind):
do {
try await exitMacOsNativeUnconventionalState(
window: window,
prevParentKind: prevParentKind,
workspace: workspace,
.nonCancellable,
)
} catch {
return .fail(io.err(bugPrompt()))
}
default:
do {
try await window.relayoutWindow(on: workspace, .nonCancellable)
} catch {
return .fail(io.err(bugPrompt()))
}
}
}
return .succ
}
}
```
## /Sources/AppBundle/command/impl/MacosNativeMinimizeCommand.swift
```swift path="/Sources/AppBundle/command/impl/MacosNativeMinimizeCommand.swift"
import AppKit
import Common
/// See: MacosNativeFullscreenCommand. Problem ID-B6E178F2
struct MacosNativeMinimizeCommand: Command {
let args: MacosNativeMinimizeCmdArgs
/*conforms*/ let shouldResetClosedWindowsCache = false
func run(_ env: CmdEnv, _ io: CmdIo) async -> BinaryExitCode {
// resolveTargetOrReportError on already minimized windows will always fail
// It would be easier if minimized windows were part of the workspace in tree hierarchy
guard let target = args.resolveTargetOrReportError(env, io) else { return .fail }
guard let window = target.windowOrNil else {
return .fail(io.err(noWindowIsFocused))
}
guard let newState: Bool = try? await !window.isMacosMinimized(.nonCancellable) else { return .fail(io.err(bugPrompt())) }
window.asMacWindow().setNativeMinimized(newState)
if newState { // minimize
window.bind(to: macosMinimizedWindowsContainer, adaptiveWeight: 1, index: INDEX_BIND_LAST)
return .succ
} else { // unminimize
return .fail(io.err("The command is uncapable of unminimizing windows yet. Sorry")) // dead code. should never be possible, see the comment above
}
}
}
```
## /Sources/AppBundle/command/impl/ModeCommand.swift
```swift path="/Sources/AppBundle/command/impl/ModeCommand.swift"
import AppKit
import Common
struct ModeCommand: Command {
let args: ModeCmdArgs
/*conforms*/ let shouldResetClosedWindowsCache = false
func run(_ env: CmdEnv, _ io: CmdIo) async -> BinaryExitCode {
await activateMode_nonCancellable(args.targetMode.val)
return .succ
}
}
```
## /Sources/AppBundle/command/impl/MoveCommand.swift
```swift path="/Sources/AppBundle/command/impl/MoveCommand.swift"
import AppKit
import Common
struct MoveCommand: Command {
let args: MoveCmdArgs
/*conforms*/ let shouldResetClosedWindowsCache = true
func run(_ env: CmdEnv, _ io: CmdIo) async -> BinaryExitCode {
let direction = args.direction.val
guard let target = args.resolveTargetOrReportError(env, io) else { return .fail }
guard let currentWindow = target.windowOrNil else {
return .fail(io.err(noWindowIsFocused))
}
if await shouldFailBecauseFullscreen_nonCancellable(
window: currentWindow,
failIfFullscreen: args.failIfFullscreen,
failIfMacosNativeFullscreen: args.failIfMacosNativeFullscreen,
) {
return .fail
}
switch currentWindow.windowParentCases {
case .unbound: return .fail
case .tilingContainer(let parent):
guard let indexOfCurrent = currentWindow.ownIndex else { return .fail(io.err(bugPrompt())) }
let indexOfSiblingTarget = indexOfCurrent + direction.focusOffset
if parent.orientation == direction.orientation && parent.children.indices.contains(indexOfSiblingTarget) {
switch parent.children[indexOfSiblingTarget].tilingTreeNodeCasesOrDie() {
case .tilingContainer(let topLevelSiblingTargetContainer):
return deepMoveIn(window: currentWindow, into: topLevelSiblingTargetContainer, moveDirection: direction, io)
case .window: // "swap windows"
let prevBinding = currentWindow.unbindFromParent()
currentWindow.bind(to: parent, adaptiveWeight: prevBinding.adaptiveWeight, index: indexOfSiblingTarget)
return .succ
}
} else {
return moveOut(tilingWindow: currentWindow, direction: direction, io, args, env)
}
case .floatingWindowsContainer: // floating window
return .fail(io.err("moving floating windows isn't yet supported")) // todo
case .macosMinimizedWindowsContainer, .macosFullscreenWindowsContainer, .macosHiddenAppsWindowsContainer:
return .fail(io.err(moveOutMacosUnconventionalWindow))
case .macosPopupWindowsContainer:
return .fail(io.err(bugPrompt())) // Impossible
}
}
}
@MainActor private func hitWorkspaceBoundaries(
_ window: Window,
_ workspace: Workspace,
_ io: CmdIo,
_ args: MoveCmdArgs,
_ direction: CardinalDirection,
_ env: CmdEnv,
) -> BinaryExitCode {
switch args.boundaries {
case .workspace:
switch args.boundariesAction {
case .stop: return .succ
case .fail: return .fail
case .createImplicitContainer:
createImplicitContainerAndMoveWindow(window, workspace, direction)
return .succ
}
case .allMonitorsOuterFrame:
guard let (monitors, index) = window.nodeMonitor?.findRelativeMonitor(inDirection: direction) else {
return .fail(io.err("Should never happen. Can't find the current monitor"))
}
if monitors.indices.contains(index) {
let moveNodeToMonitorArgs = MoveNodeToMonitorCmdArgs(target: .direction(direction))
.copy(\.windowId, window.windowId)
.copy(\.focusFollowsWindow, focus.windowOrNil == window)
return MoveNodeToMonitorCommand(args: moveNodeToMonitorArgs).run(env, io)
} else {
return hitAllMonitorsOuterFrameBoundaries(window, workspace, args, direction)
}
}
}
@MainActor private func hitAllMonitorsOuterFrameBoundaries(
_ window: Window,
_ workspace: Workspace,
_ args: MoveCmdArgs,
_ direction: CardinalDirection,
) -> BinaryExitCode {
switch args.boundariesAction {
case .stop: return .succ
case .fail: return .fail
case .createImplicitContainer:
createImplicitContainerAndMoveWindow(window, workspace, direction)
return .succ
}
}
private let moveOutMacosUnconventionalWindow = "moving macOS fullscreen, minimized windows and windows of hidden apps isn't yet supported. This behavior is subject to change"
@MainActor private func moveOut(
tilingWindow window: Window,
direction: CardinalDirection,
_ io: CmdIo,
_ args: MoveCmdArgs,
_ env: CmdEnv,
) -> BinaryExitCode {
let innerMostTilingContainer = window.parents.first(where: {
return switch $0.parent?.cases {
case .tilingContainer(let parent): parent.orientation == direction.orientation
// Stop searching: we have hit the workspace
case nil, .workspace: true
// Impossible: tilingContainer's parent can only be a workspace or tilingContainer
case .floatingWindowsContainer,
.macosMinimizedWindowsContainer,
.macosFullscreenWindowsContainer,
.macosHiddenAppsWindowsContainer,
.macosPopupWindowsContainer: true
}
}) as? TilingContainer
guard let innerMostTilingContainer else { return .fail(io.err(bugPrompt())) } // Impossible
switch innerMostTilingContainer.tilingContainerParentCases {
case .unbound: return .fail
case .tilingContainer(let parent):
check(parent.orientation == direction.orientation)
guard let ownIndex = innerMostTilingContainer.ownIndex else { return .fail(io.err(bugPrompt())) }
window.bind(to: parent, adaptiveWeight: WEIGHT_AUTO, index: ownIndex + direction.insertionOffset)
return .succ
case .workspace(let parent):
return hitWorkspaceBoundaries(window, parent, io, args, direction, env)
}
}
@MainActor private func createImplicitContainerAndMoveWindow(
_ window: Window,
_ workspace: Workspace,
_ direction: CardinalDirection,
) {
let prevRoot = workspace.rootTilingContainer
prevRoot.unbindFromParent()
// Force tiles layout
_ = TilingContainer(parent: workspace, adaptiveWeight: WEIGHT_AUTO, direction.orientation, .tiles, index: 0)
check(prevRoot != workspace.rootTilingContainer)
prevRoot.bind(to: workspace.rootTilingContainer, adaptiveWeight: WEIGHT_AUTO, index: 0)
window.bind(to: workspace.rootTilingContainer, adaptiveWeight: WEIGHT_AUTO, index: direction.insertionOffset)
}
@MainActor private func deepMoveIn(window: Window, into container: TilingContainer, moveDirection: CardinalDirection, _ io: CmdIo) -> BinaryExitCode {
let deepTarget = container.tilingTreeNodeCasesOrDie().findDeepMoveInTargetRecursive(moveDirection.orientation)
switch deepTarget {
case .tilingContainer(let deepTarget):
window.bind(to: deepTarget, adaptiveWeight: WEIGHT_AUTO, index: 0)
case .window(let deepTarget):
guard let parent = deepTarget.parent as? TilingContainer else { return .fail(io.err(bugPrompt())) }
guard let deepTargetIndex = deepTarget.ownIndex else { return .fail(io.err(bugPrompt())) }
window.bind(to: parent, adaptiveWeight: WEIGHT_AUTO, index: deepTargetIndex + 1)
}
return .succ
}
extension TilingTreeNodeCases {
@MainActor fileprivate func findDeepMoveInTargetRecursive(_ orientation: Orientation) -> TilingTreeNodeCases {
switch self {
case .window:
self
case .tilingContainer(let container) where container.orientation == orientation:
.tilingContainer(container)
case .tilingContainer(let container):
container.mostRecentChild.orDie("Empty containers must be detached during normalization")
.tilingTreeNodeCasesOrDie()
.findDeepMoveInTargetRecursive(orientation)
}
}
}
func shouldFailBecauseFullscreen_nonCancellable(
window: Window,
failIfFullscreen: Bool,
failIfMacosNativeFullscreen: Bool,
) async -> Bool {
if failIfFullscreen && window.isFullscreen {
return true
}
if failIfMacosNativeFullscreen {
if true == (try? await window.isMacosFullscreen(.nonCancellable)) {
return true
}
}
return false
}
```
## /Sources/AppBundle/command/impl/MoveMouseCommand.swift
```swift path="/Sources/AppBundle/command/impl/MoveMouseCommand.swift"
import AppKit
import Common
struct MoveMouseCommand: Command {
let args: MoveMouseCmdArgs
/*conforms*/ let shouldResetClosedWindowsCache = false
func run(_ env: CmdEnv, _ io: CmdIo) async -> BinaryExitCode {
let mouse = mouseLocation
guard let target = args.resolveTargetOrReportError(env, io) else { return .fail }
switch args.mouseTarget.val {
case .windowLazyCenter:
guard let rect = await windowSubjectRectOrReportError(target, io) else { return .fail }
if rect.contains(mouse) {
return switch args.failIfNoop {
case true: .fail
case false:
.succ(io.err("The mouse already belongs to the window. Tip: use --fail-if-noop to exit with non-zero code"))
}
}
return moveMouse(io, rect.center)
case .windowForceCenter:
guard let rect = await windowSubjectRectOrReportError(target, io) else { return .fail }
return moveMouse(io, rect.center)
case .monitorLazyCenter:
let rect = target.workspace.workspaceMonitor.rect
if rect.contains(mouse) {
return switch args.failIfNoop {
case true: .fail
case false:
.succ(io.err("The mouse already belongs to the monitor. Tip: use --fail-if-noop to exit with non-zero code"))
}
}
return moveMouse(io, rect.center)
case .monitorForceCenter:
return moveMouse(io, target.workspace.workspaceMonitor.rect.center)
}
}
}
private func moveMouse(_ io: CmdIo, _ point: CGPoint) -> BinaryExitCode {
let event = CGEvent(
mouseEventSource: nil,
mouseType: CGEventType.mouseMoved,
mouseCursorPosition: point,
mouseButton: CGMouseButton.left,
)
switch event {
case nil: return .fail(io.err("Failed to move mouse"))
case let event?:
event.post(tap: CGEventTapLocation.cghidEventTap)
return .succ
}
}
@MainActor
private func windowSubjectRectOrReportError(_ target: LiveFocus, _ io: CmdIo) async -> Rect? {
// todo bug it's bad that we operate on the "ax physical" state directly. command seq won't work correctly
// focus <direction> command has the similar problem
if let window: Window = target.windowOrNil {
if let rect = window.lastAppliedLayoutPhysicalRect {
return rect
} else if let rect = try? await window.getAxRect(.nonCancellable) {
return rect
} else {
io.err("Failed to get rect of window '\(window.windowId)'")
return nil
}
} else {
io.err(noWindowIsFocused)
return nil
}
}
```
## /Sources/AppBundle/command/impl/MoveNodeToMonitorCommand.swift
```swift path="/Sources/AppBundle/command/impl/MoveNodeToMonitorCommand.swift"
import AppKit
import Common
struct MoveNodeToMonitorCommand: Command {
let args: MoveNodeToMonitorCmdArgs
/*conforms*/ let shouldResetClosedWindowsCache = true
func run(_ env: CmdEnv, _ io: CmdIo) -> BinaryExitCode {
guard let target = args.resolveTargetOrReportError(env, io) else { return .fail }
guard let window = target.windowOrNil else {
return .fail(io.err(noWindowIsFocused))
}
guard let currentMonitor = window.nodeMonitor else {
return .fail(io.err(windowIsntPartOfTree(window)))
}
switch args.target.val.resolve(currentMonitor, wrapAround: args.wrapAround) {
case .success(let targetMonitor):
let targetWs = targetMonitor.activeWorkspace
let index = true == args.target.val.directionOrNil
.map { dir in dir.isPositive && targetWs.rootTilingContainer.orientation == dir.orientation }
? 0
: INDEX_BIND_LAST
return moveWindowToWorkspace(
window,
targetWs,
io,
focusFollowsWindow: args.focusFollowsWindow,
failIfNoop: args.failIfNoop,
index: index,
)
case .failure(let msg):
return .fail(io.err(msg))
}
}
}
func windowIsntPartOfTree(_ window: Window) -> String {
"Window \(window.windowId) is not part of tree (minimized or hidden)"
}
```
## /Sources/AppBundle/command/impl/MoveNodeToWorkspaceCommand.swift
```swift path="/Sources/AppBundle/command/impl/MoveNodeToWorkspaceCommand.swift"
import Common
struct MoveNodeToWorkspaceCommand: Command {
let args: MoveNodeToWorkspaceCmdArgs
/*conforms*/ let shouldResetClosedWindowsCache: Bool = true
func run(_ env: CmdEnv, _ io: CmdIo) -> BinaryExitCode {
guard let target = args.resolveTargetOrReportError(env, io) else { return .fail }
guard let window = target.windowOrNil else { return .fail(io.err(noWindowIsFocused)) }
let subjectWs = window.nodeWorkspace
let targetWorkspace: Workspace
switch args.target.val {
case .relative(let nextPrev):
guard let subjectWs else { return .fail(io.err("Window \(window.windowId) doesn't belong to any workspace")) }
let ws = getNextPrevWorkspace(
current: subjectWs,
isNext: nextPrev == .next,
wrapAround: args.wrapAround,
stdin: args.useStdin ? io.readStdin() : nil,
target: target,
)
guard let ws = ws.getOrNil(appendErrorTo: &io.stderr) else { return .fail }
targetWorkspace = ws
case .direct(let name):
targetWorkspace = Workspace.get(byName: name.raw)
}
return moveWindowToWorkspace(window, targetWorkspace, io, focusFollowsWindow: args.focusFollowsWindow, failIfNoop: args.failIfNoop)
}
}
@MainActor
func moveWindowToWorkspace(_ window: Window, _ targetWorkspace: Workspace, _ io: CmdIo, focusFollowsWindow: Bool, failIfNoop: Bool, index: Int = INDEX_BIND_LAST) -> BinaryExitCode {
if window.nodeWorkspace == targetWorkspace {
return switch failIfNoop {
case true: .fail
case false:
.succ(io.err("Window '\(window.windowId)' already belongs to workspace '\(targetWorkspace.name)'. Tip: use --fail-if-noop to exit with non-zero code"))
}
}
let targetContainer: NonLeafTreeNodeObject = window.isFloating
? targetWorkspace.floatingWindowsContainer
: targetWorkspace.rootTilingContainer
window.bind(to: targetContainer, adaptiveWeight: WEIGHT_AUTO, index: index)
return .from(bool: focusFollowsWindow ? window.focusWindow() : true)
}
```
## /Sources/AppBundle/command/impl/MoveWorkspaceToMonitorCommand.swift
```swift path="/Sources/AppBundle/command/impl/MoveWorkspaceToMonitorCommand.swift"
import AppKit
import Common
struct MoveWorkspaceToMonitorCommand: Command {
let args: MoveWorkspaceToMonitorCmdArgs
/*conforms*/ let shouldResetClosedWindowsCache = true
func run(_ env: CmdEnv, _ io: CmdIo) -> BinaryExitCode {
guard let target = args.resolveTargetOrReportError(env, io) else { return .fail }
let focusedWorkspace = target.workspace
let prevMonitor = focusedWorkspace.workspaceMonitor
switch args.target.val.resolve(target.workspace.workspaceMonitor, wrapAround: args.wrapAround) {
case .success(let targetMonitor):
if targetMonitor.monitorId_oneBased == prevMonitor.monitorId_oneBased {
return .succ
}
if targetMonitor.setActiveWorkspace(focusedWorkspace) {
let stubWorkspace = getStubWorkspace(for: prevMonitor)
check(
prevMonitor.setActiveWorkspace(stubWorkspace),
"getStubWorkspace generated incompatible stub workspace (\(stubWorkspace)) for the monitor (\(prevMonitor)",
)
return .succ
} else {
return .fail(io.err(
"Can't move workspace '\(focusedWorkspace.name)' to monitor '\(targetMonitor.name)'. workspace-to-monitor-force-assignment doesn't allow it",
))
}
case .failure(let msg):
return .fail(io.err(msg))
}
}
}
```
## /Sources/AppBundle/command/impl/ReloadConfigCommand.swift
```swift path="/Sources/AppBundle/command/impl/ReloadConfigCommand.swift"
import AppKit
import Common
struct ReloadConfigCommand: Command {
let args: ReloadConfigCmdArgs
/*conforms*/ let shouldResetClosedWindowsCache = false
func run(_ env: CmdEnv, _ io: CmdIo) async -> BinaryExitCode {
let result = await reloadConfig_nonCancellable(args: args)
if !result.stdout.isEmpty {
io.out(result.stdout)
}
if !result.stderr.isEmpty {
io.err(result.stderr)
}
return .from(bool: result.isOk)
}
}
struct ReloadConfigResult {
let isOk: Bool
let stdout: String
let stderr: String
}
@MainActor func reloadConfig_nonCancellable(
args: ReloadConfigCmdArgs = ReloadConfigCmdArgs(rawArgs: []),
forceConfigUrl: URL? = nil,
) async -> ReloadConfigResult {
let result = readConfig(forceConfigUrl: forceConfigUrl)
let parseResult = result.parseConfigResult
let warningsAsErrors = args.warningsAsErrors
var errors = parseResult.errors.map { $0.description(.error) }
var warnings = parseResult.warnings.map { $0.description(.warning) }
let containsWarnings = !parseResult.warnings.isEmpty
if !args.noGui {
let lines = errors + warnings
switch true {
case !errors.isEmpty:
let msg = failedToParseMsg(configUrl: result.configUrl, errorsCount: parseResult.errors.count, warningsCount: parseResult.warnings.count, lines: lines)
MessageModel.shared.message = Message(body: msg, containsWarnings: containsWarnings)
case warningsAsErrors && !warnings.isEmpty:
let msg = parsedWithWarningsMsg(configUrl: result.configUrl, warningsCount: parseResult.warnings.count, lines: lines)
MessageModel.shared.message = Message(body: msg, containsWarnings: containsWarnings)
default:
MessageModel.shared.message = nil
}
}
if parseResult.allowReloadConfig && !args.dryRun {
TrayMenuModel.shared.lastReloadConfigContainedWarnings = containsWarnings
resetHotKeys()
config = parseResult.config
configUrl = result.configUrl
await activateMode_nonCancellable(activeMode)
syncStartAtLogin()
syncFocusFollowsMouse(config)
syncConfigFileWatcher()
}
if warningsAsErrors {
errors += warnings
warnings = []
}
return ReloadConfigResult(
isOk: errors.isEmpty,
stdout: errors.joined(separator: "\n\n"),
stderr: warnings.joined(separator: "\n\n"),
)
}
private func failedToParseMsg(configUrl: URL, errorsCount: Int, warningsCount: Int, lines: [String]) -> String {
let path = configUrl.absoluteURL.path.singleQuoted
let header = "Failed to parse \(path). \(errorsCount) error(s). \(warningsCount) warning(s)"
return "\(header)\n\n\(lines.joined(separator: "\n\n"))"
}
private func parsedWithWarningsMsg(configUrl: URL, warningsCount: Int, lines: [String]) -> String {
let path = configUrl.absoluteURL.path.singleQuoted
let header = "Parsed \(path) with \(warningsCount) warning(s). Feel free to close this window."
return "\(header)\n\n\(lines.joined(separator: "\n\n"))"
}
```
## /Sources/AppBundle/command/impl/ResizeCommand.swift
```swift path="/Sources/AppBundle/command/impl/ResizeCommand.swift"
import AppKit
import Common
struct ResizeCommand: Command {
let args: ResizeCmdArgs
/*conforms*/ let shouldResetClosedWindowsCache = true
func run(_ env: CmdEnv, _ io: CmdIo) -> BinaryExitCode {
guard let target = args.resolveTargetOrReportError(env, io) else { return .fail }
let candidates = target.windowOrNil?.parentsWithSelf
.filter { ($0.parent as? TilingContainer)?.layout == .tiles }
?? []
let orientation: Orientation?
let parent: TilingContainer?
let node: TreeNode?
switch args.dimension.val {
case .width:
orientation = .h
node = candidates.first(where: { ($0.parent as? TilingContainer)?.orientation == orientation })
parent = node?.parent as? TilingContainer
case .height:
orientation = .v
node = candidates.first(where: { ($0.parent as? TilingContainer)?.orientation == orientation })
parent = node?.parent as? TilingContainer
case .smart:
node = candidates.first
parent = node?.parent as? TilingContainer
orientation = parent?.orientation
case .smartOpposite:
orientation = (candidates.first?.parent as? TilingContainer)?.orientation.opposite
node = candidates.first(where: { ($0.parent as? TilingContainer)?.orientation == orientation })
parent = node?.parent as? TilingContainer
}
guard let parent else {
return .fail(io.err("resize command doesn't support floating windows yet https://github.com/nikitabobko/AeroSpace/issues/9"))
}
guard let orientation else { return .fail }
guard let node else { return .fail }
let diff: CGFloat = switch args.units.val {
case .set(let unit): CGFloat(unit) - node.getWeight(orientation)
case .add(let unit): CGFloat(unit)
case .subtract(let unit): -CGFloat(unit)
}
guard let childDiff = diff.div(parent.children.count - 1) else { return .fail }
parent.children.lazy
.filter { $0 != node }
.forEach { $0.setWeight(parent.orientation, $0.getWeight(parent.orientation) - childDiff) }
node.setWeight(orientation, node.getWeight(orientation) + diff)
return .succ
}
}
```
## /Sources/AppBundle/command/impl/RunCallbackCommand.swift
```swift path="/Sources/AppBundle/command/impl/RunCallbackCommand.swift"
import AppKit
import Common
struct RunCallbackCommand: Command {
let args: RunCallbackCmdArgs
/*conforms*/ let shouldResetClosedWindowsCache = true
func run(_ env: CmdEnv, _ io: CmdIo) async -> Int32ExitCode {
guard let target = args.resolveTargetOrReportError(env, io) else { return .fail }
switch args.callback.val {
case .onWindowDetected where args.forEveryWindow:
var exitCode = Int32ExitCode.succ
for window in MacWindow.allWindows {
exitCode = await onWindowDetected(env, io, window)
}
return exitCode
case .onWindowDetected:
guard let window = target.windowOrNil else { return .fail(io.err(noWindowIsFocused)) }
return await onWindowDetected(env, io, window)
case .onFocusChanged:
return await onFocusChanged(env, io, target)
case .onFocusedMonitorChanged:
return await onFocusedMonitorChanged(env, io, target)
}
}
}
```
## /Sources/AppBundle/command/impl/SplitCommand.swift
```swift path="/Sources/AppBundle/command/impl/SplitCommand.swift"
import AppKit
import Common
struct SplitCommand: Command {
let args: SplitCmdArgs
/*conforms*/ let shouldResetClosedWindowsCache = true
func run(_ env: CmdEnv, _ io: CmdIo) -> BinaryExitCode {
if config.enableNormalizationFlattenContainers {
return .fail(io.err("'split' has no effect when 'enable-normalization-flatten-containers' normalization enabled. My recommendation: keep the normalizations enabled, and prefer 'join-with' over 'split'."))
}
guard let target = args.resolveTargetOrReportError(env, io) else { return .fail }
guard let window = target.windowOrNil else {
return .fail(io.err(noWindowIsFocused))
}
guard let parent = window.parent else { return .fail(io.err(bugPrompt())) }
switch parent.cases {
case .floatingWindowsContainer:
// Nothing to do for floating and macOS native fullscreen windows
return .fail(io.err("Can't split floating windows"))
case .tilingContainer(let parent):
let orientation: Orientation = switch args.arg.val {
case .vertical: .v
case .horizontal: .h
case .opposite: parent.orientation.opposite
}
if parent.children.count == 1 {
parent.changeOrientation(orientation)
} else {
let data = window.unbindFromParent()
let newParent = TilingContainer(
parent: parent,
adaptiveWeight: data.adaptiveWeight,
orientation,
.tiles,
index: data.index,
)
window.bind(to: newParent, adaptiveWeight: WEIGHT_AUTO, index: 0)
}
return .succ
case .macosMinimizedWindowsContainer, .macosFullscreenWindowsContainer, .macosHiddenAppsWindowsContainer:
return .fail(io.err("Can't split macos fullscreen, minimized windows and windows of hidden apps. This behavior may change in the future"))
case .macosPopupWindowsContainer, .workspace:
return .fail(io.err(bugPrompt())) // Impossible
}
}
}
```
## /Sources/AppBundle/command/impl/SummonWorkspaceCommand.swift
```swift path="/Sources/AppBundle/command/impl/SummonWorkspaceCommand.swift"
import AppKit
import Common
struct SummonWorkspaceCommand: Command {
let args: SummonWorkspaceCmdArgs
/*conforms*/ let shouldResetClosedWindowsCache = true
func run(_ env: CmdEnv, _ io: CmdIo) -> BinaryExitCode {
let workspace = Workspace.get(byName: args.target.val.raw)
let monitor = focus.workspace.workspaceMonitor
if monitor.activeWorkspace == workspace {
return switch args.failIfNoop {
case true: .fail
case false:
.succ(io.err("Workspace '\(workspace.name)' is already visible on the focused monitor. Tip: use --fail-if-noop to exit with non-zero code"))
}
}
let prevMonitor = workspace.isVisible ? workspace.workspaceMonitor : nil
if monitor.setActiveWorkspace(workspace) {
if let prevMonitor {
let stubWorkspace = getStubWorkspace(for: prevMonitor)
check(
prevMonitor.setActiveWorkspace(stubWorkspace),
"getStubWorkspace generated incompatible stub workspace (\(stubWorkspace)) for the monitor (\(prevMonitor)",
)
}
return .from(bool: workspace.focusWorkspace())
} else {
return .fail(io.err("Can't move workspace '\(workspace.name)' to monitor '\(monitor.name)'. workspace-to-monitor-force-assignment doesn't allow it"))
}
}
}
```
## /Sources/AppBundle/command/impl/SwapCommand.swift
```swift path="/Sources/AppBundle/command/impl/SwapCommand.swift"
import AppKit
import Common
struct SwapCommand: Command {
let args: SwapCmdArgs
/*conforms*/ let shouldResetClosedWindowsCache: Bool = true
func run(_ env: CmdEnv, _ io: CmdIo) async -> BinaryExitCode {
guard let target = args.resolveTargetOrReportError(env, io) else {
return .fail
}
guard let currentWindow = target.windowOrNil else {
return .fail(io.err(noWindowIsFocused))
}
let targetWindow: Window?
switch args.target.val {
case .direction(let direction):
switch currentWindow.closestParent(hasChildrenInDirection: direction, withLayout: nil) {
case let (parent, ownIndex)?:
targetWindow = parent.children[ownIndex + direction.focusOffset].findLeafWindowRecursive(snappedTo: direction.opposite)
case nil where args.wrapAround:
targetWindow = target.workspace.findLeafWindowRecursive(snappedTo: direction.opposite)
case nil:
return .fail
}
case .dfsRelative(let nextPrev):
let windows = target.workspace.rootTilingContainer.allLeafWindowsRecursive
guard let currentIndex = windows.firstIndex(where: { $0 == target.windowOrNil }) else {
return .fail
}
var targetIndex = switch nextPrev {
case .dfsNext: currentIndex + 1
case .dfsPrev: currentIndex - 1
}
if !(0 ..< windows.count).contains(targetIndex) {
if !args.wrapAround {
return .fail
}
targetIndex = (targetIndex + windows.count) % windows.count
}
targetWindow = windows[targetIndex]
}
guard let targetWindow else {
return .fail
}
swapWindows(mruDominant: currentWindow, targetWindow)
if args.swapFocus {
return .from(bool: targetWindow.focusWindow())
}
return .succ
}
}
```
## /Sources/AppBundle/command/impl/TestCommand.swift
```swift path="/Sources/AppBundle/command/impl/TestCommand.swift"
import AppKit
import Common
struct TestCommand: Command {
let args: TestCmdArgs
/*conforms*/ let shouldResetClosedWindowsCache: Bool = false
func run(_ env: CmdEnv, _ io: CmdIo) async -> ConditionalExitCode {
guard let target = args.resolveTargetOrReportError(env, io) else { return .fail }
let lhs: Result<Primitive, InterVarExpansionError>
switch target.windowOrNil {
case let window?:
guard let window = try? await WindowWithPrefetchedTitle.resolveWindow(window, for: args.lhs.val, .nonCancellable) else { return .fail(io.err(bugPrompt())) }
lhs = args.lhs.val.expandFormatVar(obj: .window(window))
case nil:
lhs = args.lhs.val.expandFormatVar(obj: .workspace(target.workspace))
}
guard let lhs = lhs.getOrNil(onFailure: { err in
switch err {
case .unknownInterpolationVariable: io.err(noWindowIsFocused)
case .notPossible, .nullParent,
.rightPaddingCannotBeExpanded, .windowParentIllegalRelation: io.err(err.description)
}
}) else { return .fail }
let infixOperator = args.infixOperator.val
let rhs = args.rhs.val
let lhsType = lhs.kind.rawValue
let incompatibleLhsAndOperatorMsg = """
Interpolation variable: \(args.lhs.val.rawValue.singleQuoted) has a type of \(lhsType). The \(lhsType) type is not compatible with \(args.infixOperator.val.rawValue.singleQuoted) operator.
"""
let result: Result<Bool, String> = switch (lhs, infixOperator) {
case (.bool(let lhs), .equals):
Bool(rhs).toResult("Can't convert String \(rhs.singleQuoted) to Bool").map { rhs in lhs == rhs }
case (.bool, .matchesRegex):
.failure(incompatibleLhsAndOperatorMsg)
case (.int(let lhs), .equals):
Int64(rhs).toResult("Can't convert String \(rhs.singleQuoted) to Int").map { rhs in lhs == rhs }
case (.int, .matchesRegex):
.failure(incompatibleLhsAndOperatorMsg)
case (.string(let lhs), .equals):
.success(lhs == rhs)
case (.string(let lhs), .matchesRegex):
CaseInsensitiveRegex.new(rhs).map { rhs in lhs.contains(caseInsensitiveRegex: rhs) }
}
return switch result {
case .success(let result): result ? ._true : ._false
case .failure(let err): .fail(io.err(err))
}
}
}
```
## /Sources/AppBundle/command/impl/TestNotCommand.swift
```swift path="/Sources/AppBundle/command/impl/TestNotCommand.swift"
import AppKit
import Common
struct TestNotCommand: Command {
let args: TestNotCmdArgs
/*conforms*/ let shouldResetClosedWindowsCache: Bool = false
func run(_ env: CmdEnv, _ io: CmdIo) async -> ConditionalExitCode {
switch await TestCommand(args: args.testArgs).run(env, io) {
case ._false: ._true
case ._true: ._false
case .fail: .fail
}
}
}
```
## /Sources/AppBundle/command/impl/TriggerBindingCommand.swift
```swift path="/Sources/AppBundle/command/impl/TriggerBindingCommand.swift"
import AppKit
import Common
struct TriggerBindingCommand: Command {
let args: TriggerBindingCmdArgs
/*conforms*/ let shouldResetClosedWindowsCache = false
func run(_ env: CmdEnv, _ io: CmdIo) async -> Int32ExitCode {
return if let mode = config.modes[args.mode] {
if let binding = mode.bindings.values.first(where: { $0.descriptionWithKeyNotation == args.binding.val }) {
// refreshSession is not needed since commands are already run in refreshSession
await binding.commands.run(env, io)
} else {
.fail(io.err("Binding '\(args.binding.val)' is not presented in mode '\(args.mode)'"))
}
} else {
.fail(io.err("Mode '\(args.mode)' doesn't exist. " +
"Available modes: \(config.modes.keys.joined(separator: ","))"))
}
}
}
```
## /Sources/AppBundle/command/impl/TrueCommand.swift
```swift path="/Sources/AppBundle/command/impl/TrueCommand.swift"
import AppKit
import Common
struct TrueCommand: Command {
let args: TrueCmdArgs
/*conforms*/ let shouldResetClosedWindowsCache: Bool = false
func run(_ env: CmdEnv, _ io: CmdIo) async -> ConditionalExitCode { ._true }
static let instance = TrueCommand(args: TrueCmdArgs(rawArgs: []))
}
```
## /Sources/AppBundle/command/impl/VolumeCommand.swift
```swift path="/Sources/AppBundle/command/impl/VolumeCommand.swift"
import AppKit
import Common
import ISSoundAdditions
struct VolumeCommand: Command {
let args: VolumeCmdArgs
/*conforms*/ let shouldResetClosedWindowsCache = false
func run(_ env: CmdEnv, _ io: CmdIo) -> BinaryExitCode {
switch args.action.val {
case .up:
Sound.output.increaseVolume(by: 0.0625, autoMuteUnmute: true)
case .down:
Sound.output.decreaseVolume(by: 0.0625, autoMuteUnmute: true)
case .muteToggle:
Sound.output.isMuted.toggle()
case .muteOn:
Sound.output.isMuted = true
case .muteOff:
Sound.output.isMuted = false
case .set(let int):
Sound.output.setVolume(Float(int) / 100, autoMuteUnmute: true)
}
if args.gui {
do {
VolumePanel.shared.update(with: Sound.output.isMuted ? 0 : try Sound.output.readVolume())
} catch {
return .fail(io.err(bugPrompt(String(describing: error))))
}
}
return .succ
}
}
```
## /Sources/AppBundle/command/impl/WorkspaceBackAndForthCommand.swift
```swift path="/Sources/AppBundle/command/impl/WorkspaceBackAndForthCommand.swift"
import AppKit
import Common
struct WorkspaceBackAndForthCommand: Command {
let args: WorkspaceBackAndForthCmdArgs
/*conforms*/ let shouldResetClosedWindowsCache = false
func run(_ env: CmdEnv, _ io: CmdIo) -> BinaryExitCode {
return .from(bool: prevFocusedWorkspace?.focusWorkspace() != nil)
}
}
```
## /Sources/AppBundle/command/impl/WorkspaceCommand.swift
```swift path="/Sources/AppBundle/command/impl/WorkspaceCommand.swift"
import AppKit
import Common
import Foundation
struct WorkspaceCommand: Command {
let args: WorkspaceCmdArgs
/*conforms*/ let shouldResetClosedWindowsCache = false
func run(_ env: CmdEnv, _ io: CmdIo) -> BinaryExitCode { // todo refactor
guard let target = args.resolveTargetOrReportError(env, io) else { return .fail }
let focusedWs = target.workspace
let workspaceName: String
switch args.target.val {
case .relative(let nextPrev):
let workspace = getNextPrevWorkspace(
current: focusedWs,
isNext: nextPrev == .next,
wrapAround: args.wrapAround,
stdin: args.useStdin ? io.readStdin() : nil,
target: target,
)
guard let workspace = workspace.getOrNil(appendErrorTo: &io.stderr) else { return .fail }
workspaceName = workspace.name
case .direct(let name):
workspaceName = name.raw
if args.autoBackAndForth && focusedWs.name == workspaceName {
return WorkspaceBackAndForthCommand(args: WorkspaceBackAndForthCmdArgs(rawArgs: [])).run(env, io)
}
}
if focusedWs.name == workspaceName {
return switch args.failIfNoop {
case true: .fail
case false:
.succ(io.err("Workspace '\(workspaceName)' is already focused. Tip: use --fail-if-noop to exit with non-zero code"))
}
} else {
return .from(bool: Workspace.get(byName: workspaceName).focusWorkspace())
}
}
}
@MainActor func getNextPrevWorkspace(current: Workspace, isNext: Bool, wrapAround: Bool, stdin: String?, target: LiveFocus) -> ResOrStr<Workspace> {
Result { () throws(String) in
let stdinWorkspaces: [WorkspaceName] = try stdin?.split(whereSeparator: \.isWhitespace)
.map { String($0).trim() }
.filter { !$0.isEmpty }
.mapAllOrFailure(WorkspaceName.parse)
.get()
?? []
let currentMonitor = current.workspaceMonitor
let workspaces: [Workspace] = stdin != nil
? stdinWorkspaces.map { Workspace.get(byName: $0.raw) }
: Workspace.all.filter { $0.workspaceMonitor.rect.topLeftCorner == currentMonitor.rect.topLeftCorner }
.toSet()
.union([current])
.sorted()
if workspaces.isEmpty { throw "The list of workspaces is empty" }
let index = workspaces.firstIndex(where: { $0 == target.workspace })
.map { index in isNext ? index + 1 : index - 1 }
?? 0
return wrapAround
? try workspaces.get(wrappingIndex: index).toResult("List of workspaces is empty").get()
: try workspaces.getOrNil(atIndex: index)
.toResult(
index >= workspaces.count
? "Reached the end of the supplied workspaces list"
: "Rached the beginning of the supplied workspaces list",
)
.get()
}
}
```
## /Sources/AppBundle/command/parseCommand.swift
```swift path="/Sources/AppBundle/command/parseCommand.swift"
import Common
func parseCommand(
_ raw: String,
allowExecAndForget: Bool,
allowEval: Bool,
) -> ParsedCmd<Shell<any Command>> {
if allowExecAndForget && raw.starts(with: "exec-and-forget") {
return .cmd(.cmd(ExecAndForgetCommand(args: ExecAndForgetCmdArgs(bashScript: raw.removePrefix("exec-and-forget")))))
}
return switch raw.lexAndParseShell() {
case .success(let it):
it.flatMap { args in
switch parseCommand(args) {
case .cmd(_ as EvalCommand) where !allowEval: .failure("Illegal eval (Tip: nested evals are forbidden)", EXIT_CODE_TWO)
case .cmd(let it): .cmd(it)
case .failure(let it): .failure(it)
case .help(let it): .help(it)
}
}
case .failure(let it): ParsedCmd.failure(it, EXIT_CODE_TWO)
}
}
func parseCommand(_ args: [String]) -> ParsedCmd<any Command> {
parseCmdArgs(args.slice).flatMap { $0.toCommand() }
}
func expectedActualTypeError(expected: TomlType, actual: TomlType) -> String {
"Expected type is \(expected.rawValue.singleQuoted). But actual type is \(actual.rawValue.singleQuoted)"
}
func expectedActualTypeError(expected: [TomlType], actual: TomlType) -> String {
switch expected.singleOrNil() {
case let single?: expectedActualTypeError(expected: single, actual: actual)
case nil: "Expected types are \(expected.map { "'\($0)'" }.joined(separator: " or ")). But actual type is '\(actual)'"
}
}
```
## /Sources/AppBundle/config/Config.swift
```swift path="/Sources/AppBundle/config/Config.swift"
import AppKit
import Common
import HotKey
import OrderedCollections
func getDefaultConfigUrlFromProject() -> URL {
var url = URL(filePath: #filePath)
check(FileManager.default.fileExists(atPath: url.path))
while !FileManager.default.fileExists(atPath: url.appending(component: ".git").path) {
url.deleteLastPathComponent()
}
let projectRoot: URL = url
return projectRoot.appending(component: "docs/config-examples/default-config.toml")
}
var defaultConfigUrl: URL {
if isUnitTest {
return getDefaultConfigUrlFromProject()
} else {
return Bundle.main.url(forResource: "default-config", withExtension: "toml")
// Useful for debug builds that are not app bundles
?? getDefaultConfigUrlFromProject()
}
}
@MainActor let defaultConfig: Config = {
let parsedConfig = parseConfig(Result { try String(contentsOf: defaultConfigUrl, encoding: .utf8) }.getOrDie())
if !parsedConfig.errors.isEmpty {
die("Can't parse default config: \(parsedConfig.errors)")
}
return parsedConfig.config
}()
@MainActor var config: Config = defaultConfig // todo move to Ctx?
@MainActor var configUrl: URL = defaultConfigUrl
struct Config: ConvenienceMutable {
var configVersion: ConfigVersion = ._1
var _afterLoginCommand: [any Command] = []
var afterStartupCommand: Shell<any Command> = .empty
var _indentForNestedContainersWithTheSameOrientation: Void = ()
var enableNormalizationFlattenContainers: Bool = true
var _nonEmptyWorkspacesRootContainersLayoutOnStartup: Void = ()
var defaultRootContainerLayout: Layout = .tiles
var defaultRootContainerOrientation: DefaultContainerOrientation = .auto
var startAtLogin: Bool = false
var autoReloadConfig: Bool = false
var automaticallyUnhideMacosHiddenApps: Bool = false
var accordionPadding: Int = 30
var enableNormalizationOppositeOrientationForNestedContainers: Bool = true
var persistentWorkspaces: OrderedSet<String> = []
var execOnWorkspaceChange: [String] = [] // todo deprecate
var keyMapping = KeyMapping()
var execConfig: ExecConfig = ExecConfig()
var focusFollowsMouse: FocusFollowsMouse = FocusFollowsMouse()
var onFocusChanged: Shell<any Command> = .empty
// var onFocusedWorkspaceChanged: [any Command] = []
var onFocusedMonitorChanged: Shell<any Command> = .empty
var gaps: Gaps = .zero
var workspaceToMonitorForceAssignment: [String: [MonitorDescription]] = [:]
var modes: [String: Mode] = [:]
var onWindowDetected: [WindowDetectedCallback] = []
var onModeChanged: Shell<any Command> = .empty
}
struct FocusFollowsMouse: ConvenienceMutable {
var enabled: Bool = false
}
enum ConfigVersion: Int, Comparable, CaseIterable, Sendable, CustomStringConvertible {
case _1 = 1
case _2 = 2
static let max = allCases.max().orDie()
static let min = allCases.min().orDie()
static func < (lhs: ConfigVersion, rhs: ConfigVersion) -> Bool { lhs.rawValue < rhs.rawValue }
var description: String { rawValue.description }
}
enum DefaultContainerOrientation: String {
case horizontal, vertical, auto
}
```
## /Sources/AppBundle/config/ConfigFile.swift
```swift path="/Sources/AppBundle/config/ConfigFile.swift"
import Common
import Foundation
let configDotfileName = ".aerospace.toml"
func findCustomConfigUrl() -> ConfigFile {
let xdgConfigHome = ProcessInfo.processInfo.environment["XDG_CONFIG_HOME"].map { URL(filePath: $0) }
?? FileManager.default.homeDirectoryForCurrentUser.appending(path: ".config/")
let candidates: [URL] = switch serverArgs.configLocation {
case let configLocation?: [URL(filePath: configLocation)]
case nil:
[
FileManager.default.homeDirectoryForCurrentUser.appending(path: configDotfileName),
xdgConfigHome.appending(path: "aerospace").appending(path: "aerospace.toml"),
]
}
let existingCandidates: [URL] = candidates.filter { (candidate: URL) in FileManager.default.fileExists(atPath: candidate.path) }
let count = existingCandidates.count
return switch count {
case 0: .noCustomConfigExists
case 1: .file(existingCandidates.first.orDie())
default: .ambiguousConfigError(existingCandidates)
}
}
enum ConfigFile {
case file(URL), ambiguousConfigError(_ candidates: [URL]), noCustomConfigExists
var urlOrNil: URL? {
return switch self {
case .file(let url): url
case .ambiguousConfigError, .noCustomConfigExists: nil
}
}
}
```
## /Sources/AppBundle/config/ConfigFileWatcher.swift
```swift path="/Sources/AppBundle/config/ConfigFileWatcher.swift"
import Common
import Foundation
private struct ConfigFileWatcher: ~Copyable {
private let source: DispatchSourceFileSystemObject
private let fd: Int32
init?(url: URL, onChange: @escaping @MainActor () -> Void) {
fd = unsafe open(url.path, O_EVTONLY)
if fd < 0 { return nil }
source = DispatchSource.makeFileSystemObjectSource(
fileDescriptor: fd,
eventMask: [.write, .delete, .rename, .revoke],
queue: .main,
)
source.setEventHandler { MainActor.checkIsolated { onChange() } }
source.setCancelHandler { [fd] in close(fd) }
source.activate()
}
deinit {
source.cancel()
}
}
@MainActor private var currentWatcher: ConfigFileWatcher? = nil
@MainActor private var debounceTask: Task<Void, any Error>? = nil
private let debounceDelay: Duration = .milliseconds(200)
@MainActor func syncConfigFileWatcher() {
currentWatcher = nil
if !config.autoReloadConfig { return }
currentWatcher = ConfigFileWatcher(url: configUrl) {
debounceTask?.cancel()
debounceTask = Task.startUnstructured {
try await Task.sleep(for: debounceDelay)
if let token: RunSessionGuard = .isServerEnabled {
try await runLightSession(.configAutoReload, token) {
_ = await reloadConfig_nonCancellable()
}
}
}
}
}
```
## /Sources/AppBundle/config/DynamicConfigValue.swift
```swift path="/Sources/AppBundle/config/DynamicConfigValue.swift"
import Common
struct PerMonitorValue<Value: Equatable>: Equatable {
let description: MonitorDescription
let value: Value
}
extension PerMonitorValue: Sendable where Value: Sendable {}
enum DynamicConfigValue<Value: Equatable>: Equatable {
case constant(Value)
case perMonitor([PerMonitorValue<Value>], default: Value)
}
extension DynamicConfigValue: Sendable where Value: Sendable {}
extension DynamicConfigValue {
@MainActor func getValue(for monitor: any MonitorInfo) -> Value {
switch self {
case .constant(let value): return value
case .perMonitor(let array, let defaultValue):
let sortedMonitors = sortedMonitorInfos
return array
.lazy
.compactMap {
$0.description.resolveMonitor(sortedMonitors: sortedMonitors)?.rect.topLeftCorner == monitor.rect.topLeftCorner
? $0.value
: nil
}
.first ?? defaultValue
}
}
}
func parseDynamicValue<T>(
_ raw: OrderedJson,
ofType valueType: T.Type,
_ fallback: T,
_ backtrace: ConfigBacktrace,
_ c: inout ConfigParserContext,
) -> DynamicConfigValue<T> {
if let simpleValue = parseSimpleType(raw, ofType: T.self) {
return .constant(simpleValue)
} else if let array = raw.asArrayOrNil {
if array.isEmpty {
c.errors.append(.init(backtrace, "The array must not be empty"))
return .constant(fallback)
}
guard let defaultValue = array.last.flatMap({ parseSimpleType($0, ofType: T.self) }) else {
c.errors.append(.init(backtrace, "The last item in the array must be of type \(T.self)"))
return .constant(fallback)
}
if array.dropLast().isEmpty {
c.errors.append(.init(backtrace, "The array must contain at least one monitor pattern"))
return .constant(fallback)
}
let rules: [PerMonitorValue<T>] = parsePerMonitorValues(array.dropLast(), backtrace, &c)
return .perMonitor(rules, default: defaultValue)
} else {
c.errors.append(.init(backtrace, "Unsupported type: \(raw.tomlType), expected: \(valueType) or array"))
return .constant(fallback)
}
}
func parsePerMonitorValues<T>(_ array: OrderedJson.JsonArray, _ backtrace: ConfigBacktrace, _ c: inout ConfigParserContext) -> [PerMonitorValue<T>] {
array.enumerated().compactMap { (index: Int, raw: OrderedJson) -> PerMonitorValue<T>? in
var backtrace = backtrace + .index(index)
guard let (key, value) = raw.unwrapTableWithSingleKey(expectedKey: "monitor", &backtrace)
.flatMap({ $0.value.unwrapTableWithSingleKey(expectedKey: nil, &backtrace) })
.getOrNil(appendErrorTo: &c.errors)
else {
return nil
}
let monitorDescriptionResult = parseMonitorDescription(.string(key), backtrace)
guard let monitorDescription = monitorDescriptionResult.getOrNil(appendErrorTo: &c.errors) else { return nil }
guard let value = parseSimpleType(value, ofType: T.self) else {
c.errors.append(.init(backtrace, "Expected type is '\(T.self)'. But actual type is '\(value.tomlType)'"))
return nil
}
return PerMonitorValue(description: monitorDescription, value: value)
}
}
```
## /Sources/AppBundle/config/HotkeyBinding.swift
```swift path="/Sources/AppBundle/config/HotkeyBinding.swift"
import AppKit
import Common
import Foundation
import HotKey
@MainActor private var hotkeys: [String: HotKey] = [:]
@MainActor func resetHotKeys() {
// Explicitly unregister all hotkeys. We cannot always rely on destruction of the HotKey object to trigger
// unregistration because we might be running inside a hotkey handler that is keeping its HotKey object alive.
for (_, key) in hotkeys {
key.isEnabled = false
}
hotkeys = [:]
}
extension HotKey {
var isEnabled: Bool {
get { !isPaused }
set {
if isEnabled != newValue {
isPaused = !newValue
}
}
}
}
@MainActor var activeMode: String? = mainModeId
@MainActor func activateMode_nonCancellable(_ targetMode: String?) async {
let targetBindings = targetMode.flatMap { config.modes[$0] }?.bindings ?? [:]
for binding in targetBindings.values where !hotkeys.keys.contains(binding.descriptionWithKeyCode) {
hotkeys[binding.descriptionWithKeyCode] = HotKey(key: binding.keyCode, modifiers: binding.modifiers, keyDownHandler: {
Task.startUnstructured {
if let activeMode {
broadcastEvent(.bindingTriggered(
mode: activeMode,
binding: binding.descriptionWithKeyNotation,
))
try await runLightSession(.hotkeyBinding, .checkServerIsEnabledOrDie()) { () throws in
_ = await config.modes[activeMode]?.bindings[binding.descriptionWithKeyCode]?.commands
.run(.defaultEnv, .emptyStdin)
}
}
}
})
}
for (binding, key) in hotkeys {
key.isEnabled = targetBindings.keys.contains(binding)
}
let oldMode = activeMode
activeMode = targetMode
if oldMode != targetMode {
broadcastEvent(.modeChanged(mode: targetMode))
_ = await config.onModeChanged.run(.defaultEnv, .emptyStdin)
}
}
struct HotkeyBinding: Equatable, Sendable {
let modifiers: NSEvent.ModifierFlags
let keyCode: Key
let commands: Shell<any Command>
let descriptionWithKeyCode: String
let descriptionWithKeyNotation: String
init(_ modifiers: NSEvent.ModifierFlags, _ keyCode: Key, _ commands: Shell<any Command>, descriptionWithKeyNotation: String) {
self.modifiers = modifiers
self.keyCode = keyCode
self.commands = commands
self.descriptionWithKeyCode = modifiers.isEmpty
? keyCode.toString()
: modifiers.toString() + "-" + keyCode.toString()
self.descriptionWithKeyNotation = descriptionWithKeyNotation
}
static func == (lhs: HotkeyBinding, rhs: HotkeyBinding) -> Bool {
lhs.modifiers == rhs.modifiers &&
lhs.keyCode == rhs.keyCode &&
lhs.descriptionWithKeyCode == rhs.descriptionWithKeyCode &&
lhs.commands.strictEquals(rhs.commands)
}
}
func parseBindings(_ raw: OrderedJson, _ backtrace: ConfigBacktrace, _ c: inout ConfigParserContext, _ mapping: [String: Key]) -> [String: HotkeyBinding] {
guard let rawTable = raw.asDictOrNil else {
c.errors += [expectedActualTypeDiagnostic(expected: .table, actual: raw.tomlType, backtrace)]
return [:]
}
var result: [String: HotkeyBinding] = [:]
for (binding, rawCommand): (String, OrderedJson) in rawTable {
let backtrace = backtrace + .key(binding)
let binding = parseBinding(binding, backtrace, mapping)
.map { modifiers, key -> HotkeyBinding in
let commands = parseShellOfCommandsForConfig(rawCommand, backtrace, &c)
return HotkeyBinding(modifiers, key, commands, descriptionWithKeyNotation: binding)
}
.getOrNil(appendErrorTo: &c.errors)
if let binding {
if result.keys.contains(binding.descriptionWithKeyCode) {
c.errors.append(.init(backtrace, "'\(binding.descriptionWithKeyCode)' Binding redeclaration"))
}
result[binding.descriptionWithKeyCode] = binding
}
}
return result
}
func parseBinding(_ raw: String, _ backtrace: ConfigBacktrace, _ mapping: [String: Key]) -> ResOrConfigParseDiagnostic<(NSEvent.ModifierFlags, Key)> {
let rawKeys = raw.split(separator: "-")
let modifiers: ResOrConfigParseDiagnostic<NSEvent.ModifierFlags> = rawKeys.dropLast()
.mapAllOrFailure {
modifiersMap[String($0)].toResult(.init(backtrace, "Can't parse modifiers in '\(raw)' binding"))
}
.map { NSEvent.ModifierFlags($0) }
let key: ResOrConfigParseDiagnostic<Key> = rawKeys.last.flatMap { mapping[String($0)] }
.toResult(.init(backtrace, "Can't parse the key in '\(raw)' binding"))
return modifiers.flatMap { modifiers -> ResOrConfigParseDiagnostic<(NSEvent.ModifierFlags, Key)> in
key.flatMap { key -> ResOrConfigParseDiagnostic<(NSEvent.ModifierFlags, Key)> in
.success((modifiers, key))
}
}
}
```
## /Sources/AppBundle/config/Mode.swift
```swift path="/Sources/AppBundle/config/Mode.swift"
import Common
import HotKey
struct Mode: ConvenienceMutable, Equatable, Sendable {
var bindings: [String: HotkeyBinding]
static let zero = Mode(bindings: [:])
}
func parseModes(_ raw: OrderedJson, _ backtrace: ConfigBacktrace, _ c: inout ConfigParserContext, _ mapping: [String: Key]) -> [String: Mode] {
guard let rawTable = raw.asDictOrNil else {
c.errors += [expectedActualTypeDiagnostic(expected: .table, actual: raw.tomlType, backtrace)]
return [:]
}
var result: [String: Mode] = [:]
for (key, value) in rawTable {
result[key] = parseMode(value, backtrace + .key(key), &c, mapping)
}
if !result.keys.contains(mainModeId) {
c.errors += [.init(backtrace, "Please specify '\(mainModeId)' mode")]
}
return result
}
func parseMode(_ raw: OrderedJson, _ backtrace: ConfigBacktrace, _ c: inout ConfigParserContext, _ mapping: [String: Key]) -> Mode {
guard let rawTable: OrderedJson.JsonDict = raw.asDictOrNil else {
c.errors += [expectedActualTypeDiagnostic(expected: .table, actual: raw.tomlType, backtrace)]
return .zero
}
var result: Mode = .zero
for (key, value) in rawTable {
let backtrace = backtrace + .key(key)
switch key {
case "binding":
result.bindings = parseBindings(value, backtrace, &c, mapping)
default:
c.errors += [unknownKeyDiagnostic(backtrace)]
}
}
return result
}
```
## /Sources/AppBundle/config/keysMap.swift
```swift path="/Sources/AppBundle/config/keysMap.swift"
import AppKit
import Common
import HotKey
private let minus = "minus"
private let equal = "equal"
private let q = "q"
private let w = "w"
private let e = "e"
private let r = "r"
private let t = "t"
private let y = "y"
private let u = "u"
private let i = "i"
private let o = "o"
private let p = "p"
private let leftSquareBracket = "leftSquareBracket"
private let rightSquareBracket = "rightSquareBracket"
private let backslash = "backslash"
private let sectionSign = "sectionSign"
private let a = "a"
private let s = "s"
private let d = "d"
private let f = "f"
private let g = "g"
private let h = "h"
private let j = "j"
private let k = "k"
private let l = "l"
private let semicolon = "semicolon"
private let quote = "quote"
private let z = "z"
private let x = "x"
private let c = "c"
private let v = "v"
private let b = "b"
private let n = "n"
private let m = "m"
private let comma = "comma"
private let period = "period"
private let slash = "slash"
func getKeysPreset(_ layout: KeyMapping.Preset) -> [String: Key] {
return switch layout {
case .qwerty: keyNotationToKeyCode
case .dvorak: dvorakMap
case .colemak: colemakMap
}
}
extension Key: @unchecked @retroactive Sendable {}
let keyNotationToKeyCode: [String: Key] = [
sectionSign: .section,
"0": .zero,
"1": .one,
"2": .two,
"3": .three,
"4": .four,
"5": .five,
"6": .six,
"7": .seven,
"8": .eight,
"9": .nine,
minus: .minus,
equal: .equal,
q: .q,
w: .w,
e: .e,
r: .r,
t: .t,
y: .y,
u: .u,
i: .i,
o: .o,
p: .p,
leftSquareBracket: .leftBracket,
rightSquareBracket: .rightBracket,
backslash: .backslash,
a: .a,
s: .s,
d: .d,
f: .f,
g: .g,
h: .h,
j: .j,
k: .k,
l: .l,
semicolon: .semicolon,
quote: .quote,
z: .z,
x: .x,
c: .c,
v: .v,
b: .b,
n: .n,
m: .m,
comma: .comma,
period: .period,
slash: .slash,
"keypad0": .keypad0,
"keypad1": .keypad1,
"keypad2": .keypad2,
"keypad3": .keypad3,
"keypad4": .keypad4,
"keypad5": .keypad5,
"keypad6": .keypad6,
"keypad7": .keypad7,
"keypad8": .keypad8,
"keypad9": .keypad9,
"keypadClear": .keypadClear,
"keypadDecimalMark": .keypadDecimal,
"keypadDivide": .keypadDivide,
"keypadEnter": .keypadEnter,
"keypadEqual": .keypadEquals,
"keypadMinus": .keypadMinus,
"keypadMultiply": .keypadMultiply,
"keypadPlus": .keypadPlus,
"pageUp": .pageUp,
"pageDown": .pageDown,
"home": .home,
"end": .end,
"forwardDelete": .forwardDelete,
"f1": .f1,
"f2": .f2,
"f3": .f3,
"f4": .f4,
"f5": .f5,
"f6": .f6,
"f7": .f7,
"f8": .f8,
"f9": .f9,
"f10": .f10,
"f11": .f11,
"f12": .f12,
"f13": .f13,
"f14": .f14,
"f15": .f15,
"f16": .f16,
"f17": .f17,
"f18": .f18,
"f19": .f19,
"f20": .f20,
"backtick": .grave,
"space": .space,
"enter": .return,
"esc": .escape,
"backspace": .delete,
"tab": .tab,
"left": .leftArrow,
"down": .downArrow,
"up": .upArrow,
"right": .rightArrow,
]
private let dvorakMap: [String: Key] = keyNotationToKeyCode + [
leftSquareBracket: .minus,
rightSquareBracket: .equal,
quote: .q,
comma: .w,
period: .e,
p: .r,
y: .t,
f: .y,
g: .u,
c: .i,
r: .o,
l: .p,
slash: .leftBracket, // leftBracket -> leftSquareBracket
equal: .rightBracket, // rightBracket -> rightSquareBracket
backslash: .backslash,
a: .a,
o: .s,
e: .d,
u: .f,
i: .g,
d: .h,
h: .j,
t: .k,
n: .l,
s: .semicolon,
minus: .quote,
semicolon: .z,
q: .x,
j: .c,
k: .v,
x: .b,
b: .n,
m: .m,
w: .comma,
v: .period,
z: .slash,
]
private let colemakMap: [String: Key] = keyNotationToKeyCode + [
q: .q,
w: .w,
f: .e,
p: .r,
g: .t,
j: .y,
l: .u,
u: .i,
y: .o,
semicolon: .p,
leftSquareBracket: .leftBracket,
rightSquareBracket: .rightBracket,
backslash: .backslash,
a: .a,
r: .s,
s: .d,
t: .f,
d: .g,
h: .h,
n: .j,
e: .k,
i: .l,
o: .semicolon,
quote: .quote,
z: .z,
x: .x,
c: .c,
v: .v,
b: .b,
k: .n,
m: .m,
comma: .comma,
period: .period,
slash: .slash,
]
let modifiersMap: [String: NSEvent.ModifierFlags] = [
"shift": .shift,
"alt": .option,
"ctrl": .control,
"cmd": .command,
]
extension NSEvent.ModifierFlags {
func toString() -> String {
var result: [String] = []
if contains(.option) { result.append("alt") }
if contains(.control) { result.append("ctrl") }
if contains(.command) { result.append("cmd") }
if contains(.shift) { result.append("shift") }
return result.joined(separator: "-")
}
}
extension Key {
func toString() -> String {
switch self {
case .a: "a"
case .b: "b"
case .c: "c"
case .d: "d"
case .e: "e"
case .f: "f"
case .g: "g"
case .h: "h"
case .i: "i"
case .j: "j"
case .k: "k"
case .l: "l"
case .m: "m"
case .n: "n"
case .o: "o"
case .p: "p"
case .q: "q"
case .r: "r"
case .s: "s"
case .t: "t"
case .u: "u"
case .v: "v"
case .w: "w"
case .x: "x"
case .y: "y"
case .z: "z"
case .zero: "0"
case .one: "1"
case .two: "2"
case .three: "3"
case .four: "4"
case .five: "5"
case .six: "6"
case .seven: "7"
case .eight: "8"
case .nine: "9"
case .period: "period"
case .quote: "quote"
case .leftBracket: "leftSquareBracket"
case .rightBracket: "rightSquareBracket"
case .semicolon: "semicolon"
case .slash: "slash"
case .backslash: "backslash"
case .comma: "comma"
case .equal: "equal"
case .grave: "backtick"
case .minus: "minus"
case .space: "space"
case .tab: "tab"
case .return: "enter"
case .pageUp: "pageUp"
case .pageDown: "pageDown"
case .home: "home"
case .end: "end"
case .leftArrow: "left"
case .downArrow: "down"
case .upArrow: "up"
case .rightArrow: "right"
case .escape: "esc"
case .delete: "backspace"
case .section: "sectionSign"
case .f1: "f1"
case .f2: "f2"
case .f3: "f3"
case .f4: "f4"
case .f5: "f5"
case .f6: "f6"
case .f7: "f7"
case .f8: "f8"
case .f9: "f9"
case .f10: "f10"
case .f11: "f11"
case .f12: "f12"
case .f13: "f13"
case .f14: "f14"
case .f15: "f15"
case .f16: "f16"
case .f17: "f17"
case .f18: "f18"
case .f19: "f19"
case .f20: "f20"
case .keypad0: "keypad0"
case .keypad1: "keypad1"
case .keypad2: "keypad2"
case .keypad3: "keypad3"
case .keypad4: "keypad4"
case .keypad5: "keypad5"
case .keypad6: "keypad6"
case .keypad7: "keypad7"
case .keypad8: "keypad8"
case .keypad9: "keypad9"
case .keypadClear: "keypadClear"
case .keypadDecimal: "keypadDecimalMark"
case .keypadDivide: "keypadDivide"
case .keypadEnter: "keypadEnter"
case .keypadEquals: "keypadEqual"
case .keypadMinus: "keypadMinus"
case .keypadMultiply: "keypadMultiply"
case .keypadPlus: "keypadPlus"
// wtf
case .command: "cmd"
case .rightCommand: "rCmd"
case .option: "alt"
case .rightOption: "rAlt"
case .control: "ctrl"
case .rightControl: "rCtrl"
case .shift: "shift"
case .rightShift: "rShift"
case .function: "function"
case .capsLock: "capsLock"
case .forwardDelete: "forwardDelete"
case .help: "help"
case .volumeUp: "volumeUp"
case .volumeDown: "volumeDown"
case .mute: "mute"
}
}
}
// doesn't work :(
//extension NSEvent.ModifierFlags {
// static let lOption = NSEvent.ModifierFlags(rawValue: 1 << 1)
// static let rOption = NSEvent.ModifierFlags(rawValue: 1 << 2)
// static let lShift = NSEvent.ModifierFlags(rawValue: 0x00000002)
// static let rShift = NSEvent.ModifierFlags(rawValue: 0x00000004)
// static let lCommand = NSEvent.ModifierFlags(rawValue: 1 << 7)
// static let rCommand = NSEvent.ModifierFlags(rawValue: 0x00000010)
//}
// NSEvent.ModifierFlags.command.rawValue // 1 << 20
// NSEvent.ModifierFlags.option.rawValue // 1 << 19
// NSEvent.ModifierFlags.control.rawValue // 1 << 18
// NSEvent.ModifierFlags.shift.rawValue // 1 << 17
// https://github.com/koekeishiya/skhd/blob/master/src/hotkey.h
```
## /Sources/AppBundle/config/parseConfig.swift
```swift path="/Sources/AppBundle/config/parseConfig.swift"
import AppKit
import Common
import HotKey
import TOMLDecoder
import OrderedCollections
struct ReadConfigResult {
let configUrl: URL
let parseConfigResult: ParseConfigResult
@MainActor static func fatal(configUrl: URL, message: String) -> Self {
ReadConfigResult(
configUrl: configUrl,
parseConfigResult: ParseConfigResult(
config: defaultConfig,
errors: [.init(.emptyRoot, message, preventConfigReload: true)],
warnings: [],
),
)
}
}
@MainActor
func readConfig(forceConfigUrl: URL?) -> ReadConfigResult {
let configUrl: URL
if let forceConfigUrl {
configUrl = forceConfigUrl
} else {
switch findCustomConfigUrl() {
case .file(let url): configUrl = url
case .noCustomConfigExists: configUrl = defaultConfigUrl
case .ambiguousConfigError(let candidates):
let msg = """
Ambiguous config error. Several configs found:
\(candidates.map(\.path).joined(separator: "\n"))
"""
return .fatal(configUrl: defaultConfigUrl, message: msg)
}
}
let configStr: String
do {
configStr = try String(contentsOf: configUrl, encoding: .utf8)
} catch {
let msg = "Can't read contents of \(configUrl.path.singleQuoted) as a utf8 string: \(error.localizedDescription)"
return .fatal(configUrl: configUrl, message: msg)
}
return ReadConfigResult(configUrl: configUrl, parseConfigResult: parseConfig(configStr))
}
struct ConfigParseDiagnostic: Error, Equatable {
let backtrace: ConfigBacktrace
let message: String
let preventConfigReload: Bool // for severe config errors (like TOML parse error)
public init(_ backtrace: ConfigBacktrace, _ message: String, preventConfigReload: Bool = false) {
check(!message.isEmpty)
self.backtrace = backtrace
self.message = message
self.preventConfigReload = preventConfigReload
}
func description(_ severity: Severity) -> String {
let backtraceDesc = backtrace.description
// todo Make 'split' + flatten normalization prettier
return switch backtraceDesc.isEmpty {
case true: "[\(severity.rawValue)] \(message)"
case false: "[\(severity.rawValue)] \(backtraceDesc): \(message)"
}
}
enum Severity: String {
case warning = "WARNING"
case error = "ERROR"
}
}
typealias ResOrConfigParseDiagnostic<T> = Result<T, ConfigParseDiagnostic>
extension ParserProtocol {
func transformRawConfig(_ raw: S,
_ value: OrderedJson,
_ backtrace: ConfigBacktrace,
_ c: inout ConfigParserContext) -> S
{
if let value = parse(value, backtrace, &c).getOrNil(appendErrorTo: &c.errors) {
return raw.copy(keyPath, value)
}
return raw
}
}
struct ConfigParserContext {
var configVersion: ConfigVersion
var errors: [ConfigParseDiagnostic]
var warnings: [ConfigParseDiagnostic]
}
protocol ParserProtocol<S>: Sendable {
associatedtype T
associatedtype S where S: ConvenienceMutable
var keyPath: SendableWritableKeyPath<S, T> { get }
var parse: @Sendable (OrderedJson, ConfigBacktrace, inout ConfigParserContext) -> ResOrConfigParseDiagnostic<T> { get }
}
struct Parser<S: ConvenienceMutable, T>: ParserProtocol {
let keyPath: SendableWritableKeyPath<S, T>
let parse: @Sendable (OrderedJson, ConfigBacktrace, inout ConfigParserContext) -> ResOrConfigParseDiagnostic<T>
init(_ keyPath: SendableWritableKeyPath<S, T>, _ parse: @escaping @Sendable (OrderedJson, ConfigBacktrace, inout ConfigParserContext) -> T) {
self.keyPath = keyPath
self.parse = { raw, backtrace, errors -> ResOrConfigParseDiagnostic<T> in .success(parse(raw, backtrace, &errors)) }
}
init(_ keyPath: SendableWritableKeyPath<S, T>, _ parse: @escaping @Sendable (OrderedJson, ConfigBacktrace) -> ResOrConfigParseDiagnostic<T>) {
self.keyPath = keyPath
self.parse = { raw, backtrace, _ -> ResOrConfigParseDiagnostic<T> in parse(raw, backtrace) }
}
}
private let keyMappingConfigRootKey = "key-mapping"
private let configVersionConfigRootKey = "config-version"
private let modeConfigRootKey = "mode"
private let persistentWorkspacesKey = "persistent-workspaces"
// For every new config option you add, think:
// 1. Does it make sense to have different value
// 2. Prefer commands and commands flags over toml options if possible
private let configParser: [String: any ParserProtocol<Config>] = [
configVersionConfigRootKey: Parser(\.configVersion, skipParsing(Config().configVersion)), // Parsed manually
"after-login-command": Parser(\._afterLoginCommand, parseDeprecatedAfterLoginCommand),
"after-startup-command": Parser(\.afterStartupCommand, parseShellOfCommandsForConfig),
"on-focus-changed": Parser(\.onFocusChanged, parseShellOfCommandsForConfig),
"on-mode-changed": Parser(\.onModeChanged, parseShellOfCommandsForConfig),
"on-focused-monitor-changed": Parser(\.onFocusedMonitorChanged, parseShellOfCommandsForConfig),
// "on-focused-workspace-changed": Parser(\.onFocusedWorkspaceChanged, { parseCommandOrCommands($0).toParsedConfig($1) }),
"enable-normalization-flatten-containers": Parser(\.enableNormalizationFlattenContainers, parseBool),
"enable-normalization-opposite-orientation-for-nested-containers": Parser(\.enableNormalizationOppositeOrientationForNestedContainers, parseBool),
"default-root-container-layout": Parser(\.defaultRootContainerLayout, parseLayout),
"default-root-container-orientation": Parser(\.defaultRootContainerOrientation, parseDefaultContainerOrientation),
"start-at-login": Parser(\.startAtLogin, parseBool),
"auto-reload-config": Parser(\.autoReloadConfig, parseBool),
"automatically-unhide-macos-hidden-apps": Parser(\.automaticallyUnhideMacosHiddenApps, parseBool),
"accordion-padding": Parser(\.accordionPadding, parseInt),
persistentWorkspacesKey: Parser(\.persistentWorkspaces, parsePersistentWorkspaces),
"exec-on-workspace-change": Parser(\.execOnWorkspaceChange, parseArrayOfStrings),
"exec": Parser(\.execConfig, parseExecConfig),
keyMappingConfigRootKey: Parser(\.keyMapping, skipParsing(Config().keyMapping)), // Parsed manually
modeConfigRootKey: Parser(\.modes, skipParsing(Config().modes)), // Parsed manually
"gaps": Parser(\.gaps, parseGaps),
"focus-follows-mouse": Parser(\.focusFollowsMouse, parseFocusFollowsMouse),
"workspace-to-monitor-force-assignment": Parser(\.workspaceToMonitorForceAssignment, parseWorkspaceToMonitorAssignment),
"on-window-detected": Parser(\.onWindowDetected, parseOnWindowDetectedArray),
// Deprecated
"non-empty-workspaces-root-containers-layout-on-startup": Parser(\._nonEmptyWorkspacesRootContainersLayoutOnStartup, parseStartupRootContainerLayout),
"indent-for-nested-containers-with-the-same-orientation": Parser(\._indentForNestedContainersWithTheSameOrientation, parseIndentForNestedContainersWithTheSameOrientation),
]
extension ParsedCmd {
func toResult() -> ResOrStr<T> {
return switch self {
case .cmd(let a): .success(a)
case .help(let a): .failure(a)
case .failure(let a): .failure(a.msg)
}
}
}
func parseDeprecatedAfterLoginCommand(_ raw: OrderedJson, _ backtrace: ConfigBacktrace) -> ResOrConfigParseDiagnostic<[any Command]> {
if let array = raw.asArrayOrNil, array.count == 0 {
return .success([])
}
let msg = "after-login-command is deprecated since AeroSpace 0.19.0. https://github.com/nikitabobko/AeroSpace/issues/1482"
return .failure(.init(backtrace, msg))
}
func parseShellOfCommandsForConfig(_ raw: OrderedJson, _ backtrace: ConfigBacktrace, _ c: inout ConfigParserContext) -> Shell<any Command> {
if let rawString = raw.asStringOrNil {
return parseCommand(rawString, allowExecAndForget: true, allowEval: false).toResult().toParsedConfig(backtrace).getOrNil(appendErrorTo: &c.errors) ?? .empty
} else if let rawArray = raw.asArrayOrNil {
var result = [Shell<any Command>]()
for (index, elem) in rawArray.enumerated() {
let backtrace = backtrace + .index(index)
if let elem = elem.asStringOrNil {
result.append(parseCommand(elem, allowExecAndForget: true, allowEval: false).toResult().toParsedConfig(backtrace).getOrNil(appendErrorTo: &c.errors) ?? .empty)
} else {
c.errors.append(.init(backtrace, expectedActualTypeError(expected: .string, actual: elem.tomlType)))
}
}
return .newCompound(result, Shell<any Command>.seq)
} else {
c.errors.append(.init(backtrace, expectedActualTypeError(expected: [.string, .array], actual: raw.tomlType)))
return .empty
}
}
func tomlAnyToOrderedJsonRecursive(
any: Any,
_ backtrace: ConfigBacktrace,
_ errors: inout [ConfigParseDiagnostic],
) -> OrderedJson? {
switch any {
case let dict as [String: Any]:
var json = OrderedJson.JsonDict()
for (key, tomlValue) in dict.sortedEntries {
json[key] = tomlAnyToOrderedJsonRecursive(any: tomlValue, backtrace + .key(key), &errors)
}
return .dict(json)
case let array as [Any]:
var json = OrderedJson.JsonArray()
for (index, tomlValue) in array.enumerated() {
let element = tomlAnyToOrderedJsonRecursive(any: tomlValue, backtrace + .index(index), &errors)
guard let element else { continue }
json.append(element)
}
return .array(json)
default:
if let value = OrderedJson.newScalarOrNil(any) { return value }
errors.append(.init(backtrace, "Unsupported TOML type: \(type(of: any))"))
return nil
}
}
struct ParseConfigResult {
let config: Config
let errors: [ConfigParseDiagnostic]
let warnings: [ConfigParseDiagnostic]
var allowReloadConfig: Bool { errors.allSatisfy { !$0.preventConfigReload } }
}
@MainActor func parseConfig(_ rawToml: String) -> ParseConfigResult {
var errors = NonCopyable([ConfigParseDiagnostic]())
let rawTable: OrderedJson.JsonDict
do {
let dict: [String: Any] = try .init(try TOMLTable(source: rawToml))
let json = tomlAnyToOrderedJsonRecursive(any: dict, .emptyRoot, &errors.value)
switch json {
case .dict(let dict): rawTable = dict
default: // dead code
let msg = "Config parsing error: the top level type must be a TOML Table. But got: \((json ?? .null).tomlType)"
errors.value.append(.init(.emptyRoot, msg, preventConfigReload: true))
rawTable = [:]
}
} catch {
errors.value.append(.init(.emptyRoot, error.description, preventConfigReload: true))
rawTable = [:]
}
let configVersion: ConfigVersion = rawTable[configVersionConfigRootKey]
.flatMap { parseConfigVersion($0, .rootKey(configVersionConfigRootKey)).getOrNil(appendErrorTo: &errors.value) }
?? .min
var c = ConfigParserContext(configVersion: configVersion, errors: errors.consume(), warnings: [ConfigParseDiagnostic]())
var config = rawTable.parseTable(Config(), configParser, .emptyRoot, &c)
config.configVersion = configVersion
if let mapping = rawTable[keyMappingConfigRootKey].flatMap({ parseKeyMapping($0, .rootKey(keyMappingConfigRootKey), &c) }) {
config.keyMapping = mapping
}
// Parse modeConfigRootKey after keyMappingConfigRootKey
if let modes = rawTable[modeConfigRootKey].flatMap({ parseModes($0, .rootKey(modeConfigRootKey), &c, config.keyMapping.resolve()) }) {
config.modes = modes
}
if config.configVersion <= ._1 {
if rawTable.keys.contains(persistentWorkspacesKey) {
c.errors += [.init(.rootKey(persistentWorkspacesKey), "This config option is only available since 'config-version = 2'")]
}
config.persistentWorkspaces = (config.modes.values.lazy
.flatMap { (mode: Mode) -> [HotkeyBinding] in Array(mode.bindings.values) }
.flatMap { (binding: HotkeyBinding) -> [String] in
let commands = binding.commands.flatten()
return commands.filterIsInstance(of: WorkspaceCommand.self).compactMap { $0.args.target.val.workspaceNameOrNil()?.raw } +
commands.filterIsInstance(of: MoveNodeToWorkspaceCommand.self).compactMap { $0.args.target.val.workspaceNameOrNil()?.raw }
}
+ (config.workspaceToMonitorForceAssignment).keys)
.toOrderedSet()
}
if config.enableNormalizationFlattenContainers {
let containsSplitCommand = config.modes.values.lazy.flatMap { $0.bindings.values }
.flatMap { $0.commands.flatten() }
.contains { $0 is SplitCommand }
if containsSplitCommand {
c.errors += [.init(
.emptyRoot, // todo Make 'split' + flatten normalization prettier
"""
The config contains:
1. usage of 'split' command
2. enable-normalization-flatten-containers = true
These two settings don't play nicely together. 'split' command has no effect when enable-normalization-flatten-containers is disabled.
My recommendation: keep the normalizations enabled, and prefer 'join-with' over 'split'.
""",
)]
}
}
if config.configVersion < .max {
let msg = "The current 'config-version = \(config.configVersion)' is outdated. " +
"Please consider migrating to 'config-version = \(ConfigVersion.max)'. " +
"See https://nikitabobko.github.io/AeroSpace/guide#config-version for the migration guide."
c.warnings.append(.init(.emptyRoot, msg))
}
return ParseConfigResult(config: config, errors: c.errors, warnings: c.warnings)
}
func parseIndentForNestedContainersWithTheSameOrientation(_ _: OrderedJson, _ backtrace: ConfigBacktrace) -> ResOrConfigParseDiagnostic<Void> {
let msg = "Deprecated. Please drop it from the config. See https://github.com/nikitabobko/AeroSpace/issues/96"
return .failure(.init(backtrace, msg))
}
func parseConfigVersion(_ raw: OrderedJson, _ backtrace: ConfigBacktrace) -> ResOrConfigParseDiagnostic<ConfigVersion> {
parseInt(raw, backtrace)
.flatMap { ConfigVersion.init(rawValue: $0).toResult(.init(backtrace, "config-version must be in [\(ConfigVersion.min), \(ConfigVersion.max)] range")) }
}
func parseInt(_ raw: OrderedJson, _ backtrace: ConfigBacktrace) -> ResOrConfigParseDiagnostic<Int> {
raw.asIntOrNil.toResult(expectedActualTypeDiagnostic(expected: .int, actual: raw.tomlType, backtrace))
}
func parseString(_ raw: OrderedJson, _ backtrace: ConfigBacktrace) -> ResOrConfigParseDiagnostic<String> {
raw.asStringOrNil.toResult(expectedActualTypeDiagnostic(expected: .string, actual: raw.tomlType, backtrace))
}
func parseSimpleType<T>(_ raw: OrderedJson, ofType: T.Type) -> T? {
(raw.asIntOrNil as? T) ?? (raw.asStringOrNil as? T) ?? (raw.asBoolOrNil as? T)
}
extension OrderedJson {
func unwrapTableWithSingleKey(expectedKey: String? = nil, _ backtrace: inout ConfigBacktrace) -> ResOrConfigParseDiagnostic<(key: String, value: OrderedJson)> {
guard let asDictOrNil else {
return .failure(expectedActualTypeDiagnostic(expected: .table, actual: tomlType, backtrace))
}
let singleKeyError: ConfigParseDiagnostic = .init(
backtrace,
expectedKey != nil
? "The table is expected to have a single key '\(expectedKey.orDie())'"
: "The table is expected to have a single key",
)
guard let (actualKey, value): (String, OrderedJson) = asDictOrNil.count == 1 ? asDictOrNil.first : nil else {
return .failure(singleKeyError)
}
if expectedKey != nil && expectedKey != actualKey {
return .failure(singleKeyError)
}
backtrace = backtrace + .key(actualKey)
return .success((actualKey, value))
}
}
func parseTomlArray(_ raw: OrderedJson, _ backtrace: ConfigBacktrace) -> ResOrConfigParseDiagnostic<OrderedJson.JsonArray> {
raw.asArrayOrNil.toResult(expectedActualTypeDiagnostic(expected: .array, actual: raw.tomlType, backtrace))
}
func parseTable<T: ConvenienceMutable>(
_ raw: OrderedJson,
_ initial: T,
_ fieldsParser: [String: any ParserProtocol<T>],
_ backtrace: ConfigBacktrace,
_ c: inout ConfigParserContext,
) -> T {
switch raw {
case .dict(let table):
return table.parseTable(initial, fieldsParser, backtrace, &c)
default:
c.errors.append(expectedActualTypeDiagnostic(expected: .table, actual: raw.tomlType, backtrace))
return initial
}
}
private func parseStartupRootContainerLayout(_ raw: OrderedJson, _ backtrace: ConfigBacktrace) -> ResOrConfigParseDiagnostic<Void> {
parseString(raw, backtrace)
.filter(.init(backtrace, "'non-empty-workspaces-root-containers-layout-on-startup' is deprecated. Please drop it from your config")) { raw in raw == "smart" }
.map { _ in () }
}
private func parseLayout(_ raw: OrderedJson, _ backtrace: ConfigBacktrace) -> ResOrConfigParseDiagnostic<Layout> {
parseString(raw, backtrace)
.flatMap { $0.parseLayout().toResult(.init(backtrace, "Can't parse layout '\($0)'")) }
}
private func skipParsing<T: Sendable>(_ value: T) -> @Sendable (_ raw: OrderedJson, _ backtrace: ConfigBacktrace) -> ResOrConfigParseDiagnostic<T> {
{ _, _ in .success(value) }
}
private func parsePersistentWorkspaces(_ raw: OrderedJson, _ backtrace: ConfigBacktrace) -> ResOrConfigParseDiagnostic<OrderedSet<String>> {
parseArrayOfStrings(raw, backtrace)
.flatMap { arr in
let set = arr.toOrderedSet()
return set.count == arr.count ? .success(set) : .failure(.init(backtrace, "Contains duplicated workspace names"))
}
}
private func parseArrayOfStrings(_ raw: OrderedJson, _ backtrace: ConfigBacktrace) -> ResOrConfigParseDiagnostic<[String]> {
parseTomlArray(raw, backtrace)
.flatMap { arr in
arr.enumerated().mapAllOrFailure { (index, elem) in
parseString(elem, backtrace + .index(index))
}
}
}
private func parseDefaultContainerOrientation(_ raw: OrderedJson, _ backtrace: ConfigBacktrace) -> ResOrConfigParseDiagnostic<DefaultContainerOrientation> {
parseString(raw, backtrace).flatMap {
DefaultContainerOrientation(rawValue: $0)
.toResult(.init(backtrace, "Can't parse default container orientation '\($0)'"))
}
}
extension ResOrStr where Failure == String {
func toParsedConfig(_ backtrace: ConfigBacktrace) -> ResOrConfigParseDiagnostic<Success> {
mapError { .init(backtrace, $0) }
}
}
func parseBool(_ raw: OrderedJson, _ backtrace: ConfigBacktrace) -> ResOrConfigParseDiagnostic<Bool> {
raw.asBoolOrNil.toResult(expectedActualTypeDiagnostic(expected: .bool, actual: raw.tomlType, backtrace))
}
struct ConfigBacktrace: CustomStringConvertible, Equatable {
private var path: [TomlBacktraceItem] = []
private init(_ path: [TomlBacktraceItem]) {
check(path.first?.isKey != false, "Tried to construct invalid TOML path: \(path)")
self.path = path
}
static func rootKey(_ key: String) -> Self { .init([.key(key)]) }
static let emptyRoot: Self = .init([])
var description: String {
var result = ""
for (i, elem) in path.enumerated() {
switch elem {
case .key(let rootKey) where i == 0: result += rootKey
case .key(let key): result += ".\(key)"
case .index(let index): result += "[\(index)]"
}
}
return result
}
var isRootKey: Bool { path.singleOrNil().map(\.isKey) == true }
static func + (lhs: consuming Self, rhs: TomlBacktraceItem) -> Self {
lhs.path += [rhs]
return lhs
}
}
enum TomlBacktraceItem: Equatable {
case key(String)
case index(Int)
var isKey: Bool {
switch self {
case .key: true
case .index: false
}
}
}
extension OrderedJson.JsonDict {
func parseTable<T: ConvenienceMutable>(
_ initial: T,
_ fieldsParser: [String: any ParserProtocol<T>],
_ backtrace: ConfigBacktrace,
_ c: inout ConfigParserContext,
) -> T {
var raw = initial
for (key, value) in self {
let backtrace: ConfigBacktrace = backtrace + .key(key)
switch fieldsParser[key] {
case let parser?: raw = parser.transformRawConfig(raw, value, backtrace, &c)
case nil: c.errors.append(unknownKeyDiagnostic(backtrace))
}
}
return raw
}
}
func unknownKeyDiagnostic(_ backtrace: ConfigBacktrace) -> ConfigParseDiagnostic {
.init(backtrace, backtrace.isRootKey ? "Unknown top-level key" : "Unknown key")
}
func expectedActualTypeDiagnostic(expected: TomlType, actual: TomlType, _ backtrace: ConfigBacktrace) -> ConfigParseDiagnostic {
.init(backtrace, expectedActualTypeError(expected: expected, actual: actual))
}
func expectedActualTypeDiagnostic(expected: [TomlType], actual: TomlType, _ backtrace: ConfigBacktrace) -> ConfigParseDiagnostic {
.init(backtrace, expectedActualTypeError(expected: expected, actual: actual))
}
```
## /Sources/AppBundle/config/parseExecEnvVariables.swift
```swift path="/Sources/AppBundle/config/parseExecEnvVariables.swift"
import AppKit
import Common
let testEnv = ["PATH": "AEROSPACE_TEST_PATH", "AEROSPACE_INHERITED_TEST_ENV": "inherited"]
private var env: [String: String] {
isUnitTest ? testEnv : ProcessInfo.processInfo.environment
}
private let rawExecConfigParser: [String: any ParserProtocol<RawExecConfig>] = [
"inherit-env-vars": Parser(\.inheritEnvVariables, parseBool),
"env-vars": Parser(\.overriddenVars, parseEnvVariables),
]
let defaultOverriddenEnvVars = ["PATH": "/opt/homebrew/bin:/opt/homebrew/sbin:\(env["PATH"] ?? "")"]
struct ExecConfig: Equatable {
var envVariables: [String: String] = env + defaultOverriddenEnvVars
}
struct RawExecConfig: ConvenienceMutable, Equatable {
var inheritEnvVariables = true
// Already interpolated value of overridden vars
var overriddenVars: [String: String] = [:]
func expand() -> ExecConfig {
let base: [String: String] = inheritEnvVariables ? env : [:]
return ExecConfig(envVariables: base + overriddenVars)
}
}
func parseExecConfig(_ raw: OrderedJson, _ backtrace: ConfigBacktrace, _ c: inout ConfigParserContext) -> ExecConfig {
parseTable(raw, RawExecConfig(), rawExecConfigParser, backtrace, &c).expand()
}
private func parseEnvVariables(_ raw: OrderedJson, _ backtrace: ConfigBacktrace, _ c: inout ConfigParserContext) -> [String: String] {
guard let table = raw.asDictOrNil else {
c.errors.append(expectedActualTypeDiagnostic(expected: .array, actual: raw.tomlType, backtrace))
return [:]
}
let mutated = table.keys
let fullEnv: [String: String] = env
let baseEnv: [String: String] = fullEnv.filter { (key, _) -> Bool in !mutated.contains(key) }
var result: [String: String] = [:]
for (key, value) in table {
let backtrace = backtrace + .key(key)
if key == "PWD" { c.errors.append(.init(backtrace, "Changing 'PWD' is not allowed")) }
guard let rawStr = parseString(value, backtrace).getOrNil(appendErrorTo: &c.errors) else { continue }
var env = baseEnv
if let add: String = fullEnv[key] {
env[key] = add
}
switch rawStr.interpolate(with: env) {
case .success(let interpolated): result[key] = interpolated
case .failure(let _errros): c.errors += _errros.map { .init(backtrace, $0) }
}
}
return result
}
```
## /Sources/AppBundle/config/parseFocusFollowsMouse.swift
```swift path="/Sources/AppBundle/config/parseFocusFollowsMouse.swift"
private let focusFollowsMouseParserTable: [String: any ParserProtocol<FocusFollowsMouse>] = [
"enabled": Parser(\.enabled, parseBool),
]
func parseFocusFollowsMouse(_ rawConfig: OrderedJson, _ backtrace: ConfigBacktrace, _ c: inout ConfigParserContext) -> FocusFollowsMouse {
parseTable(rawConfig, FocusFollowsMouse(), focusFollowsMouseParserTable, backtrace, &c)
}
```
## /Sources/AppBundle/config/parseGaps.swift
```swift path="/Sources/AppBundle/config/parseGaps.swift"
import Common
struct Gaps: ConvenienceMutable, Equatable, Sendable {
var inner: Inner
var outer: Outer
static let zero = Gaps(inner: .zero, outer: .zero)
struct Inner: ConvenienceMutable, Equatable, Sendable {
var vertical: DynamicConfigValue<Int>
var horizontal: DynamicConfigValue<Int>
static let zero = Inner(vertical: 0, horizontal: 0)
init(vertical: Int, horizontal: Int) {
self.vertical = .constant(vertical)
self.horizontal = .constant(horizontal)
}
init(vertical: DynamicConfigValue<Int>, horizontal: DynamicConfigValue<Int>) {
self.vertical = vertical
self.horizontal = horizontal
}
}
struct Outer: ConvenienceMutable, Equatable, Sendable {
var left: DynamicConfigValue<Int>
var bottom: DynamicConfigValue<Int>
var top: DynamicConfigValue<Int>
var right: DynamicConfigValue<Int>
static let zero = Outer(left: 0, bottom: 0, top: 0, right: 0)
init(left: Int, bottom: Int, top: Int, right: Int) {
self.left = .constant(left)
self.bottom = .constant(bottom)
self.top = .constant(top)
self.right = .constant(right)
}
init(left: DynamicConfigValue<Int>, bottom: DynamicConfigValue<Int>, top: DynamicConfigValue<Int>, right: DynamicConfigValue<Int>) {
self.left = left
self.bottom = bottom
self.top = top
self.right = right
}
}
}
struct ResolvedGaps {
let inner: Inner
let outer: Outer
struct Inner {
let vertical: Int
let horizontal: Int
func get(_ orientation: Orientation) -> Int {
orientation == .h ? horizontal : vertical
}
}
struct Outer {
let left: Int
let bottom: Int
let top: Int
let right: Int
}
@MainActor init(gaps: Gaps, monitor: any MonitorInfo) {
inner = .init(
vertical: gaps.inner.vertical.getValue(for: monitor),
horizontal: gaps.inner.horizontal.getValue(for: monitor),
)
outer = .init(
left: gaps.outer.left.getValue(for: monitor),
bottom: gaps.outer.bottom.getValue(for: monitor),
top: gaps.outer.top.getValue(for: monitor),
right: gaps.outer.right.getValue(for: monitor),
)
}
}
private let gapsParser: [String: any ParserProtocol<Gaps>] = [
"inner": Parser(\.inner, parseInner),
"outer": Parser(\.outer, parseOuter),
]
private let innerParser: [String: any ParserProtocol<Gaps.Inner>] = [
"vertical": Parser(\.vertical, parseIntDynamicValue),
"horizontal": Parser(\.horizontal, parseIntDynamicValue),
]
private let outerParser: [String: any ParserProtocol<Gaps.Outer>] = [
"left": Parser(\.left, parseIntDynamicValue),
"bottom": Parser(\.bottom, parseIntDynamicValue),
"top": Parser(\.top, parseIntDynamicValue),
"right": Parser(\.right, parseIntDynamicValue),
]
private func parseIntDynamicValue(_ raw: OrderedJson, _ backtrace: ConfigBacktrace, _ c: inout ConfigParserContext) -> DynamicConfigValue<Int> {
parseDynamicValue(raw, ofType: Int.self, 0, backtrace, &c)
}
func parseGaps(_ raw: OrderedJson, _ backtrace: ConfigBacktrace, _ c: inout ConfigParserContext) -> Gaps {
parseTable(raw, .zero, gapsParser, backtrace, &c)
}
func parseInner(_ raw: OrderedJson, _ backtrace: ConfigBacktrace, _ c: inout ConfigParserContext) -> Gaps.Inner {
parseTable(raw, Gaps.Inner.zero, innerParser, backtrace, &c)
}
func parseOuter(_ raw: OrderedJson, _ backtrace: ConfigBacktrace, _ c: inout ConfigParserContext) -> Gaps.Outer {
parseTable(raw, Gaps.Outer.zero, outerParser, backtrace, &c)
}
```
## /Sources/AppBundle/config/parseKeyMapping.swift
```swift path="/Sources/AppBundle/config/parseKeyMapping.swift"
import Common
import HotKey
private let keyMappingParser: [String: any ParserProtocol<KeyMapping>] = [
"preset": Parser(\.preset, parsePreset),
"key-notation-to-key-code": Parser(\.rawKeyNotationToKeyCode, parseKeyNotationToKeyCode),
]
struct KeyMapping: ConvenienceMutable, Equatable, Sendable {
enum Preset: String, CaseIterable, Sendable {
case qwerty, dvorak, colemak
}
init(
preset: Preset = .qwerty,
rawKeyNotationToKeyCode: [String: Key] = [:],
) {
self.preset = preset
self.rawKeyNotationToKeyCode = rawKeyNotationToKeyCode
}
fileprivate var preset: Preset = .qwerty
fileprivate var rawKeyNotationToKeyCode: [String: Key] = [:]
func resolve() -> [String: Key] {
getKeysPreset(preset) + rawKeyNotationToKeyCode
}
}
func parseKeyMapping(_ raw: OrderedJson, _ backtrace: ConfigBacktrace, _ c: inout ConfigParserContext) -> KeyMapping {
parseTable(raw, KeyMapping(), keyMappingParser, backtrace, &c)
}
private func parsePreset(_ raw: OrderedJson, _ backtrace: ConfigBacktrace) -> ResOrConfigParseDiagnostic<KeyMapping.Preset> {
parseString(raw, backtrace).flatMap { parseEnum($0, KeyMapping.Preset.self).toParsedConfig(backtrace) }
}
private func parseKeyNotationToKeyCode(_ raw: OrderedJson, _ backtrace: ConfigBacktrace, _ c: inout ConfigParserContext) -> [String: Key] {
var result: [String: Key] = [:]
guard let table = raw.asDictOrNil else {
c.errors.append(expectedActualTypeDiagnostic(expected: .table, actual: raw.tomlType, backtrace))
return result
}
for (key, value): (String, OrderedJson) in table {
if isValidKeyNotation(key) {
let backtrace = backtrace + .key(key)
if let value = parseString(value, backtrace).getOrNil(appendErrorTo: &c.errors) {
switch keyNotationToKeyCode[value] {
case let value?: result[key] = value
case nil: c.errors.append(.init(backtrace, "\(value.singleQuoted) is invalid key code"))
}
}
} else {
c.errors.append(.init(backtrace, "\(key.singleQuoted) is invalid key notation"))
}
}
return result
}
private func isValidKeyNotation(_ str: String) -> Bool {
str.rangeOfCharacter(from: .whitespacesAndNewlines) == nil && !str.contains("-")
}
```
## /Sources/AppBundle/config/parseOnWindowDetected.swift
```swift path="/Sources/AppBundle/config/parseOnWindowDetected.swift"
import Common
struct WindowDetectedCallback: ConvenienceMutable, Equatable {
var matcher: WindowDetectedCallbackMatcher = .command(.empty)
var checkFurtherCallbacks: Bool = false
var rawRun: Shell<any Command>? = nil
var run: Shell<any Command> {
rawRun ?? dieT("ID-46D063B2 should have discarded nil")
}
var debugJson: Json {
var result: [String: Json] = [:]
result["matcher"] = switch matcher {
case .command(let command): .string(command.shellOfCommandsDescription)
case .legacy(let legacy): legacy.debugJson
}
if let commands = rawRun {
result["commands"] = .string(commands.shellOfCommandsDescription)
}
return .dict(result)
}
static func == (lhs: WindowDetectedCallback, rhs: WindowDetectedCallback) -> Bool {
lhs.matcher == rhs.matcher && lhs.checkFurtherCallbacks == rhs.checkFurtherCallbacks && lhs.run.strictEquals(rhs.run)
}
}
struct LegacyWindowDetectedCallbackMatcher: ConvenienceMutable, Equatable {
var appId: String?
var appNameRegexSubstring: CaseInsensitiveRegex?
var windowTitleRegexSubstring: CaseInsensitiveRegex?
var workspace: String?
var duringAeroSpaceStartup: Bool?
var debugJson: Json {
var resultParts: [String] = []
if let appId {
resultParts.append("appId=\"\(appId)\"")
}
if let appNameRegexSubstring {
resultParts.append("appNameRegexSubstring=\"\(appNameRegexSubstring.origin)\"")
}
if let windowTitleRegexSubstring {
resultParts.append("windowTitleRegexSubstring=\"\(windowTitleRegexSubstring.origin)\"")
}
if let workspace {
resultParts.append("workspace=\"\(workspace)\"")
}
if let duringAeroSpaceStartup {
resultParts.append("duringAeroSpaceStartup=\(duringAeroSpaceStartup)")
}
return .string(resultParts.joined(separator: ", "))
}
}
enum WindowDetectedCallbackMatcher: Equatable {
case command(Shell<any Command>)
case legacy(LegacyWindowDetectedCallbackMatcher)
static func == (lhs: WindowDetectedCallbackMatcher, rhs: WindowDetectedCallbackMatcher) -> Bool {
switch (lhs, rhs) {
case (.command(let command1), .command(let command2)): command1.strictEquals(command2)
case (.legacy(let matcher1), .legacy(let matcher2)): matcher1 == matcher2
default: false
}
}
}
private let windowDetectedParser: [String: any ParserProtocol<WindowDetectedCallback>] = [
"if": Parser(\.matcher, parseMatcher),
"check-further-callbacks": Parser(\.checkFurtherCallbacks, parseBool),
"run": Parser(\.rawRun, parseShellOfCommandsForConfig),
]
private let matcherParsers: [String: any ParserProtocol<LegacyWindowDetectedCallbackMatcher>] = [
"app-id": Parser(\.appId, upcast(parseString)),
"workspace": Parser(\.workspace, upcast(parseString)),
"app-name-regex-substring": Parser(\.appNameRegexSubstring, upcast(parseCasInsensitiveRegex)),
"window-title-regex-substring": Parser(\.windowTitleRegexSubstring, upcast(parseCasInsensitiveRegex)),
"during-aerospace-startup": Parser(\.duringAeroSpaceStartup, upcast(parseBool)),
]
private func upcast<T>(
_ fun: @escaping @Sendable (OrderedJson, ConfigBacktrace) -> ResOrConfigParseDiagnostic<T>,
) -> @Sendable (OrderedJson, ConfigBacktrace) -> ResOrConfigParseDiagnostic<T?> {
{ fun($0, $1).map(Optional.init) }
}
func parseOnWindowDetectedArray(_ raw: OrderedJson, _ backtrace: ConfigBacktrace, _ c: inout ConfigParserContext) -> [WindowDetectedCallback] {
if let array = raw.asArrayOrNil {
return array.enumerated().map { (index, raw) in parseWindowDetectedCallback(raw, backtrace + .index(index), &c) }.filterNotNil()
} else {
c.errors += [expectedActualTypeDiagnostic(expected: .array, actual: raw.tomlType, backtrace)]
return []
}
}
private func parseCasInsensitiveRegex(_ raw: OrderedJson, _ backtrace: ConfigBacktrace) -> ResOrConfigParseDiagnostic<CaseInsensitiveRegex> {
parseString(raw, backtrace).flatMap { CaseInsensitiveRegex.new($0).toParsedConfig(backtrace) }
}
private func parseMatcher(_ raw: OrderedJson, _ backtrace: ConfigBacktrace, _ c: inout ConfigParserContext) -> WindowDetectedCallbackMatcher {
switch raw {
case .dict(let raw):
return .legacy(raw.parseTable(LegacyWindowDetectedCallbackMatcher(), matcherParsers, backtrace, &c))
case .string(let raw):
return .command(parseCommand(raw, allowExecAndForget: false, allowEval: false).toResult().toParsedConfig(backtrace).getOrNil(appendErrorTo: &c.errors) ?? .empty)
default:
// Intentionally skip Table type from the list of expected types
c.errors.append(.init(backtrace, expectedActualTypeError(expected: .string, actual: raw.tomlType)))
return .command(.empty)
}
}
private func parseWindowDetectedCallback(_ raw: OrderedJson, _ backtrace: ConfigBacktrace, _ c: inout ConfigParserContext) -> WindowDetectedCallback? {
var myContext = ConfigParserContext(configVersion: c.configVersion, errors: [], warnings: [])
let callback = parseTable(raw, WindowDetectedCallback(), windowDetectedParser, backtrace, &myContext)
if callback.matcher == .command(.empty) && !callback.checkFurtherCallbacks {
let msg = "Omitting 'if' is error prone. You can use `if = 'true'` to preserve the previous behavior.\n" +
"But heads up! You may have missed 'check-further-callbacks = true'"
myContext.errors.append(.init(backtrace, msg))
}
if callback.rawRun == nil { // ID-46D063B2
myContext.errors.append(.init(backtrace, "'run' is mandatory key"))
}
if !myContext.errors.isEmpty {
c.errors += myContext.errors
c.warnings += myContext.warnings
return nil
}
return callback
}
```
## /Sources/AppBundle/config/parseWorkspaceToMonitorAssignment.swift
```swift path="/Sources/AppBundle/config/parseWorkspaceToMonitorAssignment.swift"
import Common
func parseWorkspaceToMonitorAssignment(_ raw: OrderedJson, _ backtrace: ConfigBacktrace, _ c: inout ConfigParserContext) -> [String: [MonitorDescription]] {
guard let rawTable = raw.asDictOrNil else {
c.errors += [expectedActualTypeDiagnostic(expected: .table, actual: raw.tomlType, backtrace)]
return [:]
}
var result: [String: [MonitorDescription]] = [:]
for (workspaceName, rawMonitorDescription) in rawTable {
result[workspaceName] = parseMonitorDescriptions(rawMonitorDescription, backtrace + .key(workspaceName), &c)
}
return result
}
func parseMonitorDescriptions(_ raw: OrderedJson, _ backtrace: ConfigBacktrace, _ c: inout ConfigParserContext) -> [MonitorDescription] {
if let array = raw.asArrayOrNil {
return array.enumerated()
.map { (index, rawDesc) in parseMonitorDescription(rawDesc, backtrace + .index(index)).getOrNil(appendErrorTo: &c.errors) }
.filterNotNil()
} else {
return parseMonitorDescription(raw, backtrace).getOrNil(appendErrorTo: &c.errors).asList()
}
}
func parseMonitorDescription(_ raw: OrderedJson, _ backtrace: ConfigBacktrace) -> ResOrConfigParseDiagnostic<MonitorDescription> {
let rawString: String
if let string = raw.asStringOrNil {
rawString = string
} else if let int = raw.asIntOrNil {
rawString = String(int)
} else {
return .failure(expectedActualTypeDiagnostic(expected: [.string, .int], actual: raw.tomlType, backtrace))
}
return parseMonitorDescription(rawString).toParsedConfig(backtrace)
}
```
## /Sources/AppBundle/config/startAtLogin.swift
```swift path="/Sources/AppBundle/config/startAtLogin.swift"
import AppKit
import Common
import ServiceManagement
@MainActor
func syncStartAtLogin() {
cleanupPlistFromPrevVersions()
let service = SMAppService.mainApp
switch true {
case !config.startAtLogin: _ = try? service.unregister()
case isDebug: print("'start-at-login = true' has no effect in debug builds")
default: _ = try? service.register()
}
}
private func cleanupPlistFromPrevVersions() { // todo Drop after a couple of versions
let launchAgentsDir = FileManager.default.homeDirectoryForCurrentUser.appending(component: "Library/LaunchAgents/")
Result { try FileManager.default.createDirectory(at: launchAgentsDir, withIntermediateDirectories: true) }.getOrDie()
let url: URL = launchAgentsDir.appending(path: "bobko.aerospace.plist")
try? FileManager.default.removeItem(at: url)
}
```
## /Sources/AppBundle/focus.swift
```swift path="/Sources/AppBundle/focus.swift"
import AppKit
import Common
enum EffectiveLeaf {
case window(Window)
case emptyWorkspace(Workspace)
}
extension LiveFocus {
var asLeaf: EffectiveLeaf {
if let windowOrNil { .window(windowOrNil) } else { .emptyWorkspace(workspace) }
}
}
/// This object should be only passed around but never memorized
/// Alternative name: ResolvedFocus
struct LiveFocus: AeroAny, Equatable {
let windowOrNil: Window?
var workspace: Workspace
@MainActor fileprivate var frozen: FrozenFocus {
return FrozenFocus(
windowId: windowOrNil?.windowId,
workspaceName: workspace.name,
monitorId_oneBased: workspace.workspaceMonitor.monitorId_oneBased ?? 0,
)
}
}
/// "old", "captured", "frozen in time" Focus
/// It's safe to keep a hard reference to this object.
/// Unlike in LiveFocus, information inside FrozenFocus isn't guaranteed to be self-consistent.
/// window - workspace - monitor relation could change since the moment object was created
private struct FrozenFocus: AeroAny, Equatable, Sendable {
let windowId: UInt32?
let workspaceName: String
// monitorId is not part of the focus. We keep it here only for 'on-focused-monitor-changed' to work
let monitorId_oneBased: Int
@MainActor var live: LiveFocus { // Important: don't access focus.monitorId here. monitorId is not part of the focus. Always prefer workspace
let window: Window? = windowId.flatMap { Window.get(byId: $0) }
let workspace = Workspace.get(byName: workspaceName)
let workspaceFocus = workspace.toLiveFocus()
let windowFocus = window?.toLiveFocusOrNil() ?? workspaceFocus
return workspaceFocus.workspace != windowFocus.workspace
? workspaceFocus // If window and workspace become separated prefer workspace
: windowFocus
}
}
@MainActor private var _focus: FrozenFocus = {
let monitor = mainMonitorInfo
return FrozenFocus(windowId: nil, workspaceName: monitor.activeWorkspace.name, monitorId_oneBased: monitor.monitorId_oneBased ?? 0)
}()
/// Global focus.
/// Commands must be cautious about accessing this property directly. There are legitimate cases.
/// But, in general, commands must firstly check --window-id, --workspace, AEROSPACE_WINDOW_ID env and
/// AEROSPACE_WORKSPACE env before accessing the global focus.
@MainActor var focus: LiveFocus { _focus.live }
@MainActor func setFocus(to newFocus: LiveFocus) -> Bool {
if _focus == newFocus.frozen { return true }
let oldFocus = focus
// Normalize mruWindow when focus away from a workspace
if oldFocus.workspace != newFocus.workspace {
oldFocus.windowOrNil?.markAsMostRecentChild()
}
_focus = newFocus.frozen
let status = newFocus.workspace.workspaceMonitor.setActiveWorkspace(newFocus.workspace)
newFocus.windowOrNil?.markAsMostRecentChild()
return status
}
extension Window {
@MainActor func focusWindow() -> Bool {
if let focus = toLiveFocusOrNil() {
return setFocus(to: focus)
} else {
// todo We should also exit-native-hidden/unminimize[/exit-native-fullscreen?] window if we want to fix ID-B6E178F2
// and retry to focus the window. Otherwise, it's not possible to focus minimized/hidden windows
return false
}
}
@MainActor func toLiveFocusOrNil() -> LiveFocus? { visualWorkspace.map { LiveFocus(windowOrNil: self, workspace: $0) } }
}
extension Workspace {
@MainActor func focusWorkspace() -> Bool { setFocus(to: toLiveFocus()) }
func toLiveFocus() -> LiveFocus {
// todo unfortunately mostRecentWindowRecursive may recursively reach empty rootTilingContainer
// while floating or macos unconventional windows might be presented
if let wd = mostRecentWindowRecursive ?? anyLeafWindowRecursive {
LiveFocus(windowOrNil: wd, workspace: self)
} else {
LiveFocus(windowOrNil: nil, workspace: self) // emptyWorkspace
}
}
}
@MainActor private var _lastKnownFocus: FrozenFocus = _focus
// Used by workspace-back-and-forth
@MainActor var _prevFocusedWorkspaceName: String? = nil {
didSet {
prevFocusedWorkspaceDate = .now
}
}
@MainActor var prevFocusedWorkspaceDate: Date = .distantPast
@MainActor var prevFocusedWorkspace: Workspace? { _prevFocusedWorkspaceName.map { Workspace.get(byName: $0) } }
// Used by focus-back-and-forth
@MainActor private var _prevFocus: FrozenFocus? = nil
@MainActor var prevFocus: LiveFocus? { _prevFocus?.live.takeIf { $0 != focus } }
@MainActor private var onFocusChangedRecursionGuard = false
// Should be called in refreshSession
@MainActor func checkOnFocusChangedCallbacks_nonCancellable() async {
if refreshSessionEvent?.isStartup == true {
return
}
let focus = focus
let frozenFocus = focus.frozen
var hasFocusChanged = false
var hasFocusedWorkspaceChanged = false
var hasFocusedMonitorChanged = false
if frozenFocus != _lastKnownFocus {
_prevFocus = _lastKnownFocus
hasFocusChanged = true
}
if frozenFocus.workspaceName != _lastKnownFocus.workspaceName {
_prevFocusedWorkspaceName = _lastKnownFocus.workspaceName
hasFocusedWorkspaceChanged = true
}
if frozenFocus.monitorId_oneBased != _lastKnownFocus.monitorId_oneBased {
hasFocusedMonitorChanged = true
}
_lastKnownFocus = frozenFocus
if onFocusChangedRecursionGuard { return }
onFocusChangedRecursionGuard = true
defer { onFocusChangedRecursionGuard = false }
if hasFocusChanged {
_ = await onFocusChanged(.defaultEnv, CmdIoImpl.emptyStdinIgnoringOut, focus)
}
if let _prevFocusedWorkspaceName, hasFocusedWorkspaceChanged {
onWorkspaceChanged(_prevFocusedWorkspaceName, frozenFocus.workspaceName, focus)
}
if hasFocusedMonitorChanged {
_ = await onFocusedMonitorChanged(.defaultEnv, CmdIoImpl.emptyStdinIgnoringOut, focus)
}
}
@MainActor func onFocusedMonitorChanged(_ env: CmdEnv, _ io: CmdIo, _ focus: LiveFocus) async -> Int32ExitCode {
broadcastEvent(.focusedMonitorChanged(
workspace: focus.workspace.name,
monitorId_oneBased: focus.workspace.workspaceMonitor.monitorId_oneBased ?? 0,
))
return await config.onFocusedMonitorChanged.run(env.withFocus(focus), io)
}
@MainActor func onFocusChanged(_ env: CmdEnv, _ io: CmdIo, _ focus: LiveFocus) async -> Int32ExitCode {
broadcastEvent(.focusChanged(
windowId: focus.windowOrNil?.windowId,
workspace: focus.workspace.name,
))
return await config.onFocusChanged.run(env.withFocus(focus), io)
}
@MainActor private func onWorkspaceChanged(_ oldWorkspace: String, _ newWorkspace: String, _ focus: LiveFocus) {
broadcastEvent(.workspaceChanged(
workspace: newWorkspace,
prevWorkspace: oldWorkspace,
))
if let exec = config.execOnWorkspaceChange.first {
let process = Process()
process.executableURL = URL(filePath: exec)
process.arguments = Array(config.execOnWorkspaceChange.dropFirst())
var environment = config.execConfig.envVariables
environment[AEROSPACE_FOCUSED_WORKSPACE] = newWorkspace
environment[AEROSPACE_PREV_WORKSPACE] = oldWorkspace
switch focus.asLeaf {
case .emptyWorkspace(let w):
environment[AEROSPACE_WORKSPACE] = w.name
environment[AEROSPACE_WINDOW_ID] = nil
case .window(let w):
environment[AEROSPACE_WORKSPACE] = nil
environment[AEROSPACE_WINDOW_ID] = w.windowId.description
}
process.environment = environment
_ = Result { try process.run() }
}
}
```
## /Sources/AppBundle/focusCache.swift
```swift path="/Sources/AppBundle/focusCache.swift"
@MainActor private var lastKnownNativeFocusedWindowId: UInt32? = nil
/// The data should flow (from nativeFocused to focused) and
/// (from nativeFocused to lastKnownNativeFocusedWindowId)
/// Alternative names: takeFocusFromMacOs, syncFocusFromMacOs
@MainActor func updateFocusCache(_ nativeFocused: Window?) {
if nativeFocused?.parent is MacosPopupWindowsContainer {
return
}
if nativeFocused?.windowId != lastKnownNativeFocusedWindowId {
_ = nativeFocused?.focusWindow()
lastKnownNativeFocusedWindowId = nativeFocused?.windowId
}
nativeFocused?.macAppUnsafe.lastNativeFocusedWindowId = nativeFocused?.windowId
}
```
## /Sources/AppBundle/getNativeFocusedWindow.swift
```swift path="/Sources/AppBundle/getNativeFocusedWindow.swift"
import AppKit
import Common
@MainActor
var appForTests: (any AbstractApp)? = nil
@MainActor
private var focusedApp: (any AbstractApp)? {
get async throws {
if isUnitTest {
return appForTests
} else {
check(appForTests == nil)
return switch NSWorkspace.shared.frontmostApplication {
case let frontmostApplication?: try await MacApp.getOrRegister(frontmostApplication)
case nil: nil
}
}
}
}
@MainActor
func getNativeFocusedWindow(_ cm: CancellationMode) async throws -> Window? {
try await focusedApp?.getFocusedWindow(cm)
}
```
## /Sources/AppBundle/initAppBundle.swift
```swift path="/Sources/AppBundle/initAppBundle.swift"
import AppKit
import Common
import Foundation
@MainActor public func initAppBundle() {
Task.startUnstructured {
initTerminationHandler()
unsafe _isCli = false
initServerArgs()
await waitForAccessibilityPermission_nonCancellable()
if isDebug {
await toggleReleaseServerIfDebug(.off)
interceptTermination(SIGINT)
interceptTermination(SIGKILL)
}
await bootstrapConfig_nonCancellable()
_ = await reloadConfig_nonCancellable()
startUnixSocketServer()
GlobalObserver.initObserver()
Workspace.garbageCollectUnusedWorkspaces() // init workspaces
_ = Workspace.all.first?.focusWorkspace()
await runHeavyCompleteRefreshSession(
.startup,
// It's important for the first initialization to be non cancellable
// to make sure that isStartup propagates to all places
assumeCancellable: false,
layoutWorkspaces: false,
)
try await runLightSession(.startup, .forceRun) {
smartLayoutAtStartup()
_ = await config.afterStartupCommand.run(.defaultEnv, .emptyStdin)
}
}
}
@MainActor private func bootstrapConfig_nonCancellable() async {
let result = await reloadConfig_nonCancellable(forceConfigUrl: defaultConfigUrl)
let msg = """
Can't load default config. Your installation is probably corrupted.
Please don't modify \(defaultConfigUrl.description.singleQuoted)
\(result.stdout)
"""
check(result.isOk, msg)
}
@MainActor
private func smartLayoutAtStartup() {
let workspace = focus.workspace
let root = workspace.rootTilingContainer
switch root.children.count <= 3 {
case true: root.layout = .tiles
case false: root.layout = .accordion
}
}
var isStartup: Bool { refreshSessionEvent?.isStartup == true }
struct ServerArgs: Sendable {
var configLocation: String? = nil
var isReadOnly: Bool = false
}
private let serverHelp = """
USAGE: \(CommandLine.arguments.first ?? "AeroSpace.app/Contents/MacOS/AeroSpace") [<options>]
OPTIONS:
-h, --help Print help
-v, --version Print AeroSpace.app version
--config-path <path> Config path. It will take priority over ~/.aerospace.toml
and ${XDG_CONFIG_HOME}/aerospace/aerospace.toml
--read-only Disable window management.
Useful if you want to use only debug-windows or other query commands.
"""
nonisolated(unsafe) private var _serverArgs = ServerArgs()
var serverArgs: ServerArgs { unsafe _serverArgs }
private func initServerArgs() {
let args = CommandLine.arguments.slice(1...) ?? []
if args.contains(where: { $0 == "-h" || $0 == "--help" }) {
exit(EXIT_CODE_ZERO, out: serverHelp)
}
var index = 0
while index < args.count {
let current = args[index]
index += 1
switch current {
case "--version", "-v":
exit(EXIT_CODE_ZERO, out: "\(aeroSpaceAppVersion) \(gitHash)")
case "--config-path":
switch args.getOrNil(atIndex: index) {
case let arg?: unsafe _serverArgs.configLocation = arg
case nil: exit(EXIT_CODE_TWO, err: "Missing <path> in --config-path flag")
}
index += 1
case "--read-only": // todo rename to '--disabled' and unite with disabled feature
unsafe _serverArgs.isReadOnly = true
case "-NSDocumentRevisionsDebugMode" where isDebug:
// Skip Xcode CLI args.
// Usually it's '-NSDocumentRevisionsDebugMode NO'/'-NSDocumentRevisionsDebugMode YES'
while args.getOrNil(atIndex: index)?.starts(with: "-") == false { index += 1 }
default:
exit(EXIT_CODE_TWO, err: "Unrecognized flag \(args.first.orDie().singleQuoted)")
}
}
if let path = serverArgs.configLocation, !FileManager.default.fileExists(atPath: path) {
exit(EXIT_CODE_TWO, err: "\(path) doesn't exist")
}
}
```
## /Sources/AppBundle/model/Json.swift
```swift path="/Sources/AppBundle/model/Json.swift"
import AppKit
import Common
enum Json: Encodable, Equatable { // todo rename to Dto? (data transfer object)
// vector
case dict(JsonDict)
case array(JsonArray)
// scalar
case null
case string(String)
case int(Int64)
case bool(Bool)
typealias JsonDict = [String: Json]
typealias JsonArray = [Json]
func encode(to encoder: any Encoder) throws {
switch self {
case .array(let value): try value.encode(to: encoder)
case .dict(let value): try value.encode(to: encoder)
case .string(let value): try value.encode(to: encoder)
case .int(let value): try value.encode(to: encoder)
case .bool(let value): try value.encode(to: encoder)
case .null: try (nil as String?).encode(to: encoder)
}
}
static func newOrDieRecursive(_ value: Any?) -> Json {
switch value {
case let value as [String: Any?]: .dict(value.mapValues(newOrDieRecursive))
case let value as [Any?]: .array(value.map(newOrDieRecursive))
default:
newScalarOrNil(value)
?? dieT("Can't parse \(String(describing: value)) (\(Swift.type(of: value))) to JSON")
}
}
static func newScalarOrNil(_ value: Any?) -> Json? {
switch value {
case let value as Int64: .int(value)
case let value as Int: .int(Int64(value))
case let value as UInt32: .int(Int64(value))
case let value as UInt: .int(Int64(value))
case let value as Bool: .bool(value)
case let value as String: .string(value)
case nil, is NSNull: .null
default: nil
}
}
static func stringOrNull(_ str: String?) -> Json { str.map(Json.string) ?? .null }
static func int(_ int: Int) -> Json { .int(Int64(exactly: int).orDie()) }
static func int(_ int: UInt32) -> Json { .int(Int64(exactly: int).orDie()) }
var rawValue: Any? {
switch self {
case .null: nil
case .array(let x): x
case .dict(let x): x
case .bool(let x): x
case .int(let x): x
case .string(let x): x
}
}
var asDictOrDie: [String: Json] { asDictOrNil.orDie("\(self) is not a dict") }
var asInt64OrNil: Int64? {
if case .int(let value) = self { value } else { nil }
}
var asDictOrNil: JsonDict? {
if case .dict(let value) = self { value } else { nil }
}
}
```
## /Sources/AppBundle/model/KnownBundleId.swift
```swift path="/Sources/AppBundle/model/KnownBundleId.swift"
enum KnownBundleId: String, Equatable {
case _1password = "com.1password.1password"
case activityMonitor = "com.apple.ActivityMonitor"
case alacritty = "org.alacritty"
case braveBrowser = "com.brave.Browser"
case chrome = "com.google.Chrome"
case cleanshotx = "pl.maketheweb.cleanshotx"
case codex = "com.openai.codex"
case emacs = "org.gnu.Emacs"
case finder = "com.apple.finder"
case ghostty = "com.mitchellh.ghostty"
case gimp = "org.gimp.gimp-2.10"
case iphonesimulator = "com.apple.iphonesimulator"
case iterm2 = "com.googlecode.iterm2"
case kitty = "net.kovidgoyal.kitty"
case outlook = "com.microsoft.Outlook"
case photoBooth = "com.apple.PhotoBooth"
case qutebrowser = "org.qutebrowser.qutebrowser"
case screenstudio = "com.timpler.screenstudio"
case slack = "com.tinyspeck.slackmacgap"
case steam = "com.valvesoftware.steam.helper"
case wezterm = "com.github.wez.wezterm"
case wisprFlow = "com.electron.wispr-flow"
case xcode = "com.apple.dt.Xcode"
case zenBrowser = "app.zen-browser.zen"
case zoom = "us.zoom.xos"
case mozillaFirefox = "org.mozilla.firefox"
case mozillaFirefoxDeveloperEdition = "org.mozilla.firefoxdeveloperedition"
case mozillaFirefoxNightly = "org.mozilla.nightly"
case vscode = "com.microsoft.VSCode"
case vscodium = "com.vscodium"
var isFirefox: Bool {
self == .mozillaFirefox
|| self == .mozillaFirefoxDeveloperEdition
|| self == .mozillaFirefoxNightly
|| self == .zenBrowser
}
var isVscode: Bool {
self == .vscode || self == .vscodium
}
}
```
## /Sources/AppBundle/model/MonitorDescriptionEx.swift
```swift path="/Sources/AppBundle/model/MonitorDescriptionEx.swift"
import Common
extension MonitorDescription {
@MainActor func resolveMonitor(sortedMonitors: [MonitorInfo]) -> MonitorInfo? {
switch self {
case .sequenceNumber(let number): sortedMonitors.getOrNil(atIndex: number - 1)
case .main: mainMonitorInfo
case .pattern(let regex): sortedMonitors.first { $0.name.contains(caseInsensitiveRegex: regex) }
case .secondary:
sortedMonitors.takeIf { $0.count == 2 }?
.first { $0.rect.topLeftCorner != mainMonitorInfo.rect.topLeftCorner }
}
}
}
```
## /Sources/AppBundle/model/MonitorEx.swift
```swift path="/Sources/AppBundle/model/MonitorEx.swift"
extension MonitorInfo {
@MainActor
var visibleRectPaddedByOuterGaps: Rect {
let topLeft = visibleRect.topLeftCorner
let gaps = ResolvedGaps(gaps: config.gaps, monitor: self)
return Rect(
topLeftX: topLeft.x + gaps.outer.left.toDouble(),
topLeftY: topLeft.y + gaps.outer.top.toDouble(),
width: visibleRect.width - gaps.outer.left.toDouble() - gaps.outer.right.toDouble(),
height: visibleRect.height - gaps.outer.top.toDouble() - gaps.outer.bottom.toDouble(),
)
}
var monitorId_oneBased: Int? {
let sorted = sortedMonitorInfos
let origin = self.rect.topLeftCorner
return sorted.firstIndex { $0.rect.topLeftCorner == origin }.map { $0 + 1 }
}
}
```
## /Sources/AppBundle/mouse/mouse.swift
```swift path="/Sources/AppBundle/mouse/mouse.swift"
import AppKit
@MainActor var currentlyManipulatedWithMouseWindowId: UInt32? = nil
var isLeftMouseButtonDown: Bool { NSEvent.pressedMouseButtons == 1 }
@MainActor
func isManipulatedWithMouse(_ window: Window) async throws -> Bool {
try await (!window.isHiddenInCorner && // Don't allow to resize/move windows of hidden workspaces
isLeftMouseButtonDown &&
(currentlyManipulatedWithMouseWindowId == nil || window.windowId == currentlyManipulatedWithMouseWindowId))
.andAsync { @Sendable @MainActor in try await getNativeFocusedWindow(.cancellable) == window }
}
/// Same motivation as in monitorFrameNormalized
var mouseLocation: CGPoint { NSEvent.mouseLocation.withYAxisFlipped }
```
The content has been capped at 50000 tokens. The user could consider applying other filters to refine the result. The better and more specific the context, the better the LLM can follow instructions. If the context seems verbose, the user can refine the filter using uithub. Thank you for using https://uithub.com - Perfect LLM context for any GitHub repo.