```
├── .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)
├── .gitignore (100 tokens)
├── .idea/
├── codeStyles/
├── Project.xml (100 tokens)
├── codeStyleConfig.xml
├── inspectionProfiles/
├── Project_Default.xml (100 tokens)
├── .swift-version
├── .swiftformat (400 tokens)
├── .swiftlint.yml (500 tokens)
├── AeroSpace.xcodeproj/
├── project.pbxproj (2.6k tokens)
├── project.xcworkspace/
├── contents.xcworkspacedata
├── CONTRIBUTING.md (1200 tokens)
├── Gemfile
├── LICENSE.txt (200 tokens)
├── Package.resolved (300 tokens)
├── Package.swift (600 tokens)
├── README.md (2.2k tokens)
├── ShellParserGenerated/
├── .gitignore
├── Package.swift (200 tokens)
├── Sources/
├── ShellParserGenerated/
├── ShellLexer.swift (2.3k tokens)
├── ShellParser.swift (6.8k tokens)
├── Sources/
├── AeroSpaceApp/
├── AeroSpaceApp.swift (100 tokens)
├── AppBundle/
├── GlobalObserver.swift (800 tokens)
├── command/
├── CmdEnv.swift (200 tokens)
├── CmdIo.swift (200 tokens)
├── Command.swift (400 tokens)
├── cmdManifest.swift (900 tokens)
├── cmdResolveTargetOrReportError.swift (300 tokens)
├── format.swift (1600 tokens)
├── formatToJson.swift (200 tokens)
├── impl/
├── BalanceSizesCommand.swift (100 tokens)
├── CloseAllWindowsButCurrentCommand.swift (200 tokens)
├── CloseCommand.swift (300 tokens)
├── ConfigCommand.swift (1400 tokens)
├── DebugWindowsCommand.swift (1100 tokens)
├── EnableCommand.swift (200 tokens)
├── ExecAndForgetCommand.swift (100 tokens)
├── FlattenWorkspaceTreeCommand.swift (100 tokens)
├── FocusBackAndForthCommand.swift (100 tokens)
├── FocusCommand.swift (1700 tokens)
├── FocusMonitorCommand.swift (700 tokens)
├── FullscreenCommand.swift (200 tokens)
├── JoinWithCommand.swift (300 tokens)
├── LayoutCommand.swift (1000 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 (400 tokens)
├── MacosNativeMinimizeCommand.swift (200 tokens)
├── ModeCommand.swift (100 tokens)
├── MoveCommand.swift (1500 tokens)
├── MoveMouseCommand.swift (500 tokens)
├── MoveNodeToMonitorCommand.swift (300 tokens)
├── MoveNodeToWorkspaceCommand.swift (400 tokens)
├── MoveWorkspaceToMonitorCommand.swift (300 tokens)
├── ReloadConfigCommand.swift (300 tokens)
├── ResizeCommand.swift (500 tokens)
├── SplitCommand.swift (400 tokens)
├── SummonWorkspaceCommand.swift (200 tokens)
├── SwapCommand.swift (400 tokens)
├── TriggerBindingCommand.swift (200 tokens)
├── VolumeCommand.swift (200 tokens)
├── WorkspaceBackAndForthCommand.swift (100 tokens)
├── WorkspaceCommand.swift (500 tokens)
├── parseCommand.swift (200 tokens)
├── config/
├── Config.swift (500 tokens)
├── ConfigFile.swift (300 tokens)
├── DynamicConfigValue.swift (700 tokens)
├── HotkeyBinding.swift (900 tokens)
├── Mode.swift (300 tokens)
├── keysMap.swift (1900 tokens)
├── parseConfig.swift (3.3k tokens)
├── parseExecEnvVariables.swift (500 tokens)
├── parseGaps.swift (800 tokens)
├── parseKeyMapping.swift (500 tokens)
├── parseOnWindowDetected.swift (1200 tokens)
├── parseWorkspaceToMonitorAssignment.swift (300 tokens)
├── startAtLogin.swift (200 tokens)
├── focus.swift (1500 tokens)
├── focusCache.swift (100 tokens)
├── getNativeFocusedWindow.swift (100 tokens)
├── initAppBundle.swift (700 tokens)
├── layout/
├── layoutRecursive.swift (1600 tokens)
├── refresh.swift (1400 tokens)
├── model/
├── Json.swift (400 tokens)
├── KnownBundleId.swift (200 tokens)
├── Monitor.swift (700 tokens)
├── MonitorDescriptionEx.swift (100 tokens)
├── MonitorEx.swift (200 tokens)
├── Rect.swift (400 tokens)
├── mouse/
├── mouse.swift (200 tokens)
├── moveWithMouse.swift (900 tokens)
├── resizeWithMouse.swift (1000 tokens)
├── normalizeLayoutReason.swift (700 tokens)
├── runLoop.swift (600 tokens)
├── server.swift (1000 tokens)
├── shell/
├── Shell.swift (2.2k tokens)
├── tree/
├── AbstractApp.swift (100 tokens)
├── MacApp.swift (3.4k tokens)
├── MacWindow.swift (2.6k tokens)
├── MacosUnconventionalWindowsContainer.swift (300 tokens)
├── TilingContainer.swift (500 tokens)
├── TreeNode.swift (1300 tokens)
├── TreeNodeCases.swift (1300 tokens)
├── TreeNodeEx.swift (800 tokens)
├── Window.swift (600 tokens)
├── Workspace.swift (1600 tokens)
├── WorkspaceEx.swift (400 tokens)
├── frozen/
├── FrozenTreeNode.swift (300 tokens)
├── FrozenWorld.swift (200 tokens)
├── closedWindowsCache.swift (1100 tokens)
├── normalizeContainers.swift (300 tokens)
├── ui/
├── AppearanceTheme.swift (100 tokens)
├── ExperimentalUISettings.swift (400 tokens)
├── MenuBar.swift (900 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/
├── ArrayEx.swift (200 tokens)
├── AxSubscription.swift (400 tokens)
├── AxUiElementMock.swift (100 tokens)
├── AxUiElementMockEx.swift (1600 tokens)
├── LazySequenceProtocolEx.swift
├── MruStack.swift (300 tokens)
├── NSRunningApplicationEx.swift
├── NsApplicationEx.swift (100 tokens)
├── SetEx.swift (100 tokens)
├── ThreadGuardedValue.swift (200 tokens)
├── accessibility.swift (2.4k tokens)
├── appBundleUtil.swift (1100 tokens)
├── axTrustedCheckOptionPrompt.swift
├── dumpAxRecursive.swift (800 tokens)
├── AppBundleTests/
├── AxWindowKindTest.swift (600 tokens)
├── assert.swift (600 tokens)
├── command/
├── BalanceSizesCommandTest.swift (200 tokens)
├── CloseCommandTest.swift (300 tokens)
├── ExecCommandTest.swift (100 tokens)
├── FlattenWorkspaceTreeCommandTest.swift (200 tokens)
├── FocusCommandTest.swift (2k tokens)
├── JoinWithCommandTest.swift (200 tokens)
├── ListAppsTest.swift (100 tokens)
├── ListModesTest.swift (600 tokens)
├── ListMonitorsTest.swift (100 tokens)
├── ListWindowsTest.swift (900 tokens)
├── ListWorkspacesTest.swift (300 tokens)
├── MoveCommandTest.swift (2.4k tokens)
├── MoveNodeToMonitorCommandTest.swift (200 tokens)
├── MoveNodeToWorkspaceCommandTest.swift (700 tokens)
├── ResizeCommandTest.swift (300 tokens)
├── SplitCommandTest.swift (500 tokens)
├── SummonWorkspaceCommandTest.swift (100 tokens)
├── SwapCommandTest.swift (1200 tokens)
├── WorkspaceCommandTest.swift (300 tokens)
├── config/
├── ConfigTest.swift (3.2k tokens)
├── ParseEnvVariablesTest.swift (500 tokens)
├── SplitArgsTest.swift (200 tokens)
├── model/
├── ClientServerTest.swift (200 tokens)
├── shell/
├── ShellTest.swift (1000 tokens)
├── testExtensions.swift
├── testUtil.swift (700 tokens)
├── tree/
├── TestApp.swift (200 tokens)
├── TestWindow.swift (300 tokens)
├── TilingContainer.swift (100 tokens)
├── TreeNodeTest.swift (700 tokens)
├── Cli/
├── _main.swift (900 tokens)
├── cliUtil.swift
├── subcommandDescriptionsGenerated.swift (600 tokens)
├── Common/
├── appMetadata.swift (100 tokens)
├── cmdArgs/
├── ArgParser.swift (600 tokens)
├── ArgParserInput.swift (100 tokens)
├── SubArgParser.swift (700 tokens)
├── cmdArgsManifest.swift (1200 tokens)
├── impl/
├── BalanceSizesCmdArgs.swift (100 tokens)
├── CloseAllWindowsButCurrentCmdArgs.swift (200 tokens)
├── CloseCmdArgs.swift (100 tokens)
├── ConfigCmdArgs.swift (500 tokens)
├── DebugWindowsCmdArgs.swift (100 tokens)
├── EnableCmdArgs.swift (300 tokens)
├── ExecAndForgetCmdArgs.swift (100 tokens)
├── FlattenWorkspaceTreeCmdArgs.swift (100 tokens)
├── FocusBackAndForthCmdArgs.swift (100 tokens)
├── FocusCmdArgs.swift (1000 tokens)
├── FocusMonitorCmdArgs.swift (500 tokens)
├── FullscreenCmdArgs.swift (200 tokens)
├── JoinWithCmdArgs.swift (200 tokens)
├── LayoutCmdArgs.swift (500 tokens)
├── ListAppsCmdArgs.swift (500 tokens)
├── ListExecEnvVarsCmdArgs.swift (100 tokens)
├── ListModesCmdArgs.swift (200 tokens)
├── ListMonitorsCmdArgs.swift (300 tokens)
├── ListWindowsCmdArgs.swift (1600 tokens)
├── ListWorkspacesCmdArgs.swift (900 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 (500 tokens)
├── MoveWorkpsaceToMonitorCmdArgs.swift (200 tokens)
├── ReloadConfigCmdArgs.swift (100 tokens)
├── ResizeCmdArgs.swift (400 tokens)
├── SplitCmdArgs.swift (200 tokens)
├── SummonWorkspaceCmdArgs.swift (200 tokens)
├── SwapCmdArgs.swift (200 tokens)
├── TriggerBindingCmdArgs.swift (200 tokens)
├── VolumeCmdArgs.swift (400 tokens)
├── WorkspaceBackAndForthCmdArgs.swift (100 tokens)
├── WorkspaceCmdArgs.swift (600 tokens)
├── parseCmdArgs.swift (500 tokens)
├── parseSpecificCmdArgs.swift (900 tokens)
├── splitArgs.swift (600 tokens)
├── subcommandParsers.swift (100 tokens)
├── cmdHelpGenerated.swift (1500 tokens)
├── gitHashGenerated.swift
├── model/
├── AxAppThreadToken.swift (100 tokens)
├── CardinalDirection.swift (100 tokens)
├── CardinalOrDfsDirection.swift (200 tokens)
├── DfsNextPrev.swift
├── Init.swift (100 tokens)
├── MonitorDescription.swift (400 tokens)
├── NextPrev.swift
├── Orientation.swift (100 tokens)
├── WorkspaceName.swift (300 tokens)
├── clientServer.swift (300 tokens)
├── sponsorshipPrompts.swift (100 tokens)
├── util/
├── AeroAny.swift (200 tokens)
├── ArrSlice.swift (700 tokens)
├── BoolEx.swift (100 tokens)
├── CollectionEx.swift (100 tokens)
├── ComparableEx.swift
├── ConvenienceCopyable.swift
├── EquatableNoop.swift (100 tokens)
├── JsonEncoderEx.swift (100 tokens)
├── Lateinit.swift (100 tokens)
├── OptionalEx.swift (400 tokens)
├── ResultEx.swift (300 tokens)
├── SequenceEx.swift (700 tokens)
├── StringEx.swift (1300 tokens)
├── StringLogicalSegments.swift (300 tokens)
├── commonUtil.swift (1300 tokens)
├── showMessageInGui.swift (200 tokens)
├── versionGenerated.swift
├── PrivateApi/
├── include/
├── module.modulemap
├── private.h (200 tokens)
├── private.m
├── axDumps/
├── about_this_mac.json5 (900 tokens)
├── alacritty_decorations_buttonless.json5 (900 tokens)
├── apple_calendar.json5 (1100 tokens)
├── apple_calendar_settings.json5 (1000 tokens)
├── apple_followup_sign_in_to_a_new_device_confirmation.json5 (700 tokens)
├── apple_mail.json5 (1500 tokens)
├── apple_mail_new_email.json5 (1100 tokens)
├── apple_mail_settings.json5 (1000 tokens)
├── archiveutility.json5 (1000 tokens)
├── calculator.json5 (1000 tokens)
├── choose_1_5_0.json5 (700 tokens)
├── chrome.json5 (1100 tokens)
├── chrome_choose_what_to_share_popup.json5 (1000 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)
├── drracket.json5 (1200 tokens)
├── finder.json5 (1300 tokens)
├── finder_quick_look.json5 (900 tokens)
├── firefox.json5 (1000 tokens)
├── firefox_extensions_popup.json5 (700 tokens)
├── firefox_mouse_hover_extensions.json5 (600 tokens)
├── firefox_mouse_hover_tab.json5 (600 tokens)
├── firefox_non_native_fullscreen.json5 (600 tokens)
├── firefox_normal_window_when_non_native_fullscreen_in_background.json5 (1000 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.json5 (700 tokens)
├── ghostty_config_error.json5 (1000 tokens)
├── ghostty_window_decorations_false.json5 (600 tokens)
├── intellij.json5 (1400 tokens)
├── intellij_background_tasks.json5 (700 tokens)
├── intellij_context_menu.json5 (700 tokens)
├── intellij_native_open_window.json5 (600 tokens)
├── intellij_quick_doc_popup.json5 (700 tokens)
├── intellij_rebase_dialog.json5 (1000 tokens)
├── iphonesimulator.json5 (1200 tokens)
├── jetbrains_toolbox.json5 (600 tokens)
├── karabiner_event_viewer.json5 (1100 tokens)
├── karabiner_settings.json5 (1100 tokens)
├── macos_capslock_popup_safari.json5 (600 tokens)
├── macos_capslock_popup_textedit.json5 (500 tokens)
├── macos_share_window_purple_pill_sublime.json5 (600 tokens)
├── marta.json5 (1100 tokens)
├── mpv_fullscreen.json5 (600 tokens)
├── mpv_windowed.json5 (1300 tokens)
├── nomachine_session_1.json5 (600 tokens)
├── nomachine_session_2.json5 (1000 tokens)
├── nomachine_welcome_window_1.json5 (600 tokens)
├── nomachine_welcome_window_2.json5 (1000 tokens)
├── qutebrowser.json5 (1200 tokens)
├── qutebrowser_context_menu.json5 (600 tokens)
├── qutebrowser_hide_decoration.json5 (600 tokens)
├── raycast.json5 (500 tokens)
├── safari.json5 (1100 tokens)
├── safari_pinterest_sign_in_with_google.json5 (1300 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 (1200 tokens)
├── slack_huddle_share_screen_floating_popup.json5 (1100 tokens)
├── spotify.json5 (1000 tokens)
├── steam_1.json5 (700 tokens)
├── steam_2.json5 (700 tokens)
├── sublime_text_4.json5 (1000 tokens)
├── system_settings.json5 (1000 tokens)
├── telegram.json5 (1100 tokens)
├── telegram_image_viewer.json5 (600 tokens)
├── terminal_app.json5 (1100 tokens)
├── transmission.json5 (1100 tokens)
├── transmission_torrent_inspector.json5 (1000 tokens)
├── vlc_empty.json5 (900 tokens)
├── vlc_fullscreen.json5 (500 tokens)
├── vlc_video_playing.json5 (1200 tokens)
├── vs_code.json5 (1000 tokens)
├── vs_code_nativeFullScreen_false.json5 (1000 tokens)
├── vs_codium.json5 (1100 tokens)
├── vs_codium_nativeFullScreen_false.json5 (900 tokens)
├── xcode.json5 (1200 tokens)
├── xcode_build_succeeded_popup.json5 (600 tokens)
├── xcode_installing_system_components.json5 (500 tokens)
├── zed.json5 (1100 tokens)
├── build-debug.sh (100 tokens)
├── build-docs.sh (200 tokens)
├── build-release.sh (900 tokens)
├── build-shell-completion.sh (200 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-enable.adoc (200 tokens)
├── aerospace-exec-and-forget.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 (600 tokens)
├── aerospace-fullscreen.adoc (300 tokens)
├── aerospace-join-with.adoc (300 tokens)
├── aerospace-layout.adoc (400 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 (200 tokens)
├── aerospace-resize.adoc (300 tokens)
├── aerospace-split.adoc (300 tokens)
├── aerospace-summon-workspace.adoc (300 tokens)
├── aerospace-swap.adoc (300 tokens)
├── aerospace-trigger-binding.adoc (300 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 (1400 tokens)
├── config-examples/
├── default-config.toml (1600 tokens)
├── i3-like-config-example.toml (600 tokens)
├── goodies.adoc (1700 tokens)
├── guide.adoc (5.6k 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-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-shell-parser.sh (200 tokens)
├── generate.sh (500 tokens)
├── grammar/
├── ShellLexer.g4 (300 tokens)
├── ShellParser.g4 (500 tokens)
├── commands-bnf-grammar.txt (1300 tokens)
├── install-from-sources.sh (200 tokens)
├── legal/
├── LICENSE.txt
├── README.md (400 tokens)
├── third-party-license/
├── LICENSE-BlueSocket.txt (2k tokens)
├── LICENSE-HotKey.txt (200 tokens)
├── LICENSE-ISSoundAdditions.txt (200 tokens)
├── LICENSE-TOMLKIT.txt (200 tokens)
├── LICENSE-antlr.txt (300 tokens)
├── LICENSE-swift-collections.txt (2.4k tokens)
├── LICENSE-tomlplusplus.txt (200 tokens)
├── makefile
├── project.yml (400 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
├── run-swift-test.sh
├── run-tests.sh (100 tokens)
├── script/
├── build-brew-cask.sh (500 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)
```
## /.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"
#!/bin/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.
- [ ] The GitHub Actions CI must pass (you can fix failures after submitting a PR).
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@v3
- run: ./.github/gh-actions-runner-xcode-select.sh
- run: brew install swiftly
- run: swiftly init --skip-install --assume-yes --verbose && swiftly install
- run: ./build-debug.sh
- run: ./run-tests.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@v3
- 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
- name: './build-release.sh'
run: |
# "-" means "Sign to run locally". There is no aerospace-codesign-certificate on GH Actions
./build-release.sh --codesign-identity -
./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 || close
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
ISSUE: ${{ github.event.issue.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-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.2.0
```
## /.swiftformat
```swiftformat path="/.swiftformat"
# https://github.com/nicklockwood/SwiftFormat/blob/main/Rules.md
--exclude ./ShellParserGenerated
--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 modifierOrder
--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-build
- ./ShellParserGenerated
# 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
- modifier_order
- 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_optional_binding # Prefer != nil over let _ =
- unused_setter_value
- weak_delegate # Delegates should be weak to avoid reference cycles
switch_case_alignment:
indented_cases: true
strict: true
# quiet: true
```
## /AeroSpace.xcodeproj/project.pbxproj
```pbxproj path="/AeroSpace.xcodeproj/project.pbxproj"
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 77;
objects = {
/* Begin PBXBuildFile section */
238EF26CAAADD1FE11312D7C /* default-config.toml in Resources */ = {isa = PBXBuildFile; fileRef = 8FE45A887100EB70912B07F0 /* default-config.toml */; };
852F88894A3B9FC385563665 /* AppBundle in Frameworks */ = {isa = PBXBuildFile; productRef = 018E55979F61DA6DA6DCB442 /* AppBundle */; };
883A44C8295FECF49F94269D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 84C35D8E25B61D4D1ADB1851 /* Assets.xcassets */; };
C40E0D9C06086C58955237D9 /* AeroSpaceApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18C104E927E079E60C91AE3E /* AeroSpaceApp.swift */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
09685297933511208058F7CF /* AeroSpace.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AeroSpace.app; sourceTree = BUILT_PRODUCTS_DIR; };
18C104E927E079E60C91AE3E /* AeroSpaceApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AeroSpaceApp.swift; sourceTree = "<group>"; };
6606D24B4B23E9582CFA3B86 /* AeroSpace */ = {isa = PBXFileReference; lastKnownFileType = folder; name = AeroSpace; path = .; sourceTree = SOURCE_ROOT; };
84C35D8E25B61D4D1ADB1851 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
8FE45A887100EB70912B07F0 /* default-config.toml */ = {isa = PBXFileReference; path = "default-config.toml"; sourceTree = "<group>"; };
CF85755BFF66B59A84F98262 /* AeroSpace.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = AeroSpace.entitlements; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
2AFAB0BC1A2742132D7CB950 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
852F88894A3B9FC385563665 /* AppBundle in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
0E0109AE5F7881520B0D2384 /* config-examples */ = {
isa = PBXGroup;
children = (
8FE45A887100EB70912B07F0 /* default-config.toml */,
);
name = "config-examples";
path = "docs/config-examples";
sourceTree = "<group>";
};
21E15F84087042E63C0150AB /* resources */ = {
isa = PBXGroup;
children = (
CF85755BFF66B59A84F98262 /* AeroSpace.entitlements */,
84C35D8E25B61D4D1ADB1851 /* Assets.xcassets */,
);
path = resources;
sourceTree = "<group>";
};
393942C56466FDBBE35F9EC0 = {
isa = PBXGroup;
children = (
6F6BCFA26BF3E35072EF2C77 /* AeroSpaceApp */,
0E0109AE5F7881520B0D2384 /* config-examples */,
3A1FF786C84025133F96138D /* Packages */,
21E15F84087042E63C0150AB /* resources */,
62BEA6F49E6648E2EE3C208F /* Products */,
);
sourceTree = "<group>";
};
3A1FF786C84025133F96138D /* Packages */ = {
isa = PBXGroup;
children = (
6606D24B4B23E9582CFA3B86 /* AeroSpace */,
);
name = Packages;
sourceTree = "<group>";
};
62BEA6F49E6648E2EE3C208F /* Products */ = {
isa = PBXGroup;
children = (
09685297933511208058F7CF /* AeroSpace.app */,
);
name = Products;
sourceTree = "<group>";
};
6F6BCFA26BF3E35072EF2C77 /* AeroSpaceApp */ = {
isa = PBXGroup;
children = (
18C104E927E079E60C91AE3E /* AeroSpaceApp.swift */,
);
name = AeroSpaceApp;
path = Sources/AeroSpaceApp;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
B00BE37A79171B0EE995EB83 /* AeroSpace */ = {
isa = PBXNativeTarget;
buildConfigurationList = 1C34EA41A1F045E016D1944D /* Build configuration list for PBXNativeTarget "AeroSpace" */;
buildPhases = (
D7A18303C03F2CB26F7BB54B /* Sources */,
BA5F2F9022B8385637D263E4 /* Resources */,
2AFAB0BC1A2742132D7CB950 /* Frameworks */,
);
buildRules = (
);
dependencies = (
);
name = AeroSpace;
packageProductDependencies = (
018E55979F61DA6DA6DCB442 /* AppBundle */,
);
productName = AeroSpace;
productReference = 09685297933511208058F7CF /* AeroSpace.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
0B585B3093DA0FC12E7983E2 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1430;
};
buildConfigurationList = D6982B0C3E92C5AF28BCD315 /* Build configuration list for PBXProject "AeroSpace" */;
compatibilityVersion = "Xcode 14.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
Base,
en,
);
mainGroup = 393942C56466FDBBE35F9EC0;
minimizedProjectReferenceProxies = 1;
packageReferences = (
9A00429279948F2879C9FE30 /* XCLocalSwiftPackageReference "." */,
);
preferredProjectObjectVersion = 77;
projectDirPath = "";
projectRoot = "";
targets = (
B00BE37A79171B0EE995EB83 /* AeroSpace */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
BA5F2F9022B8385637D263E4 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
883A44C8295FECF49F94269D /* Assets.xcassets in Resources */,
238EF26CAAADD1FE11312D7C /* default-config.toml in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
D7A18303C03F2CB26F7BB54B /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
C40E0D9C06086C58955237D9 /* AeroSpaceApp.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
175127AAF914899705FABF12 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"$(inherited)",
"DEBUG=1",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = macosx;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
};
name = Debug;
};
31B702864571F51814E4F12C /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = resources/AeroSpace.entitlements;
CODE_SIGN_IDENTITY = "aerospace-codesign-certificate";
COMBINE_HIDPI_IMAGES = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_LSUIElement = YES;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = "0.0.0-SNAPSHOT";
PRODUCT_BUNDLE_IDENTIFIER = bobko.aerospace.debug;
PRODUCT_NAME = "AeroSpace-Debug";
SDKROOT = macosx;
SWIFT_VERSION = 6.2;
};
name = Debug;
};
A991F90908318BCD1655E904 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
SWIFT_VERSION = 5.0;
};
name = Release;
};
D1D1A9E07F0AB40E14CAC0F6 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = resources/AeroSpace.entitlements;
CODE_SIGN_IDENTITY = "aerospace-codesign-certificate";
COMBINE_HIDPI_IMAGES = YES;
GCC_TREAT_WARNINGS_AS_ERRORS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_LSUIElement = YES;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = "0.0.0-SNAPSHOT";
PRODUCT_BUNDLE_IDENTIFIER = bobko.aerospace;
PRODUCT_NAME = AeroSpace;
SDKROOT = macosx;
SWIFT_TREAT_WARNINGS_AS_ERRORS = YES;
SWIFT_VERSION = 6.2;
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
1C34EA41A1F045E016D1944D /* Build configuration list for PBXNativeTarget "AeroSpace" */ = {
isa = XCConfigurationList;
buildConfigurations = (
31B702864571F51814E4F12C /* Debug */,
D1D1A9E07F0AB40E14CAC0F6 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Debug;
};
D6982B0C3E92C5AF28BCD315 /* Build configuration list for PBXProject "AeroSpace" */ = {
isa = XCConfigurationList;
buildConfigurations = (
175127AAF914899705FABF12 /* Debug */,
A991F90908318BCD1655E904 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Debug;
};
/* End XCConfigurationList section */
/* Begin XCLocalSwiftPackageReference section */
9A00429279948F2879C9FE30 /* XCLocalSwiftPackageReference "." */ = {
isa = XCLocalSwiftPackageReference;
relativePath = .;
};
/* End XCLocalSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
018E55979F61DA6DA6DCB442 /* AppBundle */ = {
isa = XCSwiftPackageProductDependency;
productName = AppBundle;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 0B585B3093DA0FC12E7983E2 /* Project object */;
}
```
## /AeroSpace.xcodeproj/project.xcworkspace/contents.xcworkspacedata
```xcworkspacedata path="/AeroSpace.xcodeproj/project.xcworkspace/contents.xcworkspacedata"
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>
```
## /CONTRIBUTING.md
# Contributing
## Users cannot create GitHub Issues directly
AeroSpace project doesn't accept Issues directly - we ask you to create a [Discussion](https://github.com/nikitabobko/AeroSpace/discussions) first.
The submitted Issues are often either obvious duplicates, environmental problems, or configuration errors by the users themselves.
For a hobby project, I don't have enough time and energy to process every such submitted Issue.
As an alternative, you can start a Discussion on [GitHub discussions](https://github.com/nikitabobko/AeroSpace/discussions) forum.
Any Discussion which clearly identifies a problem and can be confirmed or reproduced will be converted to an Issue by maintainers.
It's users' responsibility to minimize their bugs as much as possible.
All users play a part in bugs reproduction.
In general the flow is the following:
- Discussions are here to kick-off the discussion and identify what the actionable item exactly is
- Issues are created later. Issues are well-formed, clear and actionable tasks
This pattern makes it easier for maintainers or contributors to find issues to work on since _almost every_ Issue is ready to be worked on.
## Submit 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!
* Did I mention 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
## Discuss issues/discussions
One of the most useful thing you can do is to discuss issues/discussions.
Imagine that you were assigned to fix the issue.
Try to suggest the best approach and design on how to fix the issue.
Suggest the synopsis/config format, reason in written form what is good about it, what is bad about it, what are the alternatives, etc.
Basically, see the "Prior discussion" section in [Submit Pull Requests](#submit-pull-requests).
If you have something to contribute to the conversation. Do it!
Please keep the conversation to the point. Discuss one issue at a time, crossreference other issues
You can take a look at the following issues:
* Most voted issues: https://github.com/nikitabobko/AeroSpace/issues?q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc
* Sometimes conversations happen on old issues that aren’t yet closed. See https://github.com/nikitabobko/AeroSpace/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc
* Issues that are unclear on how to fix, or issues that require design of the interface (CLI or config interface) are tagged with `design-needed` tag https://github.com/nikitabobko/AeroSpace/issues?q=is%3Aissue+is%3Aopen+label%3Adesign-needed
## Submit Pull Requests
Small and trivial improvements can be submitted without any discussion.
**Prior discussion**. For non-trivial changes (such as user visible changes), it's always better to ask for prior approval and discuss what you want to do before doing it.
Please create a new discussion and describe you want to do.
Consider including
* What users will observe after your change?
* Feature interaction with existing features or potential future features
* What use cases does it cover
* What is the proposed syntax for the config
* What is the proposed synopsis of CLI command
* How you think it should be implemented (if you can describe it)
* etc.
Discussing that you want to do something doesn't put any obligations on you. If you don't want to start the discussion just because you're afraid that you won't do it. Don't be afraid!
**Commit hygiene**. Each submitted commit must be atomic change (a Pull Request may contain several commits). Don't introduce new functional changes together with refactorings in the same commit.
Similarly, when implementing features and bug fixes, please stick to the structure of the codebase as much as possible and do not take this as an opportunity to do some "refactoring along the way".
A good commit message also mentions the motivation of the change (the commit describes what, why and how)
**License Agreement**. By contributing changes to this repository, you agree to license your contributions under the MIT license.
Maintainers can merge your pull request with arbitrary modifications.
**Pull request merge**. It cannot be guaranteed that your pull request will be merged even after the discussion.
Be ready that your pull request might be rejected because the implementation isn't good, or the approach is incorrect.
The prior discussion is here for you to minimize the risk of rejection.
## 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). https://github.com/sponsors/nikitabobko
## /Gemfile
``` path="/Gemfile"
# frozen_string_literal: true
ruby '~> 3.0' # >= 3.0 and < 4.0
source "https://rubygems.org"
gem 'asciidoctor', '2.0.23'
gem 'pygments.rb', '3.0'
```
## /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" : "ce497cf9fcf14272fedb221cc48e1102e7d3c1bb2ab918cedfbfa9120f32fca3",
"pins" : [
{
"identity" : "antlr4",
"kind" : "remoteSourceControl",
"location" : "https://github.com/antlr/antlr4",
"state" : {
"revision" : "7ed420ff2c78d62883875c442d75f32e73bc86c8",
"version" : "4.13.1"
}
},
{
"identity" : "bluesocket",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Kitura/BlueSocket.git",
"state" : {
"revision" : "7b23a867008e0027bfd6f4d398d44720707bc8ca",
"version" : "2.0.4"
}
},
{
"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" : "671108c96644956dddcd89dd59c203dcdb36cec7",
"version" : "1.1.4"
}
},
{
"identity" : "tomlkit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/LebJe/TOMLKit",
"state" : {
"revision" : "404c4dd011743461bff12d00a5118d0ed59d630c",
"version" : "0.5.5"
}
}
],
"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 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(path: "./ShellParserGenerated"),
.package(url: "https://github.com/InerziaSoft/ISSoundAdditions", exact: "2.0.1"),
.package(url: "https://github.com/Kitura/BlueSocket", exact: "2.0.4"),
.package(url: "https://github.com/LebJe/TOMLKit", exact: "0.5.5"),
.package(url: "https://github.com/apple/swift-collections", exact: "1.1.4"),
.package(url: "https://github.com/soffes/HotKey", 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"),
],
),
.target(
name: "AppBundle",
dependencies: [
.product(name: "Collections", package: "swift-collections"),
.product(name: "HotKey", package: "HotKey"),
.product(name: "ISSoundAdditions", package: "ISSoundAdditions"),
.product(name: "ShellParserGenerated", package: "ShellParserGenerated"),
.product(name: "Socket", package: "BlueSocket"),
.product(name: "TOMLKit", package: "TOMLKit"),
.target(name: "Common"),
.target(name: "PrivateApi"),
],
swiftSettings: [
.enableUpcomingFeature("NonisolatedNonsendingByDefault"),
],
),
.executableTarget(
name: "AeroSpaceApp",
dependencies: [
.target(name: "AppBundle"),
],
),
.executableTarget(
name: "Cli",
dependencies: [
.target(name: "Common"),
.product(name: "Socket", package: "BlueSocket"),
],
),
.testTarget(
name: "AppBundleTests",
dependencies: [
.target(name: "AppBundle"),
],
),
],
)
```
## /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)
## 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
- [ ] 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
## 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.
## 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
| | macOS 13 (Ventura) | macOS 14 (Sonoma) | macOS 15 (Sequoia) | macOS 26 (Tahoe) |
| ------------------------------------------------------------------------------ | ------------------ | ----------------- | ------------------ | ---------------- |
| AeroSpace binary runs on ... | + | + | + | + |
| AeroSpace debug build from sources is supported on ... | | + | + | + |
| AeroSpace release build from sources is supported on ... (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/)
## 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)
- [yabai](https://github.com/koekeishiya/yabai)
## /ShellParserGenerated/.gitignore
```gitignore path="/ShellParserGenerated/.gitignore"
.DS_Store
/.build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
```
## /ShellParserGenerated/Package.swift
```swift path="/ShellParserGenerated/Package.swift"
// swift-tools-version: 5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "ShellParserGenerated",
products: [
// Products define the executables and libraries a package produces, making them visible to other packages.
.library(
name: "ShellParserGenerated",
targets: ["ShellParserGenerated"]
),
],
dependencies: [
.package(url: "https://github.com/antlr/antlr4", exact: "4.13.1"),
],
targets: [
// 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.
.target(
name: "ShellParserGenerated",
dependencies: [
.product(name: "Antlr4Static", package: "antlr4"),
]
),
]
)
```
## /ShellParserGenerated/Sources/ShellParserGenerated/ShellLexer.swift
```swift path="/ShellParserGenerated/Sources/ShellParserGenerated/ShellLexer.swift"
// Generated from ./grammar/ShellLexer.g4 by ANTLR 4.13.1
import Antlr4
open class ShellLexer: Lexer {
internal static var _decisionToDFA: [DFA] = {
var decisionToDFA = [DFA]()
let length = ShellLexer._ATN.getNumberOfDecisions()
for i in 0..<length {
decisionToDFA.append(DFA(ShellLexer._ATN.getDecisionState(i)!, i))
}
return decisionToDFA
}()
internal static let _sharedContextCache = PredictionContextCache()
public
static let TRIPLE_QUOTE=1, SINGLE_QUOTED_STRING=2, LDQUOTE=3, LPAR=4, INTERPOLATION_START=5,
RPAR=6, ELIF=7, IF=8, SWITCH=9, CASE=10, DO=11, THEN=12, ELSE=13,
FOR=14, WHILE=15, CATCH=16, IN=17, END=18, DEFER=19, AND=20,
PIPE=21, OR=22, SEMICOLON=23, NL=24, WORD=25, ARG=26, ESCAPE_NEWLINE=27,
COMMENT=28, SPACES=29, ANY=30, TEXT=31, INTERPOLATION_START_IN_DSTRING=32,
ESCAPE_SEQUENCE=33, RDQUOTE=34
public
static let IN_DSTRING=1
public
static let channelNames: [String] = [
"DEFAULT_TOKEN_CHANNEL", "HIDDEN"
]
public
static let modeNames: [String] = [
"DEFAULT_MODE", "IN_DSTRING"
]
public
static let ruleNames: [String] = [
"TRIPLE_QUOTE", "SINGLE_QUOTED_STRING", "LDQUOTE", "LPAR", "INTERPOLATION_START",
"RPAR", "ELIF", "IF", "SWITCH", "CASE", "DO", "THEN", "ELSE", "FOR", "WHILE",
"CATCH", "IN", "END", "DEFER", "AND", "PIPE", "OR", "SEMICOLON", "NL",
"WORD", "ARG", "ESCAPE_NEWLINE", "COMMENT", "SPACES", "ANY", "TEXT", "INTERPOLATION_START_IN_DSTRING",
"ESCAPE_SEQUENCE", "RDQUOTE"
]
private static let _LITERAL_NAMES: [String?] = [
nil, nil, nil, nil, "'('", nil, "')'", nil, nil, nil, nil, nil, nil, nil,
nil, nil, nil, nil, nil, nil, "'&&'", "'|'", "'||'", "';'"
]
private static let _SYMBOLIC_NAMES: [String?] = [
nil, "TRIPLE_QUOTE", "SINGLE_QUOTED_STRING", "LDQUOTE", "LPAR", "INTERPOLATION_START",
"RPAR", "ELIF", "IF", "SWITCH", "CASE", "DO", "THEN", "ELSE", "FOR", "WHILE",
"CATCH", "IN", "END", "DEFER", "AND", "PIPE", "OR", "SEMICOLON", "NL",
"WORD", "ARG", "ESCAPE_NEWLINE", "COMMENT", "SPACES", "ANY", "TEXT", "INTERPOLATION_START_IN_DSTRING",
"ESCAPE_SEQUENCE", "RDQUOTE"
]
public
static let VOCABULARY = Vocabulary(_LITERAL_NAMES, _SYMBOLIC_NAMES)
override open
func getVocabulary() -> Vocabulary {
return ShellLexer.VOCABULARY
}
public
required init(_ input: CharStream) {
RuntimeMetaData.checkVersion("4.13.1", RuntimeMetaData.VERSION)
super.init(input)
_interp = LexerATNSimulator(self, ShellLexer._ATN, ShellLexer._decisionToDFA, ShellLexer._sharedContextCache)
}
override open
func getGrammarFileName() -> String { return "ShellLexer.g4" }
override open
func getRuleNames() -> [String] { return ShellLexer.ruleNames }
override open
func getSerializedATN() -> [Int] { return ShellLexer._serializedATN }
override open
func getChannelNames() -> [String] { return ShellLexer.channelNames }
override open
func getModeNames() -> [String] { return ShellLexer.modeNames }
override open
func getATN() -> ATN { return ShellLexer._ATN }
override open
func action(_ _localctx: RuleContext?, _ ruleIndex: Int, _ actionIndex: Int) throws {
switch (ruleIndex) {
case 5:
try RPAR_action((_localctx as RuleContext?), actionIndex)
case 33:
try RDQUOTE_action((_localctx as RuleContext?), actionIndex)
default: break
}
}
private func RPAR_action(_ _localctx: RuleContext?, _ actionIndex: Int) throws {
switch (actionIndex) {
case 0:
_ = try? popMode()
default: break
}
}
private func RDQUOTE_action(_ _localctx: RuleContext?, _ actionIndex: Int) throws {
switch (actionIndex) {
case 1:
_ = try? popMode()
default: break
}
}
static let _serializedATN:[Int] = [
4,0,34,316,6,-1,6,-1,2,0,7,0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5,2,
6,7,6,2,7,7,7,2,8,7,8,2,9,7,9,2,10,7,10,2,11,7,11,2,12,7,12,2,13,7,13,
2,14,7,14,2,15,7,15,2,16,7,16,2,17,7,17,2,18,7,18,2,19,7,19,2,20,7,20,
2,21,7,21,2,22,7,22,2,23,7,23,2,24,7,24,2,25,7,25,2,26,7,26,2,27,7,27,
2,28,7,28,2,29,7,29,2,30,7,30,2,31,7,31,2,32,7,32,2,33,7,33,1,0,1,0,1,
0,1,0,1,0,1,0,3,0,77,8,0,1,1,1,1,5,1,81,8,1,10,1,12,1,84,9,1,1,1,1,1,1,
2,1,2,1,2,1,2,1,3,1,3,1,3,1,3,1,4,1,4,1,4,1,4,1,4,1,5,1,5,1,5,1,6,1,6,
1,6,1,6,1,6,1,6,5,6,110,8,6,10,6,12,6,113,9,6,1,7,1,7,1,7,1,7,5,7,119,
8,7,10,7,12,7,122,9,7,1,8,1,8,1,8,1,8,1,8,1,8,1,8,1,8,5,8,132,8,8,10,8,
12,8,135,9,8,1,9,1,9,1,9,1,9,1,9,1,9,5,9,143,8,9,10,9,12,9,146,9,9,1,10,
1,10,1,10,1,10,5,10,152,8,10,10,10,12,10,155,9,10,1,11,1,11,1,11,1,11,
1,11,1,11,5,11,163,8,11,10,11,12,11,166,9,11,1,12,1,12,1,12,1,12,1,12,
1,12,5,12,174,8,12,10,12,12,12,177,9,12,1,13,1,13,1,13,1,13,1,13,5,13,
184,8,13,10,13,12,13,187,9,13,1,14,1,14,1,14,1,14,1,14,1,14,1,14,5,14,
196,8,14,10,14,12,14,199,9,14,1,15,1,15,1,15,1,15,1,15,1,15,1,15,5,15,
208,8,15,10,15,12,15,211,9,15,1,16,1,16,1,16,1,16,5,16,217,8,16,10,16,
12,16,220,9,16,1,17,1,17,1,17,1,17,1,17,5,17,227,8,17,10,17,12,17,230,
9,17,1,18,1,18,1,18,1,18,1,18,1,18,1,18,5,18,239,8,18,10,18,12,18,242,
9,18,1,19,1,19,1,19,1,20,1,20,1,21,1,21,1,21,1,22,1,22,1,23,3,23,255,8,
23,1,23,3,23,258,8,23,1,23,1,23,1,24,4,24,263,8,24,11,24,12,24,264,1,25,
4,25,268,8,25,11,25,12,25,269,1,26,1,26,3,26,274,8,26,1,26,3,26,277,8,
26,1,26,1,26,1,26,1,26,1,27,1,27,5,27,285,8,27,10,27,12,27,288,9,27,1,
27,1,27,1,28,4,28,293,8,28,11,28,12,28,294,1,28,1,28,1,29,1,29,1,30,4,
30,302,8,30,11,30,12,30,303,1,31,1,31,1,31,1,31,1,31,1,32,1,32,1,32,1,
33,1,33,1,33,1,82,0,34,2,1,4,2,6,3,8,4,10,5,12,6,14,7,16,8,18,9,20,10,
22,11,24,12,26,13,28,14,30,15,32,16,34,17,36,18,38,19,40,20,42,21,44,22,
46,23,48,24,50,25,52,26,54,27,56,28,58,29,60,30,62,31,64,32,66,33,68,34,
2,0,1,5,4,0,45,47,65,90,95,95,97,122,8,0,33,33,37,37,43,57,61,61,65,90,
94,95,97,123,125,125,1,0,10,10,2,0,9,9,32,32,3,0,34,34,36,36,92,92,338,
0,2,1,0,0,0,0,4,1,0,0,0,0,6,1,0,0,0,0,8,1,0,0,0,0,10,1,0,0,0,0,12,1,0,
0,0,0,14,1,0,0,0,0,16,1,0,0,0,0,18,1,0,0,0,0,20,1,0,0,0,0,22,1,0,0,0,0,
24,1,0,0,0,0,26,1,0,0,0,0,28,1,0,0,0,0,30,1,0,0,0,0,32,1,0,0,0,0,34,1,
0,0,0,0,36,1,0,0,0,0,38,1,0,0,0,0,40,1,0,0,0,0,42,1,0,0,0,0,44,1,0,0,0,
0,46,1,0,0,0,0,48,1,0,0,0,0,50,1,0,0,0,0,52,1,0,0,0,0,54,1,0,0,0,0,56,
1,0,0,0,0,58,1,0,0,0,0,60,1,0,0,0,1,62,1,0,0,0,1,64,1,0,0,0,1,66,1,0,0,
0,1,68,1,0,0,0,2,76,1,0,0,0,4,78,1,0,0,0,6,87,1,0,0,0,8,91,1,0,0,0,10,
95,1,0,0,0,12,100,1,0,0,0,14,103,1,0,0,0,16,114,1,0,0,0,18,123,1,0,0,0,
20,136,1,0,0,0,22,147,1,0,0,0,24,156,1,0,0,0,26,167,1,0,0,0,28,178,1,0,
0,0,30,188,1,0,0,0,32,200,1,0,0,0,34,212,1,0,0,0,36,221,1,0,0,0,38,231,
1,0,0,0,40,243,1,0,0,0,42,246,1,0,0,0,44,248,1,0,0,0,46,251,1,0,0,0,48,
254,1,0,0,0,50,262,1,0,0,0,52,267,1,0,0,0,54,271,1,0,0,0,56,282,1,0,0,
0,58,292,1,0,0,0,60,298,1,0,0,0,62,301,1,0,0,0,64,305,1,0,0,0,66,310,1,
0,0,0,68,313,1,0,0,0,70,71,5,34,0,0,71,72,5,34,0,0,72,77,5,34,0,0,73,74,
5,39,0,0,74,75,5,39,0,0,75,77,5,39,0,0,76,70,1,0,0,0,76,73,1,0,0,0,77,
3,1,0,0,0,78,82,5,39,0,0,79,81,9,0,0,0,80,79,1,0,0,0,81,84,1,0,0,0,82,
83,1,0,0,0,82,80,1,0,0,0,83,85,1,0,0,0,84,82,1,0,0,0,85,86,5,39,0,0,86,
5,1,0,0,0,87,88,5,34,0,0,88,89,1,0,0,0,89,90,6,2,0,0,90,7,1,0,0,0,91,92,
5,40,0,0,92,93,1,0,0,0,93,94,6,3,1,0,94,9,1,0,0,0,95,96,5,36,0,0,96,97,
5,40,0,0,97,98,1,0,0,0,98,99,6,4,1,0,99,11,1,0,0,0,100,101,5,41,0,0,101,
102,6,5,2,0,102,13,1,0,0,0,103,104,5,101,0,0,104,105,5,108,0,0,105,106,
5,105,0,0,106,107,5,102,0,0,107,111,1,0,0,0,108,110,3,48,23,0,109,108,
1,0,0,0,110,113,1,0,0,0,111,109,1,0,0,0,111,112,1,0,0,0,112,15,1,0,0,0,
113,111,1,0,0,0,114,115,5,105,0,0,115,116,5,102,0,0,116,120,1,0,0,0,117,
119,3,48,23,0,118,117,1,0,0,0,119,122,1,0,0,0,120,118,1,0,0,0,120,121,
1,0,0,0,121,17,1,0,0,0,122,120,1,0,0,0,123,124,5,115,0,0,124,125,5,119,
0,0,125,126,5,105,0,0,126,127,5,116,0,0,127,128,5,99,0,0,128,129,5,104,
0,0,129,133,1,0,0,0,130,132,3,48,23,0,131,130,1,0,0,0,132,135,1,0,0,0,
133,131,1,0,0,0,133,134,1,0,0,0,134,19,1,0,0,0,135,133,1,0,0,0,136,137,
5,99,0,0,137,138,5,97,0,0,138,139,5,115,0,0,139,140,5,101,0,0,140,144,
1,0,0,0,141,143,3,48,23,0,142,141,1,0,0,0,143,146,1,0,0,0,144,142,1,0,
0,0,144,145,1,0,0,0,145,21,1,0,0,0,146,144,1,0,0,0,147,148,5,100,0,0,148,
149,5,111,0,0,149,153,1,0,0,0,150,152,3,48,23,0,151,150,1,0,0,0,152,155,
1,0,0,0,153,151,1,0,0,0,153,154,1,0,0,0,154,23,1,0,0,0,155,153,1,0,0,0,
156,157,5,116,0,0,157,158,5,104,0,0,158,159,5,101,0,0,159,160,5,110,0,
0,160,164,1,0,0,0,161,163,3,48,23,0,162,161,1,0,0,0,163,166,1,0,0,0,164,
162,1,0,0,0,164,165,1,0,0,0,165,25,1,0,0,0,166,164,1,0,0,0,167,168,5,101,
0,0,168,169,5,108,0,0,169,170,5,115,0,0,170,171,5,101,0,0,171,175,1,0,
0,0,172,174,3,48,23,0,173,172,1,0,0,0,174,177,1,0,0,0,175,173,1,0,0,0,
175,176,1,0,0,0,176,27,1,0,0,0,177,175,1,0,0,0,178,179,5,102,0,0,179,180,
5,111,0,0,180,181,5,114,0,0,181,185,1,0,0,0,182,184,3,48,23,0,183,182,
1,0,0,0,184,187,1,0,0,0,185,183,1,0,0,0,185,186,1,0,0,0,186,29,1,0,0,0,
187,185,1,0,0,0,188,189,5,119,0,0,189,190,5,104,0,0,190,191,5,105,0,0,
191,192,5,108,0,0,192,193,5,101,0,0,193,197,1,0,0,0,194,196,3,48,23,0,
195,194,1,0,0,0,196,199,1,0,0,0,197,195,1,0,0,0,197,198,1,0,0,0,198,31,
1,0,0,0,199,197,1,0,0,0,200,201,5,99,0,0,201,202,5,97,0,0,202,203,5,116,
0,0,203,204,5,99,0,0,204,205,5,104,0,0,205,209,1,0,0,0,206,208,3,48,23,
0,207,206,1,0,0,0,208,211,1,0,0,0,209,207,1,0,0,0,209,210,1,0,0,0,210,
33,1,0,0,0,211,209,1,0,0,0,212,213,5,105,0,0,213,214,5,110,0,0,214,218,
1,0,0,0,215,217,3,48,23,0,216,215,1,0,0,0,217,220,1,0,0,0,218,216,1,0,
0,0,218,219,1,0,0,0,219,35,1,0,0,0,220,218,1,0,0,0,221,222,5,101,0,0,222,
223,5,110,0,0,223,224,5,100,0,0,224,228,1,0,0,0,225,227,3,48,23,0,226,
225,1,0,0,0,227,230,1,0,0,0,228,226,1,0,0,0,228,229,1,0,0,0,229,37,1,0,
0,0,230,228,1,0,0,0,231,232,5,100,0,0,232,233,5,101,0,0,233,234,5,102,
0,0,234,235,5,101,0,0,235,236,5,114,0,0,236,240,1,0,0,0,237,239,3,48,23,
0,238,237,1,0,0,0,239,242,1,0,0,0,240,238,1,0,0,0,240,241,1,0,0,0,241,
39,1,0,0,0,242,240,1,0,0,0,243,244,5,38,0,0,244,245,5,38,0,0,245,41,1,
0,0,0,246,247,5,124,0,0,247,43,1,0,0,0,248,249,5,124,0,0,249,250,5,124,
0,0,250,45,1,0,0,0,251,252,5,59,0,0,252,47,1,0,0,0,253,255,3,58,28,0,254,
253,1,0,0,0,254,255,1,0,0,0,255,257,1,0,0,0,256,258,5,13,0,0,257,256,1,
0,0,0,257,258,1,0,0,0,258,259,1,0,0,0,259,260,5,10,0,0,260,49,1,0,0,0,
261,263,7,0,0,0,262,261,1,0,0,0,263,264,1,0,0,0,264,262,1,0,0,0,264,265,
1,0,0,0,265,51,1,0,0,0,266,268,7,1,0,0,267,266,1,0,0,0,268,269,1,0,0,0,
269,267,1,0,0,0,269,270,1,0,0,0,270,53,1,0,0,0,271,273,5,92,0,0,272,274,
3,58,28,0,273,272,1,0,0,0,273,274,1,0,0,0,274,276,1,0,0,0,275,277,3,56,
27,0,276,275,1,0,0,0,276,277,1,0,0,0,277,278,1,0,0,0,278,279,5,10,0,0,
279,280,1,0,0,0,280,281,6,26,3,0,281,55,1,0,0,0,282,286,5,35,0,0,283,285,
8,2,0,0,284,283,1,0,0,0,285,288,1,0,0,0,286,284,1,0,0,0,286,287,1,0,0,
0,287,289,1,0,0,0,288,286,1,0,0,0,289,290,6,27,3,0,290,57,1,0,0,0,291,
293,7,3,0,0,292,291,1,0,0,0,293,294,1,0,0,0,294,292,1,0,0,0,294,295,1,
0,0,0,295,296,1,0,0,0,296,297,6,28,3,0,297,59,1,0,0,0,298,299,9,0,0,0,
299,61,1,0,0,0,300,302,8,4,0,0,301,300,1,0,0,0,302,303,1,0,0,0,303,301,
1,0,0,0,303,304,1,0,0,0,304,63,1,0,0,0,305,306,5,36,0,0,306,307,5,40,0,
0,307,308,1,0,0,0,308,309,6,31,1,0,309,65,1,0,0,0,310,311,5,92,0,0,311,
312,9,0,0,0,312,67,1,0,0,0,313,314,5,34,0,0,314,315,6,33,4,0,315,69,1,
0,0,0,28,0,1,76,82,111,120,133,144,153,164,175,185,197,209,218,228,240,
254,257,262,264,267,269,273,276,286,294,303,5,5,1,0,5,0,0,1,5,0,6,0,0,
1,33,1
]
public
static let _ATN: ATN = try! ATNDeserializer().deserialize(_serializedATN)
}
```
## /ShellParserGenerated/Sources/ShellParserGenerated/ShellParser.swift
```swift path="/ShellParserGenerated/Sources/ShellParserGenerated/ShellParser.swift"
// Generated from ./grammar/ShellParser.g4 by ANTLR 4.13.1
import Antlr4
open class ShellParser: Parser {
internal static var _decisionToDFA: [DFA] = {
var decisionToDFA = [DFA]()
let length = ShellParser._ATN.getNumberOfDecisions()
for i in 0..<length {
decisionToDFA.append(DFA(ShellParser._ATN.getDecisionState(i)!, i))
}
return decisionToDFA
}()
internal static let _sharedContextCache = PredictionContextCache()
public
enum Tokens: Int {
case EOF = -1, TRIPLE_QUOTE = 1, SINGLE_QUOTED_STRING = 2, LDQUOTE = 3,
LPAR = 4, INTERPOLATION_START = 5, RPAR = 6, ELIF = 7,
IF = 8, SWITCH = 9, CASE = 10, DO = 11, THEN = 12, ELSE = 13,
FOR = 14, WHILE = 15, CATCH = 16, IN = 17, END = 18, DEFER = 19,
AND = 20, PIPE = 21, OR = 22, SEMICOLON = 23, NL = 24,
WORD = 25, ARG = 26, ESCAPE_NEWLINE = 27, COMMENT = 28,
SPACES = 29, ANY = 30, TEXT = 31, INTERPOLATION_START_IN_DSTRING = 32,
ESCAPE_SEQUENCE = 33, RDQUOTE = 34
}
public
static let RULE_root = 0, RULE_cmds = 1, RULE_cmd = 2, RULE_arg = 3, RULE_dStringFragment = 4
public
static let ruleNames: [String] = [
"root", "cmds", "cmd", "arg", "dStringFragment"
]
private static let _LITERAL_NAMES: [String?] = [
nil, nil, nil, nil, "'('", nil, "')'", nil, nil, nil, nil, nil, nil, nil,
nil, nil, nil, nil, nil, nil, "'&&'", "'|'", "'||'", "';'"
]
private static let _SYMBOLIC_NAMES: [String?] = [
nil, "TRIPLE_QUOTE", "SINGLE_QUOTED_STRING", "LDQUOTE", "LPAR", "INTERPOLATION_START",
"RPAR", "ELIF", "IF", "SWITCH", "CASE", "DO", "THEN", "ELSE", "FOR", "WHILE",
"CATCH", "IN", "END", "DEFER", "AND", "PIPE", "OR", "SEMICOLON", "NL",
"WORD", "ARG", "ESCAPE_NEWLINE", "COMMENT", "SPACES", "ANY", "TEXT", "INTERPOLATION_START_IN_DSTRING",
"ESCAPE_SEQUENCE", "RDQUOTE"
]
public
static let VOCABULARY = Vocabulary(_LITERAL_NAMES, _SYMBOLIC_NAMES)
override open
func getGrammarFileName() -> String { return "ShellParser.g4" }
override open
func getRuleNames() -> [String] { return ShellParser.ruleNames }
override open
func getSerializedATN() -> [Int] { return ShellParser._serializedATN }
override open
func getATN() -> ATN { return ShellParser._ATN }
override open
func getVocabulary() -> Vocabulary {
return ShellParser.VOCABULARY
}
override public
init(_ input:TokenStream) throws {
RuntimeMetaData.checkVersion("4.13.1", RuntimeMetaData.VERSION)
try super.init(input)
_interp = ParserATNSimulator(self,ShellParser._ATN,ShellParser._decisionToDFA, ShellParser._sharedContextCache)
}
public class RootContext: ParserRuleContext {
open
func cmds() -> CmdsContext? {
return getRuleContext(CmdsContext.self, 0)
}
open
func EOF() -> TerminalNode? {
return getToken(ShellParser.Tokens.EOF.rawValue, 0)
}
open
func NL() -> [TerminalNode] {
return getTokens(ShellParser.Tokens.NL.rawValue)
}
open
func NL(_ i:Int) -> TerminalNode? {
return getToken(ShellParser.Tokens.NL.rawValue, i)
}
override open
func getRuleIndex() -> Int {
return ShellParser.RULE_root
}
}
@discardableResult
open func root() throws -> RootContext {
var _localctx: RootContext
_localctx = RootContext(_ctx, getState())
try enterRule(_localctx, 0, ShellParser.RULE_root)
var _la: Int = 0
defer {
try! exitRule()
}
do {
try enterOuterAlt(_localctx, 1)
setState(13)
try _errHandler.sync(self)
_la = try _input.LA(1)
while (_la == ShellParser.Tokens.NL.rawValue) {
setState(10)
try match(ShellParser.Tokens.NL.rawValue)
setState(15)
try _errHandler.sync(self)
_la = try _input.LA(1)
}
setState(20)
try _errHandler.sync(self)
switch (ShellParser.Tokens(rawValue: try _input.LA(1))!) {
case .LPAR:fallthrough
case .IF:fallthrough
case .WORD:
setState(16)
try cmds()
setState(17)
try match(ShellParser.Tokens.EOF.rawValue)
break
case .EOF:
setState(19)
try match(ShellParser.Tokens.EOF.rawValue)
break
default:
throw ANTLRException.recognition(e: NoViableAltException(self))
}
}
catch ANTLRException.recognition(let re) {
_localctx.exception = re
_errHandler.reportError(self, re)
try _errHandler.recover(self, re)
}
return _localctx
}
public class CmdsContext: ParserRuleContext {
override open
func getRuleIndex() -> Int {
return ShellParser.RULE_cmds
}
}
public class IfElseContext: CmdsContext {
open
func IF() -> TerminalNode? {
return getToken(ShellParser.Tokens.IF.rawValue, 0)
}
open
func cmd() -> [CmdContext] {
return getRuleContexts(CmdContext.self)
}
open
func cmd(_ i: Int) -> CmdContext? {
return getRuleContext(CmdContext.self, i)
}
open
func THEN() -> [TerminalNode] {
return getTokens(ShellParser.Tokens.THEN.rawValue)
}
open
func THEN(_ i:Int) -> TerminalNode? {
return getToken(ShellParser.Tokens.THEN.rawValue, i)
}
open
func END() -> TerminalNode? {
return getToken(ShellParser.Tokens.END.rawValue, 0)
}
open
func cmds() -> [CmdsContext] {
return getRuleContexts(CmdsContext.self)
}
open
func cmds(_ i: Int) -> CmdsContext? {
return getRuleContext(CmdsContext.self, i)
}
open
func ELIF() -> [TerminalNode] {
return getTokens(ShellParser.Tokens.ELIF.rawValue)
}
open
func ELIF(_ i:Int) -> TerminalNode? {
return getToken(ShellParser.Tokens.ELIF.rawValue, i)
}
open
func ELSE() -> TerminalNode? {
return getToken(ShellParser.Tokens.ELSE.rawValue, 0)
}
public
init(_ ctx: CmdsContext) {
super.init()
copyFrom(ctx)
}
}
public class SeqContext: CmdsContext {
open
func cmd() -> CmdContext? {
return getRuleContext(CmdContext.self, 0)
}
open
func cmds() -> [CmdsContext] {
return getRuleContexts(CmdsContext.self)
}
open
func cmds(_ i: Int) -> CmdsContext? {
return getRuleContext(CmdsContext.self, i)
}
open
func SEMICOLON() -> [TerminalNode] {
return getTokens(ShellParser.Tokens.SEMICOLON.rawValue)
}
open
func SEMICOLON(_ i:Int) -> TerminalNode? {
return getToken(ShellParser.Tokens.SEMICOLON.rawValue, i)
}
open
func NL() -> [TerminalNode] {
return getTokens(ShellParser.Tokens.NL.rawValue)
}
open
func NL(_ i:Int) -> TerminalNode? {
return getToken(ShellParser.Tokens.NL.rawValue, i)
}
public
init(_ ctx: CmdsContext) {
super.init()
copyFrom(ctx)
}
}
@discardableResult
open func cmds() throws -> CmdsContext {
var _localctx: CmdsContext
_localctx = CmdsContext(_ctx, getState())
try enterRule(_localctx, 2, ShellParser.RULE_cmds)
var _la: Int = 0
defer {
try! exitRule()
}
do {
var _alt:Int
setState(60)
try _errHandler.sync(self)
switch(try getInterpreter().adaptivePredict(_input,9, _ctx)) {
case 1:
_localctx = IfElseContext(_localctx);
try enterOuterAlt(_localctx, 1)
setState(22)
try match(ShellParser.Tokens.IF.rawValue)
setState(23)
try cmd(0)
setState(24)
try match(ShellParser.Tokens.THEN.rawValue)
setState(26)
try _errHandler.sync(self)
_la = try _input.LA(1)
if (((Int64(_la) & ~0x3f) == 0 && ((Int64(1) << _la) & 33554704) != 0)) {
setState(25)
try cmds()
}
setState(36)
try _errHandler.sync(self)
_la = try _input.LA(1)
while (_la == ShellParser.Tokens.ELIF.rawValue) {
setState(28)
try match(ShellParser.Tokens.ELIF.rawValue)
setState(29)
try cmd(0)
setState(30)
try match(ShellParser.Tokens.THEN.rawValue)
setState(32)
try _errHandler.sync(self)
_la = try _input.LA(1)
if (((Int64(_la) & ~0x3f) == 0 && ((Int64(1) << _la) & 33554704) != 0)) {
setState(31)
try cmds()
}
setState(38)
try _errHandler.sync(self)
_la = try _input.LA(1)
}
setState(43)
try _errHandler.sync(self)
_la = try _input.LA(1)
if (_la == ShellParser.Tokens.ELSE.rawValue) {
setState(39)
try match(ShellParser.Tokens.ELSE.rawValue)
setState(41)
try _errHandler.sync(self)
_la = try _input.LA(1)
if (((Int64(_la) & ~0x3f) == 0 && ((Int64(1) << _la) & 33554704) != 0)) {
setState(40)
try cmds()
}
}
setState(45)
try match(ShellParser.Tokens.END.rawValue)
break
case 2:
_localctx = SeqContext(_localctx);
try enterOuterAlt(_localctx, 2)
setState(47)
try cmd(0)
setState(49);
try _errHandler.sync(self)
_alt = 1;
repeat {
switch (_alt) {
case 1:
setState(48)
_la = try _input.LA(1)
if (!(_la == ShellParser.Tokens.SEMICOLON.rawValue || _la == ShellParser.Tokens.NL.rawValue)) {
try _errHandler.recoverInline(self)
}
else {
_errHandler.reportMatch(self)
try consume()
}
break
default:
throw ANTLRException.recognition(e: NoViableAltException(self))
}
setState(51);
try _errHandler.sync(self)
_alt = try getInterpreter().adaptivePredict(_input,7,_ctx)
} while (_alt != 2 && _alt != ATN.INVALID_ALT_NUMBER)
setState(56)
try _errHandler.sync(self)
_alt = try getInterpreter().adaptivePredict(_input,8,_ctx)
while (_alt != 1 && _alt != ATN.INVALID_ALT_NUMBER) {
if ( _alt==1+1 ) {
setState(53)
try cmds()
}
setState(58)
try _errHandler.sync(self)
_alt = try getInterpreter().adaptivePredict(_input,8,_ctx)
}
break
case 3:
_localctx = SeqContext(_localctx);
try enterOuterAlt(_localctx, 3)
setState(59)
try cmd(0)
break
default: break
}
}
catch ANTLRException.recognition(let re) {
_localctx.exception = re
_errHandler.reportError(self, re)
try _errHandler.recover(self, re)
}
return _localctx
}
public class CmdContext: ParserRuleContext {
override open
func getRuleIndex() -> Int {
return ShellParser.RULE_cmd
}
}
public class ArgsContext: CmdContext {
open
func WORD() -> TerminalNode? {
return getToken(ShellParser.Tokens.WORD.rawValue, 0)
}
open
func arg() -> [ArgContext] {
return getRuleContexts(ArgContext.self)
}
open
func arg(_ i: Int) -> ArgContext? {
return getRuleContext(ArgContext.self, i)
}
public
init(_ ctx: CmdContext) {
super.init()
copyFrom(ctx)
}
}
public class CmdErrorContext: CmdContext {
open
func LPAR() -> TerminalNode? {
return getToken(ShellParser.Tokens.LPAR.rawValue, 0)
}
open
func cmds() -> CmdsContext? {
return getRuleContext(CmdsContext.self, 0)
}
open
func RPAR() -> [TerminalNode] {
return getTokens(ShellParser.Tokens.RPAR.rawValue)
}
open
func RPAR(_ i:Int) -> TerminalNode? {
return getToken(ShellParser.Tokens.RPAR.rawValue, i)
}
public
init(_ ctx: CmdContext) {
super.init()
copyFrom(ctx)
}
}
public class OrContext: CmdContext {
open
func cmd() -> [CmdContext] {
return getRuleContexts(CmdContext.self)
}
open
func cmd(_ i: Int) -> CmdContext? {
return getRuleContext(CmdContext.self, i)
}
open
func OR() -> TerminalNode? {
return getToken(ShellParser.Tokens.OR.rawValue, 0)
}
open
func NL() -> [TerminalNode] {
return getTokens(ShellParser.Tokens.NL.rawValue)
}
open
func NL(_ i:Int) -> TerminalNode? {
return getToken(ShellParser.Tokens.NL.rawValue, i)
}
public
init(_ ctx: CmdContext) {
super.init()
copyFrom(ctx)
}
}
public class ParensContext: CmdContext {
open
func LPAR() -> TerminalNode? {
return getToken(ShellParser.Tokens.LPAR.rawValue, 0)
}
open
func cmds() -> CmdsContext? {
return getRuleContext(CmdsContext.self, 0)
}
open
func RPAR() -> TerminalNode? {
return getToken(ShellParser.Tokens.RPAR.rawValue, 0)
}
public
init(_ ctx: CmdContext) {
super.init()
copyFrom(ctx)
}
}
public class AndContext: CmdContext {
open
func cmd() -> [CmdContext] {
return getRuleContexts(CmdContext.self)
}
open
func cmd(_ i: Int) -> CmdContext? {
return getRuleContext(CmdContext.self, i)
}
open
func AND() -> TerminalNode? {
return getToken(ShellParser.Tokens.AND.rawValue, 0)
}
open
func NL() -> [TerminalNode] {
return getTokens(ShellParser.Tokens.NL.rawValue)
}
open
func NL(_ i:Int) -> TerminalNode? {
return getToken(ShellParser.Tokens.NL.rawValue, i)
}
public
init(_ ctx: CmdContext) {
super.init()
copyFrom(ctx)
}
}
public class PipeContext: CmdContext {
open
func cmd() -> [CmdContext] {
return getRuleContexts(CmdContext.self)
}
open
func cmd(_ i: Int) -> CmdContext? {
return getRuleContext(CmdContext.self, i)
}
open
func PIPE() -> TerminalNode? {
return getToken(ShellParser.Tokens.PIPE.rawValue, 0)
}
open
func NL() -> [TerminalNode] {
return getTokens(ShellParser.Tokens.NL.rawValue)
}
open
func NL(_ i:Int) -> TerminalNode? {
return getToken(ShellParser.Tokens.NL.rawValue, i)
}
public
init(_ ctx: CmdContext) {
super.init()
copyFrom(ctx)
}
}
public final func cmd( ) throws -> CmdContext {
return try cmd(0)
}
@discardableResult
private func cmd(_ _p: Int) throws -> CmdContext {
let _parentctx: ParserRuleContext? = _ctx
let _parentState: Int = getState()
var _localctx: CmdContext
_localctx = CmdContext(_ctx, _parentState)
let _startState: Int = 4
try enterRecursionRule(_localctx, 4, ShellParser.RULE_cmd, _p)
var _la: Int = 0
defer {
try! unrollRecursionContexts(_parentctx)
}
do {
var _alt: Int
try enterOuterAlt(_localctx, 1)
setState(84)
try _errHandler.sync(self)
switch(try getInterpreter().adaptivePredict(_input,11, _ctx)) {
case 1:
_localctx = ArgsContext(_localctx)
_ctx = _localctx
setState(63)
try match(ShellParser.Tokens.WORD.rawValue)
setState(67)
try _errHandler.sync(self)
_alt = try getInterpreter().adaptivePredict(_input,10,_ctx)
while (_alt != 2 && _alt != ATN.INVALID_ALT_NUMBER) {
if ( _alt==1 ) {
setState(64)
try arg()
}
setState(69)
try _errHandler.sync(self)
_alt = try getInterpreter().adaptivePredict(_input,10,_ctx)
}
break
case 2:
_localctx = ParensContext(_localctx)
_ctx = _localctx
setState(70)
try match(ShellParser.Tokens.LPAR.rawValue)
setState(71)
try cmds()
setState(72)
try match(ShellParser.Tokens.RPAR.rawValue)
break
case 3:
_localctx = CmdErrorContext(_localctx)
_ctx = _localctx
setState(74)
try match(ShellParser.Tokens.LPAR.rawValue)
setState(75)
try cmds()
setState(76)
try match(ShellParser.Tokens.RPAR.rawValue)
setState(77)
try match(ShellParser.Tokens.RPAR.rawValue)
notifyErrorListeners("Unbalanced parenthesis")
break
case 4:
_localctx = CmdErrorContext(_localctx)
_ctx = _localctx
setState(80)
try match(ShellParser.Tokens.LPAR.rawValue)
setState(81)
try cmds()
notifyErrorListeners("Unbalanced parenthesis")
break
default: break
}
_ctx!.stop = try _input.LT(-1)
setState(115)
try _errHandler.sync(self)
_alt = try getInterpreter().adaptivePredict(_input,16,_ctx)
while (_alt != 2 && _alt != ATN.INVALID_ALT_NUMBER) {
if ( _alt==1 ) {
if _parseListeners != nil {
try triggerExitRuleEvent()
}
setState(113)
try _errHandler.sync(self)
switch(try getInterpreter().adaptivePredict(_input,15, _ctx)) {
case 1:
_localctx = PipeContext( CmdContext(_parentctx, _parentState))
try pushNewRecursionContext(_localctx, _startState, ShellParser.RULE_cmd)
setState(86)
if (!(precpred(_ctx, 6))) {
throw ANTLRException.recognition(e:FailedPredicateException(self, "precpred(_ctx, 6)"))
}
setState(90)
try _errHandler.sync(self)
_la = try _input.LA(1)
while (_la == ShellParser.Tokens.NL.rawValue) {
setState(87)
try match(ShellParser.Tokens.NL.rawValue)
setState(92)
try _errHandler.sync(self)
_la = try _input.LA(1)
}
setState(93)
try match(ShellParser.Tokens.PIPE.rawValue)
setState(94)
try cmd(7)
break
case 2:
_localctx = AndContext( CmdContext(_parentctx, _parentState))
try pushNewRecursionContext(_localctx, _startState, ShellParser.RULE_cmd)
setState(95)
if (!(precpred(_ctx, 5))) {
throw ANTLRException.recognition(e:FailedPredicateException(self, "precpred(_ctx, 5)"))
}
setState(99)
try _errHandler.sync(self)
_la = try _input.LA(1)
while (_la == ShellParser.Tokens.NL.rawValue) {
setState(96)
try match(ShellParser.Tokens.NL.rawValue)
setState(101)
try _errHandler.sync(self)
_la = try _input.LA(1)
}
setState(102)
try match(ShellParser.Tokens.AND.rawValue)
setState(103)
try cmd(6)
break
case 3:
_localctx = OrContext( CmdContext(_parentctx, _parentState))
try pushNewRecursionContext(_localctx, _startState, ShellParser.RULE_cmd)
setState(104)
if (!(precpred(_ctx, 4))) {
throw ANTLRException.recognition(e:FailedPredicateException(self, "precpred(_ctx, 4)"))
}
setState(108)
try _errHandler.sync(self)
_la = try _input.LA(1)
while (_la == ShellParser.Tokens.NL.rawValue) {
setState(105)
try match(ShellParser.Tokens.NL.rawValue)
setState(110)
try _errHandler.sync(self)
_la = try _input.LA(1)
}
setState(111)
try match(ShellParser.Tokens.OR.rawValue)
setState(112)
try cmd(5)
break
default: break
}
}
setState(117)
try _errHandler.sync(self)
_alt = try getInterpreter().adaptivePredict(_input,16,_ctx)
}
}
catch ANTLRException.recognition(let re) {
_localctx.exception = re
_errHandler.reportError(self, re)
try _errHandler.recover(self, re)
}
return _localctx;
}
public class ArgContext: ParserRuleContext {
override open
func getRuleIndex() -> Int {
return ShellParser.RULE_arg
}
}
public class WordContext: ArgContext {
open
func ARG() -> TerminalNode? {
return getToken(ShellParser.Tokens.ARG.rawValue, 0)
}
open
func WORD() -> TerminalNode? {
return getToken(ShellParser.Tokens.WORD.rawValue, 0)
}
public
init(_ ctx: ArgContext) {
super.init()
copyFrom(ctx)
}
}
public class SubstitutionContext: ArgContext {
open
func INTERPOLATION_START() -> TerminalNode? {
return getToken(ShellParser.Tokens.INTERPOLATION_START.rawValue, 0)
}
open
func cmds() -> CmdsContext? {
return getRuleContext(CmdsContext.self, 0)
}
open
func RPAR() -> TerminalNode? {
return getToken(ShellParser.Tokens.RPAR.rawValue, 0)
}
public
init(_ ctx: ArgContext) {
super.init()
copyFrom(ctx)
}
}
public class ArgErrorContext: ArgContext {
open
func LDQUOTE() -> TerminalNode? {
return getToken(ShellParser.Tokens.LDQUOTE.rawValue, 0)
}
open
func RDQUOTE() -> [TerminalNode] {
return getTokens(ShellParser.Tokens.RDQUOTE.rawValue)
}
open
func RDQUOTE(_ i:Int) -> TerminalNode? {
return getToken(ShellParser.Tokens.RDQUOTE.rawValue, i)
}
open
func dStringFragment() -> [DStringFragmentContext] {
return getRuleContexts(DStringFragmentContext.self)
}
open
func dStringFragment(_ i: Int) -> DStringFragmentContext? {
return getRuleContext(DStringFragmentContext.self, i)
}
open
func INTERPOLATION_START() -> TerminalNode? {
return getToken(ShellParser.Tokens.INTERPOLATION_START.rawValue, 0)
}
open
func cmds() -> CmdsContext? {
return getRuleContext(CmdsContext.self, 0)
}
open
func RPAR() -> [TerminalNode] {
return getTokens(ShellParser.Tokens.RPAR.rawValue)
}
open
func RPAR(_ i:Int) -> TerminalNode? {
return getToken(ShellParser.Tokens.RPAR.rawValue, i)
}
public
init(_ ctx: ArgContext) {
super.init()
copyFrom(ctx)
}
}
public class SQuotedStringContext: ArgContext {
open
func SINGLE_QUOTED_STRING() -> TerminalNode? {
return getToken(ShellParser.Tokens.SINGLE_QUOTED_STRING.rawValue, 0)
}
public
init(_ ctx: ArgContext) {
super.init()
copyFrom(ctx)
}
}
public class DQuotedStringContext: ArgContext {
open
func LDQUOTE() -> TerminalNode? {
return getToken(ShellParser.Tokens.LDQUOTE.rawValue, 0)
}
open
func RDQUOTE() -> TerminalNode? {
return getToken(ShellParser.Tokens.RDQUOTE.rawValue, 0)
}
open
func dStringFragment() -> [DStringFragmentContext] {
return getRuleContexts(DStringFragmentContext.self)
}
open
func dStringFragment(_ i: Int) -> DStringFragmentContext? {
return getRuleContext(DStringFragmentContext.self, i)
}
public
init(_ ctx: ArgContext) {
super.init()
copyFrom(ctx)
}
}
@discardableResult
open func arg() throws -> ArgContext {
var _localctx: ArgContext
_localctx = ArgContext(_ctx, getState())
try enterRule(_localctx, 6, ShellParser.RULE_arg)
var _la: Int = 0
defer {
try! exitRule()
}
do {
var _alt:Int
setState(161)
try _errHandler.sync(self)
switch(try getInterpreter().adaptivePredict(_input,20, _ctx)) {
case 1:
_localctx = WordContext(_localctx);
try enterOuterAlt(_localctx, 1)
setState(118)
try match(ShellParser.Tokens.ARG.rawValue)
break
case 2:
_localctx = WordContext(_localctx);
try enterOuterAlt(_localctx, 2)
setState(119)
try match(ShellParser.Tokens.WORD.rawValue)
break
case 3:
_localctx = SQuotedStringContext(_localctx);
try enterOuterAlt(_localctx, 3)
setState(120)
try match(ShellParser.Tokens.SINGLE_QUOTED_STRING.rawValue)
break
case 4:
_localctx = DQuotedStringContext(_localctx);
try enterOuterAlt(_localctx, 4)
setState(121)
try match(ShellParser.Tokens.LDQUOTE.rawValue)
setState(125)
try _errHandler.sync(self)
_la = try _input.LA(1)
while (((Int64(_la) & ~0x3f) == 0 && ((Int64(1) << _la) & 15032385536) != 0)) {
setState(122)
try dStringFragment()
setState(127)
try _errHandler.sync(self)
_la = try _input.LA(1)
}
setState(128)
try match(ShellParser.Tokens.RDQUOTE.rawValue)
break
case 5:
_localctx = SubstitutionContext(_localctx);
try enterOuterAlt(_localctx, 5)
setState(129)
try match(ShellParser.Tokens.INTERPOLATION_START.rawValue)
setState(130)
try cmds()
setState(131)
try match(ShellParser.Tokens.RPAR.rawValue)
break
case 6:
_localctx = ArgErrorContext(_localctx);
try enterOuterAlt(_localctx, 6)
setState(133)
try match(ShellParser.Tokens.LDQUOTE.rawValue)
setState(137)
try _errHandler.sync(self)
_la = try _input.LA(1)
while (((Int64(_la) & ~0x3f) == 0 && ((Int64(1) << _la) & 15032385536) != 0)) {
setState(134)
try dStringFragment()
setState(139)
try _errHandler.sync(self)
_la = try _input.LA(1)
}
setState(140)
try match(ShellParser.Tokens.RDQUOTE.rawValue)
setState(141)
try match(ShellParser.Tokens.RDQUOTE.rawValue)
notifyErrorListeners("Unbalanced quotes")
break
case 7:
_localctx = ArgErrorContext(_localctx);
try enterOuterAlt(_localctx, 7)
setState(143)
try match(ShellParser.Tokens.LDQUOTE.rawValue)
setState(147)
try _errHandler.sync(self)
_alt = try getInterpreter().adaptivePredict(_input,19,_ctx)
while (_alt != 2 && _alt != ATN.INVALID_ALT_NUMBER) {
if ( _alt==1 ) {
setState(144)
try dStringFragment()
}
setState(149)
try _errHandler.sync(self)
_alt = try getInterpreter().adaptivePredict(_input,19,_ctx)
}
notifyErrorListeners("Unbalanced quotes")
break
case 8:
_localctx = ArgErrorContext(_localctx);
try enterOuterAlt(_localctx, 8)
setState(151)
try match(ShellParser.Tokens.INTERPOLATION_START.rawValue)
setState(152)
try cmds()
setState(153)
try match(ShellParser.Tokens.RPAR.rawValue)
setState(154)
try match(ShellParser.Tokens.RPAR.rawValue)
notifyErrorListeners("Unbalanced parenthesis")
break
case 9:
_localctx = ArgErrorContext(_localctx);
try enterOuterAlt(_localctx, 9)
setState(157)
try match(ShellParser.Tokens.INTERPOLATION_START.rawValue)
setState(158)
try cmds()
notifyErrorListeners("Unbalanced parenthesis")
break
default: break
}
}
catch ANTLRException.recognition(let re) {
_localctx.exception = re
_errHandler.reportError(self, re)
try _errHandler.recover(self, re)
}
return _localctx
}
public class DStringFragmentContext: ParserRuleContext {
override open
func getRuleIndex() -> Int {
return ShellParser.RULE_dStringFragment
}
}
public class InterpolationContext: DStringFragmentContext {
open
func INTERPOLATION_START_IN_DSTRING() -> TerminalNode? {
return getToken(ShellParser.Tokens.INTERPOLATION_START_IN_DSTRING.rawValue, 0)
}
open
func cmds() -> CmdsContext? {
return getRuleContext(CmdsContext.self, 0)
}
open
func RPAR() -> TerminalNode? {
return getToken(ShellParser.Tokens.RPAR.rawValue, 0)
}
public
init(_ ctx: DStringFragmentContext) {
super.init()
copyFrom(ctx)
}
}
public class DStringFragmentErrorContext: DStringFragmentContext {
open
func INTERPOLATION_START_IN_DSTRING() -> TerminalNode? {
return getToken(ShellParser.Tokens.INTERPOLATION_START_IN_DSTRING.rawValue, 0)
}
open
func cmds() -> CmdsContext? {
return getRuleContext(CmdsContext.self, 0)
}
open
func RPAR() -> [TerminalNode] {
return getTokens(ShellParser.Tokens.RPAR.rawValue)
}
open
func RPAR(_ i:Int) -> TerminalNode? {
return getToken(ShellParser.Tokens.RPAR.rawValue, i)
}
public
init(_ ctx: DStringFragmentContext) {
super.init()
copyFrom(ctx)
}
}
public class EscapeSequenceContext: DStringFragmentContext {
open
func ESCAPE_SEQUENCE() -> TerminalNode? {
return getToken(ShellParser.Tokens.ESCAPE_SEQUENCE.rawValue, 0)
}
public
init(_ ctx: DStringFragmentContext) {
super.init()
copyFrom(ctx)
}
}
public class TextContext: DStringFragmentContext {
open
func TEXT() -> TerminalNode? {
return getToken(ShellParser.Tokens.TEXT.rawValue, 0)
}
public
init(_ ctx: DStringFragmentContext) {
super.init()
copyFrom(ctx)
}
}
@discardableResult
open func dStringFragment() throws -> DStringFragmentContext {
var _localctx: DStringFragmentContext
_localctx = DStringFragmentContext(_ctx, getState())
try enterRule(_localctx, 8, ShellParser.RULE_dStringFragment)
defer {
try! exitRule()
}
do {
setState(179)
try _errHandler.sync(self)
switch(try getInterpreter().adaptivePredict(_input,21, _ctx)) {
case 1:
_localctx = TextContext(_localctx);
try enterOuterAlt(_localctx, 1)
setState(163)
try match(ShellParser.Tokens.TEXT.rawValue)
break
case 2:
_localctx = EscapeSequenceContext(_localctx);
try enterOuterAlt(_localctx, 2)
setState(164)
try match(ShellParser.Tokens.ESCAPE_SEQUENCE.rawValue)
break
case 3:
_localctx = InterpolationContext(_localctx);
try enterOuterAlt(_localctx, 3)
setState(165)
try match(ShellParser.Tokens.INTERPOLATION_START_IN_DSTRING.rawValue)
setState(166)
try cmds()
setState(167)
try match(ShellParser.Tokens.RPAR.rawValue)
break
case 4:
_localctx = DStringFragmentErrorContext(_localctx);
try enterOuterAlt(_localctx, 4)
setState(169)
try match(ShellParser.Tokens.INTERPOLATION_START_IN_DSTRING.rawValue)
setState(170)
try cmds()
setState(171)
try match(ShellParser.Tokens.RPAR.rawValue)
setState(172)
try match(ShellParser.Tokens.RPAR.rawValue)
notifyErrorListeners("Unbalanced parenthesis")
break
case 5:
_localctx = DStringFragmentErrorContext(_localctx);
try enterOuterAlt(_localctx, 5)
setState(175)
try match(ShellParser.Tokens.INTERPOLATION_START_IN_DSTRING.rawValue)
setState(176)
try cmds()
notifyErrorListeners("Unbalanced parenthesis")
break
default: break
}
}
catch ANTLRException.recognition(let re) {
_localctx.exception = re
_errHandler.reportError(self, re)
try _errHandler.recover(self, re)
}
return _localctx
}
override open
func sempred(_ _localctx: RuleContext?, _ ruleIndex: Int, _ predIndex: Int)throws -> Bool {
switch (ruleIndex) {
case 2:
return try cmd_sempred(_localctx?.castdown(CmdContext.self), predIndex)
default: return true
}
}
private func cmd_sempred(_ _localctx: CmdContext!, _ predIndex: Int) throws -> Bool {
switch (predIndex) {
case 0:return precpred(_ctx, 6)
case 1:return precpred(_ctx, 5)
case 2:return precpred(_ctx, 4)
default: return true
}
}
static let _serializedATN:[Int] = [
4,1,34,182,2,0,7,0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,1,0,5,0,12,8,0,10,0,
12,0,15,9,0,1,0,1,0,1,0,1,0,3,0,21,8,0,1,1,1,1,1,1,1,1,3,1,27,8,1,1,1,
1,1,1,1,1,1,3,1,33,8,1,5,1,35,8,1,10,1,12,1,38,9,1,1,1,1,1,3,1,42,8,1,
3,1,44,8,1,1,1,1,1,1,1,1,1,4,1,50,8,1,11,1,12,1,51,1,1,5,1,55,8,1,10,1,
12,1,58,9,1,1,1,3,1,61,8,1,1,2,1,2,1,2,5,2,66,8,2,10,2,12,2,69,9,2,1,2,
1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2,3,2,85,8,2,1,2,1,2,
5,2,89,8,2,10,2,12,2,92,9,2,1,2,1,2,1,2,1,2,5,2,98,8,2,10,2,12,2,101,9,
2,1,2,1,2,1,2,1,2,5,2,107,8,2,10,2,12,2,110,9,2,1,2,1,2,5,2,114,8,2,10,
2,12,2,117,9,2,1,3,1,3,1,3,1,3,1,3,5,3,124,8,3,10,3,12,3,127,9,3,1,3,1,
3,1,3,1,3,1,3,1,3,1,3,5,3,136,8,3,10,3,12,3,139,9,3,1,3,1,3,1,3,1,3,1,
3,5,3,146,8,3,10,3,12,3,149,9,3,1,3,1,3,1,3,1,3,1,3,1,3,1,3,1,3,1,3,1,
3,1,3,3,3,162,8,3,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,
1,4,1,4,1,4,3,4,180,8,4,1,4,1,56,1,4,5,0,2,4,6,8,0,1,1,0,23,24,212,0,13,
1,0,0,0,2,60,1,0,0,0,4,84,1,0,0,0,6,161,1,0,0,0,8,179,1,0,0,0,10,12,5,
24,0,0,11,10,1,0,0,0,12,15,1,0,0,0,13,11,1,0,0,0,13,14,1,0,0,0,14,20,1,
0,0,0,15,13,1,0,0,0,16,17,3,2,1,0,17,18,5,0,0,1,18,21,1,0,0,0,19,21,5,
0,0,1,20,16,1,0,0,0,20,19,1,0,0,0,21,1,1,0,0,0,22,23,5,8,0,0,23,24,3,4,
2,0,24,26,5,12,0,0,25,27,3,2,1,0,26,25,1,0,0,0,26,27,1,0,0,0,27,36,1,0,
0,0,28,29,5,7,0,0,29,30,3,4,2,0,30,32,5,12,0,0,31,33,3,2,1,0,32,31,1,0,
0,0,32,33,1,0,0,0,33,35,1,0,0,0,34,28,1,0,0,0,35,38,1,0,0,0,36,34,1,0,
0,0,36,37,1,0,0,0,37,43,1,0,0,0,38,36,1,0,0,0,39,41,5,13,0,0,40,42,3,2,
1,0,41,40,1,0,0,0,41,42,1,0,0,0,42,44,1,0,0,0,43,39,1,0,0,0,43,44,1,0,
0,0,44,45,1,0,0,0,45,46,5,18,0,0,46,61,1,0,0,0,47,49,3,4,2,0,48,50,7,0,
0,0,49,48,1,0,0,0,50,51,1,0,0,0,51,49,1,0,0,0,51,52,1,0,0,0,52,56,1,0,
0,0,53,55,3,2,1,0,54,53,1,0,0,0,55,58,1,0,0,0,56,57,1,0,0,0,56,54,1,0,
0,0,57,61,1,0,0,0,58,56,1,0,0,0,59,61,3,4,2,0,60,22,1,0,0,0,60,47,1,0,
0,0,60,59,1,0,0,0,61,3,1,0,0,0,62,63,6,2,-1,0,63,67,5,25,0,0,64,66,3,6,
3,0,65,64,1,0,0,0,66,69,1,0,0,0,67,65,1,0,0,0,67,68,1,0,0,0,68,85,1,0,
0,0,69,67,1,0,0,0,70,71,5,4,0,0,71,72,3,2,1,0,72,73,5,6,0,0,73,85,1,0,
0,0,74,75,5,4,0,0,75,76,3,2,1,0,76,77,5,6,0,0,77,78,5,6,0,0,78,79,6,2,
-1,0,79,85,1,0,0,0,80,81,5,4,0,0,81,82,3,2,1,0,82,83,6,2,-1,0,83,85,1,
0,0,0,84,62,1,0,0,0,84,70,1,0,0,0,84,74,1,0,0,0,84,80,1,0,0,0,85,115,1,
0,0,0,86,90,10,6,0,0,87,89,5,24,0,0,88,87,1,0,0,0,89,92,1,0,0,0,90,88,
1,0,0,0,90,91,1,0,0,0,91,93,1,0,0,0,92,90,1,0,0,0,93,94,5,21,0,0,94,114,
3,4,2,7,95,99,10,5,0,0,96,98,5,24,0,0,97,96,1,0,0,0,98,101,1,0,0,0,99,
97,1,0,0,0,99,100,1,0,0,0,100,102,1,0,0,0,101,99,1,0,0,0,102,103,5,20,
0,0,103,114,3,4,2,6,104,108,10,4,0,0,105,107,5,24,0,0,106,105,1,0,0,0,
107,110,1,0,0,0,108,106,1,0,0,0,108,109,1,0,0,0,109,111,1,0,0,0,110,108,
1,0,0,0,111,112,5,22,0,0,112,114,3,4,2,5,113,86,1,0,0,0,113,95,1,0,0,0,
113,104,1,0,0,0,114,117,1,0,0,0,115,113,1,0,0,0,115,116,1,0,0,0,116,5,
1,0,0,0,117,115,1,0,0,0,118,162,5,26,0,0,119,162,5,25,0,0,120,162,5,2,
0,0,121,125,5,3,0,0,122,124,3,8,4,0,123,122,1,0,0,0,124,127,1,0,0,0,125,
123,1,0,0,0,125,126,1,0,0,0,126,128,1,0,0,0,127,125,1,0,0,0,128,162,5,
34,0,0,129,130,5,5,0,0,130,131,3,2,1,0,131,132,5,6,0,0,132,162,1,0,0,0,
133,137,5,3,0,0,134,136,3,8,4,0,135,134,1,0,0,0,136,139,1,0,0,0,137,135,
1,0,0,0,137,138,1,0,0,0,138,140,1,0,0,0,139,137,1,0,0,0,140,141,5,34,0,
0,141,142,5,34,0,0,142,162,6,3,-1,0,143,147,5,3,0,0,144,146,3,8,4,0,145,
144,1,0,0,0,146,149,1,0,0,0,147,145,1,0,0,0,147,148,1,0,0,0,148,150,1,
0,0,0,149,147,1,0,0,0,150,162,6,3,-1,0,151,152,5,5,0,0,152,153,3,2,1,0,
153,154,5,6,0,0,154,155,5,6,0,0,155,156,6,3,-1,0,156,162,1,0,0,0,157,158,
5,5,0,0,158,159,3,2,1,0,159,160,6,3,-1,0,160,162,1,0,0,0,161,118,1,0,0,
0,161,119,1,0,0,0,161,120,1,0,0,0,161,121,1,0,0,0,161,129,1,0,0,0,161,
133,1,0,0,0,161,143,1,0,0,0,161,151,1,0,0,0,161,157,1,0,0,0,162,7,1,0,
0,0,163,180,5,31,0,0,164,180,5,33,0,0,165,166,5,32,0,0,166,167,3,2,1,0,
167,168,5,6,0,0,168,180,1,0,0,0,169,170,5,32,0,0,170,171,3,2,1,0,171,172,
5,6,0,0,172,173,5,6,0,0,173,174,6,4,-1,0,174,180,1,0,0,0,175,176,5,32,
0,0,176,177,3,2,1,0,177,178,6,4,-1,0,178,180,1,0,0,0,179,163,1,0,0,0,179,
164,1,0,0,0,179,165,1,0,0,0,179,169,1,0,0,0,179,175,1,0,0,0,180,9,1,0,
0,0,22,13,20,26,32,36,41,43,51,56,60,67,84,90,99,108,113,115,125,137,147,
161,179
]
public
static let _ATN = try! ATNDeserializer().deserialize(_serializedATN)
}
```
## /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 { @MainActor in
if !TrayMenuModel.shared.isEnabled { return }
if notifName == NSWorkspace.didActivateApplicationNotification.rawValue {
runRefreshSession(.globalObserver(notifName), optimisticallyPreLayoutWorkspaces: true)
} else {
runRefreshSession(.globalObserver(notifName))
}
}
}
private static func onHideApp(_ notification: Notification) {
let notifName = notification.name.rawValue
Task { @MainActor in
guard let token: RunSessionGuard = .isServerEnabled else { return }
try await runSession(.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 { @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.activeWorkspace != focus.workspace:
_ = try await runSession(.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:
runRefreshSession(.globalObserverLeftMouseUp)
}
}
}
}
}
```
## /Sources/AppBundle/command/CmdEnv.swift
```swift path="/Sources/AppBundle/command/CmdEnv.swift"
import Common
struct CmdEnv: ConvenienceCopyable { // todo forward env from cli to server
var windowId: UInt32?
var workspaceName: String?
var pwd: String?
static var defaultEnv: CmdEnv { CmdEnv(windowId: nil, workspaceName: nil, pwd: nil) }
func withFocus(_ focus: LiveFocus) -> CmdEnv {
switch focus.asLeaf {
case .window(let wd): .defaultEnv.copy(\.windowId, wd.windowId)
case .emptyWorkspace(let ws): .defaultEnv.copy(\.workspaceName, ws.name)
}
}
@MainActor
var asMap: [String: String] {
var result = config.execConfig.envVariables
if let pwd {
result["PWD"] = pwd
}
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"
final class CmdStdin {
private var input: String = ""
init(_ input: String) {
self.input = input
}
static var emptyStdin: CmdStdin { .init("") }
func readAll() -> String {
let result = input
input = ""
return result
}
}
final class CmdIo {
private var stdin: CmdStdin
var stdout: [String] = []
var stderr: [String] = []
init(stdin: CmdStdin) { self.stdin = stdin }
@discardableResult func out(_ msg: String) -> Bool { stdout.append(msg); return true }
@discardableResult func err(_ msg: String) -> Bool { stderr.append(msg); return false }
@discardableResult func out(_ msg: [String]) -> Bool { stdout += msg; return true }
@discardableResult func err(_ msg: [String]) -> Bool { stderr += msg; return false }
func readStdin() -> String { stdin.readAll() }
}
struct CmdResult {
let stdout: [String]
let stderr: [String]
let exitCode: Int32
}
```
## /Sources/AppBundle/command/Command.swift
```swift path="/Sources/AppBundle/command/Command.swift"
import AppKit
import Common
protocol Command: AeroAny, Equatable, Sendable {
associatedtype T where T: CmdArgs
var args: T { get }
@MainActor
func run(_ env: CmdEnv, _ io: CmdIo) async throws -> Bool
/// We should reset closedWindowsCache when the command can potentiall 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: CmdStdin) async throws -> CmdResult {
return try await [self].runCmdSeq(env, stdin)
}
var isExec: Bool { self is ExecAndForgetCommand }
}
// There are 4 entry points for running commands:
// 1. config keybindings
// 2. CLI requests to server
// 3. on-window-detected callback
// 4. Tray icon buttons
extension [Command] {
@MainActor
func runCmdSeq(_ env: CmdEnv, _ io: sending CmdIo) async throws -> Bool {
var isSucc = true
for command in self {
isSucc = try await command.run(env, io) && isSucc
if command.shouldResetClosedWindowsCache { resetClosedWindowsCache() }
refreshModel()
}
return isSucc
}
@MainActor
func runCmdSeq(_ env: CmdEnv, _ stdin: CmdStdin) async throws -> CmdResult {
let io: CmdIo = CmdIo(stdin: stdin)
let isSucc = try await runCmdSeq(env, io)
return CmdResult(stdout: io.stdout, stderr: io.stderr, exitCode: isSucc ? 0 : 1)
}
}
```
## /Sources/AppBundle/command/cmdManifest.swift
```swift path="/Sources/AppBundle/command/cmdManifest.swift"
import Common
extension CmdArgs {
func toCommand() -> any Command {
let command: any Command
switch Self.info.kind {
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 .enable:
command = EnableCommand(args: self as! EnableCmdArgs)
case .execAndForget:
die("exec-and-forget is parsed separately")
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 .split:
command = SplitCommand(args: self as! SplitCmdArgs)
case .summonWorkspace:
command = SummonWorkspaceCommand(args: self as! SummonWorkspaceCmdArgs)
case .swap:
command = SwapCommand(args: self as! SwapCmdArgs)
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 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
enum AeroObj {
case window(window: Window, title: String)
case workspace(Workspace)
case app(any AbstractApp)
case monitor(Monitor)
var kind: AeroObjKind {
switch self {
case .window: .window
case .workspace: .workspace
case .app: .app
case .monitor: .monitor
}
}
}
extension [AeroObj] {
@MainActor
func format(_ format: [StringInterToken]) -> Result<[String], String> {
var cellTable: [[Cell<String>]] = []
for obj in self {
var line: [Cell<String>] = []
var curCell: String = ""
var errors: [String] = []
for token in format {
switch token {
case .interVar(PlainInterVar.rightPadding.rawValue):
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.joinErrors()) }
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(Int)
case int32(Int32)
case uint32(UInt32)
case string(String)
func toString() -> String {
switch self {
case .bool(let x): x.description
case .int(let x): x.description
case .int32(let x): x.description
case .uint32(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 .int32(let x): x
case .uint32(let x): x
case .string(let x): x
}
var container = encoder.singleValueContainer()
try container.encode(value)
}
}
private struct Cell<T> {
let value: T
let rightPadding: Bool
}
extension String {
@MainActor
func expandFormatVar(obj: AeroObj) -> Result<Primitive, String> {
let formatVar = self.toFormatVar()
switch (obj, formatVar) {
case (_, .none): break
case (.window(let w, _), .workspace):
return w.nodeWorkspace.flatMap(AeroObj.workspace).map(expandFormatVar) ?? .success(.string("NULL-WOKRSPACE"))
case (.window(let w, _), .monitor):
return w.nodeMonitor.flatMap(AeroObj.monitor).map(expandFormatVar) ?? .success(.string("NULL-MONITOR"))
case (.window(let w, _), .app):
return expandFormatVar(obj: .app(w.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, formatVar) {
case (.window(let w, let title), .window(let f)):
return switch f {
case .windowId: .success(.uint32(w.windowId))
case .windowIsFullscreen: .success(.bool(w.isFullscreen))
case .windowTitle: .success(.string(title))
case .windowLayout, .windowParentContainerLayout: toLayoutResult(w: w)
}
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: .success(m.monitorId.map { .int($0 + 1) } ?? .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(.int32(a.pid))
case .appExecPath: .success(.string(a.execPath ?? "NULL-APP-EXEC-PATH"))
case .appBundlePath: .success(.string(a.bundlePath ?? "NULL-APP-BUNDLE-PATH"))
}
default: break
}
if self == PlainInterVar.newline.rawValue { return .success(.string("\n")) }
if self == PlainInterVar.tab.rawValue { return .success(.string("\t")) }
return .failure("Unknown interpolation variable '\(self)'. " +
"Possible values:\n\(getAvailableInterVars(for: obj.kind).joined(separator: "\n").prependLines(" "))")
}
private func toFormatVar() -> FormatVar? {
FormatVar.WindowFormatVar(rawValue: self).flatMap(FormatVar.window)
?? FormatVar.WorkspaceFormatVar(rawValue: self).flatMap(FormatVar.workspace)
?? FormatVar.AppFormatVar(rawValue: self).flatMap(FormatVar.app)
?? FormatVar.MonitorFormatVar(rawValue: self).flatMap(FormatVar.monitor)
}
}
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, String> {
guard let parent = w.parent else { return .failure("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("Not possible")
case .shimContainerRelation: .failure("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: [StringInterToken], 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.rawValue) where ignoreRightPaddingVar:
break
case .literal:
break // should be spaces
case .interVar(let varName):
switch varName.expandFormatVar(obj: richObj) {
case .success(let expanded): rawObj[varName] = expanded
case .failure(let error): return .failure(error)
}
}
}
list.append(rawObj)
}
return JSONEncoder.aeroSpaceDefault.encodeToString(list).map(Result.success)
?? .failure("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*/ var shouldResetClosedWindowsCache = false
func run(_ env: CmdEnv, _ io: CmdIo) -> Bool {
guard let target = args.resolveTargetOrReportError(env, io) else { return false }
balance(target.workspace.rootTilingContainer)
return true
}
}
@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*/ var shouldResetClosedWindowsCache = false
func run(_ env: CmdEnv, _ io: CmdIo) async throws -> Bool {
guard let target = args.resolveTargetOrReportError(env, io) else { return false }
guard let focused = target.windowOrNil else {
return io.err("Empty workspace")
}
guard let workspace = focused.nodeWorkspace else {
return io.err("Focused window '\(focused.windowId)' doesn't belong to workspace")
}
var result = true
for window in workspace.allLeafWindowsRecursive where window != focused {
result = try await CloseCommand(args: args.closeArgs).run(env.copy(\.windowId, window.windowId), io) && 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*/ var shouldResetClosedWindowsCache = false
func run(_ env: CmdEnv, _ io: CmdIo) async throws -> Bool {
try await allowOnlyCancellationError {
guard let target = args.resolveTargetOrReportError(env, io) else { return false }
guard let window = target.windowOrNil else {
return io.err("Empty workspace")
}
// Access ax directly. Not cool :(
if try await args.quitIfLastWindow.andAsync({ @MainActor @Sendable in try await window.macAppUnsafe.getAxWindowsCount() == 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 true
} else {
return io.err("Failed to quit '\(window.app.name ?? "Unknown app")'")
}
} else {
window.closeAxWindow()
return true
}
}
}
}
```
## /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*/ var shouldResetClosedWindowsCache = false
func run(_ env: CmdEnv, _ io: CmdIo) -> Bool {
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 io.out(out)
case .allKeys:
let configMap = buildConfigMap()
var allKeys: [String] = []
configMap.dumpAllKeysRecursive(path: ".", result: &allKeys)
return io.out(allKeys.joined(separator: "\n"))
case .configPath:
return 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) -> Bool {
let keyPath: [String]
switch key.toKeyPath() {
case .success(let _keyPath): keyPath = _keyPath
case .failure(let error):
return io.err(error)
}
var configMap: ConfigMapValue
switch buildConfigMap().find(keyPath: keyPath.slice) {
case .success(let value):
configMap = value
case .failure(let error):
return io.err(error)
}
if args.keys {
switch configMap {
case .scalar(let scalar):
return 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 {
if let json = JSONEncoder.aeroSpaceDefault.encodeToString(configMap) {
return io.out(json)
} else {
return io.err("Can't convert json Data to String")
}
} else {
switch configMap {
case .scalar(let scalar):
return io.out(scalar.describe)
case .map:
return io.err("Complicated objects can be printed only with --json flag. " +
"Alternatively, you can try to inspect keys of the object with --keys flag")
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): io.out(array.sorted().joined(separator: "\n"))
case .failure(let error): 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):
if let child = map[key] {
return child.find(keyPath: keyPath.slice(1...).orDie())
} else {
return .failure("No value at key token '\(key)'")
}
case .array(let array):
if let key = Int(key) {
if let child = array.getOrNil(atIndex: key) {
return child.find(keyPath: keyPath.slice(1...).orDie())
} else {
return .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 [Command] {
var prettyDescription: String {
map { $0.args.description }.joined(separator: "; ")
}
}
@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.prettyDescription))
}
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*/ var shouldResetClosedWindowsCache = false
func run(_ env: CmdEnv, _ io: CmdIo) async throws -> Bool {
if let windowId = args.windowId {
guard let window = Window.get(byId: windowId) else {
return io.err("Can't find window with the specified window-id: \(windowId)")
}
io.out(try await dumpWindowDebugInfo(window) + "\n")
io.out(disclaimer)
return true
}
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 true
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 false }
if let window = target.windowOrNil {
try await debugWindowsIfRecording(window)
}
return true
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 false
}
}
}
@MainActor
private func dumpWindowDebugInfo(_ window: Window) 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()
result["Aero.axWindowId"] = .uint32(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())
let isDialog = try await window.isDialogHeuristic()
let isWindow = try await window.isWindowHeuristic()
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 try 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) 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)
}
```
## /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*/ var shouldResetClosedWindowsCache = false
func run(_ env: CmdEnv, _ io: CmdIo) async throws -> Bool {
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 {
io.out((newState ? "Already enabled" : "Already disabled") +
"Tip: use --fail-if-noop to exit with non-zero code")
return !args.failIfNoop
}
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() ?? window.lastFloatingSize
}
}
activateMode(mainModeId)
} else {
activateMode(nil)
}
return true
}
}
```
## /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*/ var shouldResetClosedWindowsCache = false
func run(_ env: CmdEnv, _ io: CmdIo) -> Bool {
// 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
process.executableURL = URL(filePath: "/bin/bash")
process.arguments = ["-c", args.bashScript]
return Result { try process.run() }.isSuccess
}
}
```
## /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) -> Bool {
guard let target = args.resolveTargetOrReportError(env, io) else { return false }
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 true
}
}
```
## /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*/ var shouldResetClosedWindowsCache = false
func run(_ env: CmdEnv, _ io: CmdIo) -> Bool {
if let prevFocus {
return setFocus(to: prevFocus)
} else {
return 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*/ var shouldResetClosedWindowsCache = false
func run(_ env: CmdEnv, _ io: CmdIo) async throws -> Bool {
guard let target = args.resolveTargetOrReportError(env, io) else { return false }
// todo bug: floating windows break mru
let floatingWindows = args.floatingAsTiling ? try 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 false }
return windowToFocus.focusWindow()
} else {
return hitWorkspaceBoundaries(target, io, args, direction)
}
case .windowId(let windowId):
if let windowToFocus = Window.get(byId: windowId) {
return windowToFocus.focusWindow()
} else {
return 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 windowToFocus.focusWindow()
} else {
return 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 false
}
var targetIndex = switch nextPrev {
case .dfsNext: currentIndex + 1
case .dfsPrev: currentIndex - 1
}
if !(0 ..< windows.count).contains(targetIndex) {
switch args.boundariesAction {
case .stop: return true
case .fail: return false
case .wrapAroundTheWorkspace: targetIndex = (targetIndex + windows.count) % windows.count
case .wrapAroundAllMonitors: return dieT("Must be discarded by args parser")
}
}
return windows[targetIndex].focusWindow()
}
}
}
@MainActor private func hitWorkspaceBoundaries(
_ target: LiveFocus,
_ io: CmdIo,
_ args: FocusCmdArgs,
_ direction: CardinalDirection,
) -> Bool {
switch args.boundaries {
case .workspace:
return switch args.boundariesAction {
case .stop: true
case .fail: false
case .wrapAroundTheWorkspace: wrapAroundTheWorkspace(target, io, direction)
case .wrapAroundAllMonitors: dieT("Must be discarded by args parser")
}
case .allMonitorsOuterFrame:
let currentMonitor = target.workspace.workspaceMonitor
guard let (monitors, index) = currentMonitor.findRelativeMonitor(inDirection: direction) else {
return io.err("Should never happen. Can't find the current monitor")
}
if let targetMonitor = monitors.getOrNil(atIndex: index) {
return targetMonitor.activeWorkspace.focusWorkspace()
} else {
guard let wrapped = monitors.get(wrappingIndex: index) else { return false }
return hitAllMonitorsOuterFrameBoundaries(target, io, args, direction, wrapped)
}
}
}
@MainActor private func hitAllMonitorsOuterFrameBoundaries(
_ target: LiveFocus,
_ io: CmdIo,
_ args: FocusCmdArgs,
_ direction: CardinalDirection,
_ wrappedMonitor: Monitor,
) -> Bool {
switch args.boundariesAction {
case .stop:
return true
case .fail:
return false
case .wrapAroundTheWorkspace:
return wrapAroundTheWorkspace(target, io, direction)
case .wrapAroundAllMonitors:
wrappedMonitor.activeWorkspace.findLeafWindowRecursive(snappedTo: direction.opposite)?.markAsMostRecentChild()
return wrappedMonitor.activeWorkspace.focusWorkspace()
}
}
@MainActor private func wrapAroundTheWorkspace(_ target: LiveFocus, _ io: CmdIo, _ direction: CardinalDirection) -> Bool {
guard let windowToFocus = target.workspace.findLeafWindowRecursive(snappedTo: direction.opposite) else {
return io.err(noWindowIsFocused)
}
return windowToFocus.focusWindow()
}
@MainActor private func makeFloatingWindowsSeenAsTiling(workspace: Workspace) async throws -> [FloatingWindowData] {
let mruBefore = workspace.mostRecentWindowRecursive
defer {
mruBefore?.markAsMostRecentChild()
}
var _floatingWindows: [FloatingWindowData] = []
for window in workspace.floatingWindows {
let center = try await window.getCenter() // 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 else { continue }
// todo bug: what if there are no tiling windows on the workspace?
guard let target = center.coerceIn(rect: workspace.workspaceMonitor.visibleRectPaddedByOuterGaps)?
.findIn(tree: workspace.rootTilingContainer, virtual: true) else { continue }
guard let targetCenter = try await target.getCenter() else { continue }
guard let tilingParent = target.parent as? TilingContainer else { continue }
let index = center.getProjection(tilingParent.orientation) >= targetCenter.getProjection(tilingParent.orientation)
? target.ownIndex.orDie() + 1
: target.ownIndex.orDie()
let data = window.unbindFromParent()
_floatingWindows.append(FloatingWindowData(window: window, center: center, parent: tilingParent, adaptiveWeight: data.adaptiveWeight, index: index))
}
let floatingWindows: [FloatingWindowData] = _floatingWindows.sortedBy { $0.center.getProjection($0.parent.orientation) }.reversed()
for floating in floatingWindows { // Make floating windows be seen as tiling
floating.window.bind(to: floating.parent, 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, adaptiveWeight: floating.adaptiveWeight, index: INDEX_BIND_LAST)
}
}
private struct FloatingWindowData {
let window: Window
let center: CGPoint
let parent: 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:
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*/ var shouldResetClosedWindowsCache = false
func run(_ env: CmdEnv, _ io: CmdIo) -> Bool {
guard let target = args.resolveTargetOrReportError(env, io) else { return false }
return switch args.target.val.resolve(target.workspace.workspaceMonitor, wrapAround: args.wrapAround) {
case .success(let targetMonitor): targetMonitor.activeWorkspace.focusWorkspace()
case .failure(let msg): io.err(msg)
}
}
}
extension MonitorTarget {
func resolve(_ currentMonitor: Monitor, wrapAround: Bool) -> Result<Monitor, 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 = sortedMonitors
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 = sortedMonitors
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 Monitor {
func relation(to monitor: Monitor) -> 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: [Monitor], index: Int)? {
let currentMonitor = self
let monitors = sortedMonitors.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*/ var shouldResetClosedWindowsCache = false
func run(_ env: CmdEnv, _ io: CmdIo) -> Bool {
guard let target = args.resolveTargetOrReportError(env, io) else { return false }
guard let window = target.windowOrNil else {
return io.err(noWindowIsFocused)
}
let newState: Bool = switch args.toggle {
case .on: true
case .off: false
case .toggle: !window.isFullscreen
}
if newState == window.isFullscreen {
io.err((newState ? "Already fullscreen. " : "Already not fullscreen. ") +
"Tip: use --fail-if-noop to exit with non-zero code")
return !args.failIfNoop
}
window.isFullscreen = newState
window.noOuterGapsInFullscreen = args.noOuterGaps
// Focus on its own workspace
window.markAsMostRecentChild()
return true
}
}
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*/ var shouldResetClosedWindowsCache = true
func run(_ env: CmdEnv, _ io: CmdIo) -> Bool {
let direction = args.direction.val
guard let target = args.resolveTargetOrReportError(env, io) else { return false }
guard let currentWindow = target.windowOrNil else {
return io.err(noWindowIsFocused)
}
guard let (parent, ownIndex) = currentWindow.closestParent(hasChildrenInDirection: direction, withLayout: nil) else {
return 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 true
}
}
```
## /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*/ var shouldResetClosedWindowsCache = true
func run(_ env: CmdEnv, _ io: CmdIo) async throws -> Bool {
guard let target = args.resolveTargetOrReportError(env, io) else { return false }
guard let window = target.windowOrNil else {
return io.err(noWindowIsFocused)
}
let targetDescription = args.toggleBetween.val.first(where: { !window.matchesDescription($0) })
?? args.toggleBetween.val.first.orDie()
if window.matchesDescription(targetDescription) { return false }
switch targetDescription {
case .h_accordion:
return changeTilingLayout(io, targetLayout: .accordion, targetOrientation: .h, window: window)
case .v_accordion:
return changeTilingLayout(io, targetLayout: .accordion, targetOrientation: .v, window: window)
case .h_tiles:
return changeTilingLayout(io, targetLayout: .tiles, targetOrientation: .h, window: window)
case .v_tiles:
return changeTilingLayout(io, targetLayout: .tiles, targetOrientation: .v, window: window)
case .accordion:
return changeTilingLayout(io, targetLayout: .accordion, targetOrientation: nil, window: window)
case .tiles:
return changeTilingLayout(io, targetLayout: .tiles, targetOrientation: nil, window: window)
case .horizontal:
return changeTilingLayout(io, targetLayout: nil, targetOrientation: .h, window: window)
case .vertical:
return changeTilingLayout(io, targetLayout: nil, targetOrientation: .v, window: window)
case .tiling:
guard let parent = window.parent else { return false }
switch parent.cases {
case .macosPopupWindowsContainer:
return false // Impossible
case .macosMinimizedWindowsContainer, .macosFullscreenWindowsContainer, .macosHiddenAppsWindowsContainer:
return io.err("Can't change layout for macOS minimized, fullscreen windows or windows or hidden apps. This behavior is subject to change")
case .tilingContainer:
return true // Nothing to do
case .workspace(let workspace):
window.lastFloatingSize = try await window.getAxSize() ?? window.lastFloatingSize
try await window.relayoutWindow(on: workspace, forceTile: true)
return true
}
case .floating:
let workspace = target.workspace
window.bindAsFloatingWindow(to: workspace)
if let size = window.lastFloatingSize { window.setSizeAsync(size) }
return true
}
}
}
@MainActor private func changeTilingLayout(_ io: CmdIo, targetLayout: Layout?, targetOrientation: Orientation?, window: Window) -> Bool {
guard let parent = window.parent else { return false }
switch parent.cases {
case .tilingContainer(let parent):
let targetOrientation = targetOrientation ?? parent.orientation
let targetLayout = targetLayout ?? parent.layout
parent.layout = targetLayout
parent.changeOrientation(targetOrientation)
return true
case .workspace, .macosMinimizedWindowsContainer, .macosFullscreenWindowsContainer,
.macosPopupWindowsContainer, .macosHiddenAppsWindowsContainer:
return io.err("The window is non-tiling")
}
}
extension Window {
fileprivate func matchesDescription(_ layout: LayoutCmdArgs.LayoutDescription) -> Bool {
return switch layout {
case .accordion: (parent as? TilingContainer)?.layout == .accordion
case .tiles: (parent as? TilingContainer)?.layout == .tiles
case .horizontal: (parent as? TilingContainer)?.orientation == .h
case .vertical: (parent as? TilingContainer)?.orientation == .v
case .h_accordion: (parent as? TilingContainer).map { $0.layout == .accordion && $0.orientation == .h } == true
case .v_accordion: (parent as? TilingContainer).map { $0.layout == .accordion && $0.orientation == .v } == true
case .h_tiles: (parent as? TilingContainer).map { $0.layout == .tiles && $0.orientation == .h } == true
case .v_tiles: (parent as? TilingContainer).map { $0.layout == .tiles && $0.orientation == .v } == true
case .tiling: parent is TilingContainer
case .floating: parent is Workspace
}
}
}
```
## /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*/ var shouldResetClosedWindowsCache = false
func run(_ env: CmdEnv, _ io: CmdIo) -> Bool {
var result = Array(MacApp.allAppsMap.values)
if let hidden = args.macosHidden {
result = result.filter { $0.nsApp.isHidden == hidden }
}
if args.outputOnlyCount {
return io.out("\(result.count)")
} else {
let list = result.map { AeroObj.app($0) }
if args.json {
return switch list.formatToJson(args.format, ignoreRightPaddingVar: args._format.isEmpty) {
case .success(let json): io.out(json)
case .failure(let msg): io.err(msg)
}
} else {
return switch list.format(args.format) {
case .success(let lines): io.out(lines)
case .failure(let msg): io.err(msg)
}
}
}
}
}
```
## /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*/ var shouldResetClosedWindowsCache = false
func run(_ env: CmdEnv, _ io: CmdIo) -> Bool {
for (key, value) in config.execConfig.envVariables {
io.out("\(key)=\(value)")
}
return true
}
}
```
## /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*/ var shouldResetClosedWindowsCache = false
func run(_ env: CmdEnv, _ io: CmdIo) -> Bool {
let modes: [String] = args.current ? [activeMode ?? mainModeId] : config.modes.keys.sorted()
return switch true {
case args.outputOnlyCount:
io.out("\(modes.count)")
case args.json:
JSONEncoder.aeroSpaceDefault.encodeToString(modes.map { ["mode-id": $0] }).map(io.out)
?? io.err("Failed to encode JSON")
default:
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*/ var shouldResetClosedWindowsCache = false
func run(_ env: CmdEnv, _ io: CmdIo) -> Bool {
let focus = focus
var result = sortedMonitors
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 }
}
if args.outputOnlyCount {
return io.out("\(result.count)")
} else {
let list = result.map { AeroObj.monitor($0) }
if args.json {
return switch list.formatToJson(args.format, ignoreRightPaddingVar: args._format.isEmpty) {
case .success(let json): io.out(json)
case .failure(let msg): io.err(msg)
}
} else {
return switch list.format(args.format) {
case .success(let lines): io.out(lines)
case .failure(let msg): io.err(msg)
}
}
}
}
}
```
## /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*/ var shouldResetClosedWindowsCache = false
func run(_ env: CmdEnv, _ io: CmdIo) async throws -> Bool {
let focus = focus
var windows: [Window] = []
if args.filteringOptions.focused {
if let window = focus.windowOrNil {
windows = [window]
} else {
return 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 false }
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 io.out("\(windows.count)")
} else {
var _list: [(window: Window, title: String)] = [] // todo cleanup
for window in windows {
_list.append((window, try await window.title))
}
_list = _list.filter { $0.window.isBound }
_list = _list.sortedBy([{ $0.window.app.name ?? "" }, \.title])
let list = _list.map { AeroObj.window(window: $0.window, title: $0.title) }
if args.json {
return switch list.formatToJson(args.format, ignoreRightPaddingVar: args._format.isEmpty) {
case .success(let json): io.out(json)
case .failure(let msg): io.err(msg)
}
} else {
return switch list.format(args.format) {
case .success(let lines): io.out(lines)
case .failure(let msg): io.err(msg)
}
}
}
}
}
```
## /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*/ var shouldResetClosedWindowsCache = false
func run(_ env: CmdEnv, _ io: CmdIo) -> Bool {
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 false }
result = result.filter { monitors.contains($0.workspaceMonitor.rect.topLeftCorner) }
}
if let empty = args.filteringOptions.empty {
result = result.filter { $0.isEffectivelyEmpty == empty }
}
if args.outputOnlyCount {
return io.out("\(result.count)")
} else {
let list = result.map { AeroObj.workspace($0) }
if args.json {
return switch list.formatToJson(args.format, ignoreRightPaddingVar: args._format.isEmpty) {
case .success(let json): io.out(json)
case .failure(let msg): io.err(msg)
}
} else {
return switch list.format(args.format) {
case .success(let lines): io.out(lines)
case .failure(let msg): io.err(msg)
}
}
}
}
}
extension [MonitorId] {
@MainActor func resolveMonitors(_ io: CmdIo) -> Set<CGPoint> {
var requested: Set<CGPoint> = []
let sortedMonitors = sortedMonitors
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: [Monitor]) -> [Monitor] {
switch self {
case .focused:
return [focus.workspace.workspaceMonitor]
case .mouse:
return [mouseLocation.monitorApproximation]
case .all:
return monitors
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*/ var shouldResetClosedWindowsCache = false
func run(_ env: CmdEnv, _ io: CmdIo) async throws -> Bool {
guard let target = args.resolveTargetOrReportError(env, io) else { return false }
guard let window = target.windowOrNil else {
return io.err(noWindowIsFocused)
}
let prevState = try await window.isMacosFullscreen
let newState: Bool = switch args.toggle {
case .on: true
case .off: false
case .toggle: !prevState
}
if newState == prevState {
io.err((newState ? "Already fullscreen. " : "Already not fullscreen. ") +
"Tip: use --fail-if-noop to exit with non-zero exit code")
return !args.failIfNoop
}
window.asMacWindow().setNativeFullscreen(newState)
guard let workspace = window.visualWorkspace else {
return 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):
try await exitMacOsNativeUnconventionalState(window: window, prevParentKind: prevParentKind, workspace: workspace)
default:
try await window.relayoutWindow(on: workspace)
}
}
return true
}
}
```
## /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*/ var shouldResetClosedWindowsCache = false
func run(_ env: CmdEnv, _ io: CmdIo) async throws -> Bool {
// 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 false }
guard let window = target.windowOrNil else {
return io.err(noWindowIsFocused)
}
let newState: Bool = try await !window.isMacosMinimized
window.asMacWindow().setNativeMinimized(newState)
if newState { // minimize
window.bind(to: macosMinimizedWindowsContainer, adaptiveWeight: 1, index: INDEX_BIND_LAST)
return true
} else { // unminimize
return 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*/ var shouldResetClosedWindowsCache = false
func run(_ env: CmdEnv, _ io: CmdIo) -> Bool {
activateMode(args.targetMode.val)
return true
}
}
```
## /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*/ var shouldResetClosedWindowsCache = true
func run(_ env: CmdEnv, _ io: CmdIo) -> Bool {
let direction = args.direction.val
guard let target = args.resolveTargetOrReportError(env, io) else { return false }
guard let currentWindow = target.windowOrNil else {
return io.err(noWindowIsFocused)
}
guard let parent = currentWindow.parent else { return false }
switch parent.cases {
case .tilingContainer(let parent):
let indexOfCurrent = currentWindow.ownIndex.orDie()
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)
case .window: // "swap windows"
let prevBinding = currentWindow.unbindFromParent()
currentWindow.bind(to: parent, adaptiveWeight: prevBinding.adaptiveWeight, index: indexOfSiblingTarget)
return true
}
} else {
return moveOut(window: currentWindow, direction: direction, io, args, env)
}
case .workspace: // floating window
return io.err("moving floating windows isn't yet supported") // todo
case .macosMinimizedWindowsContainer, .macosFullscreenWindowsContainer, .macosHiddenAppsWindowsContainer:
return io.err(moveOutMacosUnconventionalWindow)
case .macosPopupWindowsContainer:
return false // Impossible
}
}
}
@MainActor private func hitWorkspaceBoundaries(
_ window: Window,
_ workspace: Workspace,
_ io: CmdIo,
_ args: MoveCmdArgs,
_ direction: CardinalDirection,
_ env: CmdEnv,
) -> Bool {
switch args.boundaries {
case .workspace:
switch args.boundariesAction {
case .stop: return true
case .fail: return false
case .createImplicitContainer:
createImplicitContainerAndMoveWindow(window, workspace, direction)
return true
}
case .allMonitorsOuterFrame:
guard let (monitors, index) = window.nodeMonitor?.findRelativeMonitor(inDirection: direction) else {
return 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, io, args, direction)
}
}
}
@MainActor private func hitAllMonitorsOuterFrameBoundaries(
_ window: Window,
_ workspace: Workspace,
_ io: CmdIo,
_ args: MoveCmdArgs,
_ direction: CardinalDirection,
) -> Bool {
switch args.boundariesAction {
case .stop: return true
case .fail: return false
case .createImplicitContainer:
createImplicitContainerAndMoveWindow(window, workspace, direction)
return true
}
}
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(
window: Window,
direction: CardinalDirection,
_ io: CmdIo,
_ args: MoveCmdArgs,
_ env: CmdEnv,
) -> Bool {
let innerMostChild = window.parents.first(where: {
return switch $0.parent?.cases {
case .tilingContainer(let parent): parent.orientation == direction.orientation
// Stop searching
case .workspace, .macosMinimizedWindowsContainer, nil, .macosFullscreenWindowsContainer,
.macosHiddenAppsWindowsContainer, .macosPopupWindowsContainer: true
}
}) as? TilingContainer
guard let innerMostChild else { return false }
guard let parent = innerMostChild.parent else { return false }
switch parent.nodeCases {
case .tilingContainer(let parent):
check(parent.orientation == direction.orientation)
guard let ownIndex = innerMostChild.ownIndex else { return false }
window.bind(to: parent, adaptiveWeight: WEIGHT_AUTO, index: ownIndex + direction.insertionOffset)
return true
case .workspace(let parent):
return hitWorkspaceBoundaries(window, parent, io, args, direction, env)
case .macosMinimizedWindowsContainer, .macosFullscreenWindowsContainer, .macosHiddenAppsWindowsContainer:
return io.err(moveOutMacosUnconventionalWindow)
case .macosPopupWindowsContainer:
return false // Impossible
case .window:
die("Window can't contain children nodes")
}
}
@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) -> Bool {
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 false }
window.bind(
to: parent,
adaptiveWeight: WEIGHT_AUTO,
index: deepTarget.ownIndex.orDie() + 1,
)
}
return true
}
extension TilingTreeNodeCases {
@MainActor fileprivate func findDeepMoveInTargetRecursive(_ orientation: Orientation) -> TilingTreeNodeCases {
return switch self {
case .window:
self
case .tilingContainer(let container):
if container.orientation == orientation {
.tilingContainer(container)
} else {
container.mostRecentChild.orDie("Empty containers must be detached during normalization")
.tilingTreeNodeCasesOrDie()
.findDeepMoveInTargetRecursive(orientation)
}
}
}
}
```
## /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*/ var shouldResetClosedWindowsCache = false
func run(_ env: CmdEnv, _ io: CmdIo) async throws -> Bool {
let mouse = mouseLocation
guard let target = args.resolveTargetOrReportError(env, io) else { return false }
switch args.mouseTarget.val {
case .windowLazyCenter:
guard let rect = try await windowSubjectRectOrReportError(target, io) else { return false }
if rect.contains(mouse) {
io.err("The mouse already belongs to the window. Tip: use --fail-if-noop to exit with non-zero code")
return !args.failIfNoop
}
return moveMouse(io, rect.center)
case .windowForceCenter:
guard let rect = try await windowSubjectRectOrReportError(target, io) else { return false }
return moveMouse(io, rect.center)
case .monitorLazyCenter:
let rect = target.workspace.workspaceMonitor.rect
if rect.contains(mouse) {
io.err("The mouse already belongs to the monitor. Tip: use --fail-if-noop to exit with non-zero code")
return !args.failIfNoop
}
return moveMouse(io, rect.center)
case .monitorForceCenter:
return moveMouse(io, target.workspace.workspaceMonitor.rect.center)
}
}
}
private func moveMouse(_ io: CmdIo, _ point: CGPoint) -> Bool {
let event = CGEvent(
mouseEventSource: nil,
mouseType: CGEventType.mouseMoved,
mouseCursorPosition: point,
mouseButton: CGMouseButton.left,
)
if let event {
event.post(tap: CGEventTapLocation.cghidEventTap)
return true
} else {
return io.err("Failed to move mouse")
}
}
@MainActor
private func windowSubjectRectOrReportError(_ target: LiveFocus, _ io: CmdIo) async throws -> 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() {
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*/ var shouldResetClosedWindowsCache = true
func run(_ env: CmdEnv, _ io: CmdIo) -> Bool {
guard let target = args.resolveTargetOrReportError(env, io) else { return false }
guard let window = target.windowOrNil else {
return io.err(noWindowIsFocused)
}
guard let currentMonitor = window.nodeMonitor else {
return 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 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) -> Bool {
guard let target = args.resolveTargetOrReportError(env, io) else { return false }
guard let window = target.windowOrNil else { return io.err(noWindowIsFocused) }
let subjectWs = window.nodeWorkspace
let targetWorkspace: Workspace
switch args.target.val {
case .relative(let nextPrev):
guard let subjectWs else { return 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 else { return io.err("Can't resolve next or prev workspace") }
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) -> Bool {
if window.nodeWorkspace == targetWorkspace {
io.err("Window '\(window.windowId)' already belongs to workspace '\(targetWorkspace.name)'. Tip: use --fail-if-noop to exit with non-zero code")
return !failIfNoop
}
let targetContainer: NonLeafTreeNodeObject = window.isFloating ? targetWorkspace : targetWorkspace.rootTilingContainer
window.bind(to: targetContainer, adaptiveWeight: WEIGHT_AUTO, index: index)
return 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*/ var shouldResetClosedWindowsCache = true
func run(_ env: CmdEnv, _ io: CmdIo) -> Bool {
guard let target = args.resolveTargetOrReportError(env, io) else { return false }
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 == prevMonitor.monitorId {
return true
}
if targetMonitor.setActiveWorkspace(focusedWorkspace) {
let stubWorkspace = getStubWorkspace(for: prevMonitor)
check(
prevMonitor.setActiveWorkspace(stubWorkspace),
"getStubWorkspace generated incompatible stub workspace (\(stubWorkspace)) for the monitor (\(prevMonitor)",
)
return true
} else {
return 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 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*/ var shouldResetClosedWindowsCache = false
func run(_ env: CmdEnv, _ io: CmdIo) -> Bool {
var stdout = ""
let isOk = reloadConfig(args: args, stdout: &stdout)
if !stdout.isEmpty {
io.out(stdout)
}
return isOk
}
}
@MainActor func reloadConfig(forceConfigUrl: URL? = nil) -> Bool {
var devNull = ""
return reloadConfig(forceConfigUrl: forceConfigUrl, stdout: &devNull)
}
@MainActor func reloadConfig(
args: ReloadConfigCmdArgs = ReloadConfigCmdArgs(rawArgs: []),
forceConfigUrl: URL? = nil,
stdout: inout String,
) -> Bool {
switch readConfig(forceConfigUrl: forceConfigUrl) {
case .success(let (parsedConfig, url)):
if !args.dryRun {
resetHotKeys()
config = parsedConfig
configUrl = url
activateMode(activeMode)
syncStartAtLogin()
MessageModel.shared.message = nil
}
return true
case .failure(let msg):
stdout.append(msg)
if !args.noGui {
Task { @MainActor in
MessageModel.shared.message = Message(description: "AeroSpace Config Error", body: msg)
}
}
return false
}
}
```
## /Sources/AppBundle/command/impl/ResizeCommand.swift
```swift path="/Sources/AppBundle/command/impl/ResizeCommand.swift"
import AppKit
import Common
struct ResizeCommand: Command { // todo cover with tests
let args: ResizeCmdArgs
/*conforms*/ var shouldResetClosedWindowsCache = true
func run(_ env: CmdEnv, _ io: CmdIo) -> Bool {
guard let target = args.resolveTargetOrReportError(env, io) else { return false }
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 io.err("resize command doesn't support floating windows yet https://github.com/nikitabobko/AeroSpace/issues/9") }
guard let orientation else { return false }
guard let node else { return false }
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 false }
parent.children.lazy
.filter { $0 != node }
.forEach { $0.setWeight(parent.orientation, $0.getWeight(parent.orientation) - childDiff) }
node.setWeight(orientation, node.getWeight(orientation) + diff)
return true
}
}
```
## /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*/ var shouldResetClosedWindowsCache = true
func run(_ env: CmdEnv, _ io: CmdIo) -> Bool {
if config.enableNormalizationFlattenContainers {
return 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 false }
guard let window = target.windowOrNil else {
return io.err(noWindowIsFocused)
}
guard let parent = window.parent else { return false }
switch parent.cases {
case .workspace:
// Nothing to do for floating and macOS native fullscreen windows
return 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 true
case .macosMinimizedWindowsContainer, .macosFullscreenWindowsContainer, .macosHiddenAppsWindowsContainer:
return io.err("Can't split macos fullscreen, minimized windows and windows of hidden apps. This behavior may change in the future")
case .macosPopupWindowsContainer:
return false // 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*/ var shouldResetClosedWindowsCache = true
func run(_ env: CmdEnv, _ io: CmdIo) -> Bool {
let workspace = Workspace.get(byName: args.target.val.raw)
let monitor = focus.workspace.workspaceMonitor
if monitor.activeWorkspace == workspace {
io.err("Workspace '\(workspace.name)' is already visible on the focused monitor. Tip: use --fail-if-noop to exit with non-zero code")
return !args.failIfNoop
}
if monitor.setActiveWorkspace(workspace) {
return workspace.focusWorkspace()
} else {
return 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 throws -> Bool {
guard let target = args.resolveTargetOrReportError(env, io) else {
return false
}
guard let currentWindow = target.windowOrNil else {
return io.err(noWindowIsFocused)
}
let targetWindow: Window?
switch args.target.val {
case .direction(let direction):
if let (parent, ownIndex) = currentWindow.closestParent(hasChildrenInDirection: direction, withLayout: nil) {
targetWindow = parent.children[ownIndex + direction.focusOffset].findLeafWindowRecursive(snappedTo: direction.opposite)
} else if args.wrapAround {
targetWindow = target.workspace.findLeafWindowRecursive(snappedTo: direction.opposite)
} else {
return false
}
case .dfsRelative(let nextPrev):
let windows = target.workspace.rootTilingContainer.allLeafWindowsRecursive
guard let currentIndex = windows.firstIndex(where: { $0 == target.windowOrNil }) else {
return false
}
var targetIndex = switch nextPrev {
case .dfsNext: currentIndex + 1
case .dfsPrev: currentIndex - 1
}
if !(0 ..< windows.count).contains(targetIndex) {
if !args.wrapAround {
return false
}
targetIndex = (targetIndex + windows.count) % windows.count
}
targetWindow = windows[targetIndex]
}
guard let targetWindow else {
return false
}
swapWindows(currentWindow, targetWindow)
if args.swapFocus {
return targetWindow.focusWindow()
}
return true
}
}
```
## /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*/ var shouldResetClosedWindowsCache = false
func run(_ env: CmdEnv, _ io: CmdIo) async throws -> Bool {
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
try await binding.commands.runCmdSeq(env, io)
} else {
io.err("Binding '\(args.binding.val)' is not presented in mode '\(args.mode)'")
}
} else {
io.err("Mode '\(args.mode)' doesn't exist. " +
"Available modes: \(config.modes.keys.joined(separator: ","))")
}
}
}
```
## /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*/ var shouldResetClosedWindowsCache = false
func run(_ env: CmdEnv, _ io: CmdIo) -> Bool {
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 let volume = try? Sound.output.readVolume() {
VolumePanel.shared.update(with: Sound.output.isMuted ? 0 : volume)
}
return true
}
}
```
## /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*/ var shouldResetClosedWindowsCache = false
func run(_ env: CmdEnv, _ io: CmdIo) -> Bool {
return 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*/ var shouldResetClosedWindowsCache = false
func run(_ env: CmdEnv, _ io: CmdIo) -> Bool { // todo refactor
guard let target = args.resolveTargetOrReportError(env, io) else { return false }
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 else { return false }
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 {
io.err("Workspace '\(workspaceName)' is already focused. Tip: use --fail-if-noop to exit with non-zero code")
return !args.failIfNoop
} else {
return Workspace.get(byName: workspaceName).focusWorkspace()
}
}
}
@MainActor func getNextPrevWorkspace(current: Workspace, isNext: Bool, wrapAround: Bool, stdin: String?, target: LiveFocus) -> Workspace? {
let stdinWorkspaces: [String] = stdin?.split(separator: "\n").map { String($0).trim() }.filter { !$0.isEmpty } ?? []
let currentMonitor = current.workspaceMonitor
let workspaces: [Workspace] = stdin != nil
? stdinWorkspaces.map { Workspace.get(byName: $0) }
: Workspace.all.filter { $0.workspaceMonitor.rect.topLeftCorner == currentMonitor.rect.topLeftCorner }
.toSet()
.union([current])
.sorted()
let index = workspaces.firstIndex(where: { $0 == target.workspace }) ?? 0
let workspace: Workspace? = if wrapAround {
workspaces.get(wrappingIndex: isNext ? index + 1 : index - 1)
} else {
workspaces.getOrNil(atIndex: isNext ? index + 1 : index - 1)
}
return workspace
}
```
## /Sources/AppBundle/command/parseCommand.swift
```swift path="/Sources/AppBundle/command/parseCommand.swift"
import Common
import TOMLKit
func parseCommand(_ raw: String) -> ParsedCmd<any Command> {
if raw.starts(with: "exec-and-forget") {
return .cmd(ExecAndForgetCommand(args: ExecAndForgetCmdArgs(bashScript: raw.removePrefix("exec-and-forget"))))
}
return switch raw.splitArgs() {
case .success(let args): parseCommand(args)
case .failure(let fail): .failure(fail)
}
}
func parseCommand(_ args: [String]) -> ParsedCmd<any Command> {
parseCmdArgs(args.slice).map { $0.toCommand() }
}
func expectedActualTypeError(expected: TOMLType, actual: TOMLType) -> String {
"Expected type is '\(expected)'. But actual type is '\(actual)'"
}
func expectedActualTypeError(expected: [TOMLType], actual: TOMLType) -> String {
if let single = expected.singleOrNil() {
return expectedActualTypeError(expected: single, actual: actual)
} else {
return "Expected types are \(expected.map { "'\($0.description)'" }.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
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((try? String(contentsOf: defaultConfigUrl)).orDie())
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: ConvenienceCopyable {
var afterLoginCommand: [any Command] = []
var afterStartupCommand: [any Command] = []
var _indentForNestedContainersWithTheSameOrientation: Void = ()
var enableNormalizationFlattenContainers: Bool = true
var _nonEmptyWorkspacesRootContainersLayoutOnStartup: Void = ()
var defaultRootContainerLayout: Layout = .tiles
var defaultRootContainerOrientation: DefaultContainerOrientation = .auto
var startAtLogin: Bool = false
var automaticallyUnhideMacosHiddenApps: Bool = false
var accordionPadding: Int = 30
var enableNormalizationOppositeOrientationForNestedContainers: Bool = true
var execOnWorkspaceChange: [String] = [] // todo deprecate
var keyMapping = KeyMapping()
var execConfig: ExecConfig = ExecConfig()
var onFocusChanged: [any Command] = []
// var onFocusedWorkspaceChanged: [any Command] = []
var onFocusedMonitorChanged: [any Command] = []
var gaps: Gaps = .zero
var workspaceToMonitorForceAssignment: [String: [MonitorDescription]] = [:]
var modes: [String: Mode] = [:]
var onWindowDetected: [WindowDetectedCallback] = []
var preservedWorkspaceNames: [String] = []
}
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] = if let configLocation = serverArgs.configLocation {
[URL(filePath: configLocation)]
} else {
[
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/DynamicConfigValue.swift
```swift path="/Sources/AppBundle/config/DynamicConfigValue.swift"
import Common
import TOMLKit
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 {
func getValue(for monitor: any Monitor) -> Value {
switch self {
case .constant(let value):
return value
case .perMonitor(let array, let defaultValue):
let sortedMonitors = sortedMonitors
return array
.lazy
.compactMap {
$0.description.resolveMonitor(sortedMonitors: sortedMonitors)?.rect.topLeftCorner == monitor.rect.topLeftCorner
? $0.value
: nil
}
.first ?? defaultValue
}
}
}
func parseDynamicValue<T>(
_ raw: TOMLValueConvertible,
_ valueType: T.Type,
_ fallback: T,
_ backtrace: TomlBacktrace,
_ errors: inout [TomlParseError],
) -> DynamicConfigValue<T> {
if let simpleValue = parseSimpleType(raw) as T? {
return .constant(simpleValue)
} else if let array = raw.array {
if array.isEmpty {
errors.append(.semantic(backtrace, "The array must not be empty"))
return .constant(fallback)
}
guard let defaultValue = array.last.flatMap({ parseSimpleType($0) as T? }) else {
errors.append(.semantic(backtrace, "The last item in the array must be of type \(T.self)"))
return .constant(fallback)
}
if array.dropLast().isEmpty {
errors.append(.semantic(backtrace, "The array must contain at least one monitor pattern"))
return .constant(fallback)
}
let rules: [PerMonitorValue<T>] = parsePerMonitorValues(TOMLArray(array.dropLast()), backtrace, &errors)
return .perMonitor(rules, default: defaultValue)
} else {
errors.append(.semantic(backtrace, "Unsupported type: \(raw.type), expected: \(valueType) or array"))
return .constant(fallback)
}
}
func parsePerMonitorValues<T>(_ array: TOMLArray, _ backtrace: TomlBacktrace, _ errors: inout [TomlParseError]) -> [PerMonitorValue<T>] {
array.enumerated().compactMap { (index: Int, raw: TOMLValueConvertible) -> 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: &errors)
else {
return nil
}
let monitorDescriptionResult = parseMonitorDescription(key, backtrace)
guard let monitorDescription = monitorDescriptionResult.getOrNil(appendErrorTo: &errors) else { return nil }
guard let value = parseSimpleType(value) as T? else {
errors.append(.semantic(backtrace, "Expected type is '\(T.self)'. But actual type is '\(value.type)'"))
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
import TOMLKit
@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(_ targetMode: String?) {
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 {
if let activeMode {
try await runSession(.hotkeyBinding, .checkServerIsEnabledOrDie) { () throws in
_ = try await config.modes[activeMode]?.bindings[binding.descriptionWithKeyCode]?.commands
.runCmdSeq(.defaultEnv, .emptyStdin)
}
}
}
})
}
for (binding, key) in hotkeys {
if targetBindings.keys.contains(binding) {
key.isEnabled = true
} else {
key.isEnabled = false
}
}
activeMode = targetMode
}
struct HotkeyBinding: Equatable, Sendable {
let modifiers: NSEvent.ModifierFlags
let keyCode: Key
let commands: [any Command]
let descriptionWithKeyCode: String
let descriptionWithKeyNotation: String
init(_ modifiers: NSEvent.ModifierFlags, _ keyCode: Key, _ commands: [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 &&
zip(lhs.commands, rhs.commands).allSatisfy { $0.equals($1) }
}
}
func parseBindings(_ raw: TOMLValueConvertible, _ backtrace: TomlBacktrace, _ errors: inout [TomlParseError], _ mapping: [String: Key]) -> [String: HotkeyBinding] {
guard let rawTable = raw.table else {
errors += [expectedActualTypeError(expected: .table, actual: raw.type, backtrace)]
return [:]
}
var result: [String: HotkeyBinding] = [:]
for (binding, rawCommand): (String, TOMLValueConvertible) in rawTable {
let backtrace = backtrace + .key(binding)
let binding = parseBinding(binding, backtrace, mapping)
.flatMap { modifiers, key -> ParsedToml<HotkeyBinding> in
parseCommandOrCommands(rawCommand).toParsedToml(backtrace).map {
HotkeyBinding(modifiers, key, $0, descriptionWithKeyNotation: binding)
}
}
.getOrNil(appendErrorTo: &errors)
if let binding {
if result.keys.contains(binding.descriptionWithKeyCode) {
errors.append(.semantic(backtrace, "'\(binding.descriptionWithKeyCode)' Binding redeclaration"))
}
result[binding.descriptionWithKeyCode] = binding
}
}
return result
}
func parseBinding(_ raw: String, _ backtrace: TomlBacktrace, _ mapping: [String: Key]) -> ParsedToml<(NSEvent.ModifierFlags, Key)> {
let rawKeys = raw.split(separator: "-")
let modifiers: ParsedToml<NSEvent.ModifierFlags> = rawKeys.dropLast()
.mapAllOrFailure {
modifiersMap[String($0)].orFailure(.semantic(backtrace, "Can't parse modifiers in '\(raw)' binding"))
}
.map { NSEvent.ModifierFlags($0) }
let key: ParsedToml<Key> = rawKeys.last.flatMap { mapping[String($0)] }
.orFailure(.semantic(backtrace, "Can't parse the key in '\(raw)' binding"))
return modifiers.flatMap { modifiers -> ParsedToml<(NSEvent.ModifierFlags, Key)> in
key.flatMap { key -> ParsedToml<(NSEvent.ModifierFlags, Key)> in
.success((modifiers, key))
}
}
}
```
## /Sources/AppBundle/config/Mode.swift
```swift path="/Sources/AppBundle/config/Mode.swift"
import Common
import HotKey
import TOMLKit
struct Mode: ConvenienceCopyable, Equatable, Sendable {
/// User visible name. Optional. todo drop it?
var name: String?
var bindings: [String: HotkeyBinding]
static let zero = Mode(name: nil, bindings: [:])
}
func parseModes(_ raw: TOMLValueConvertible, _ backtrace: TomlBacktrace, _ errors: inout [TomlParseError], _ mapping: [String: Key]) -> [String: Mode] {
guard let rawTable = raw.table else {
errors += [expectedActualTypeError(expected: .table, actual: raw.type, backtrace)]
return [:]
}
var result: [String: Mode] = [:]
for (key, value) in rawTable {
result[key] = parseMode(value, backtrace + .key(key), &errors, mapping)
}
if !result.keys.contains(mainModeId) {
errors += [.semantic(backtrace, "Please specify '\(mainModeId)' mode")]
}
return result
}
func parseMode(_ raw: TOMLValueConvertible, _ backtrace: TomlBacktrace, _ errors: inout [TomlParseError], _ mapping: [String: Key]) -> Mode {
guard let rawTable: TOMLTable = raw.table else {
errors += [expectedActualTypeError(expected: .table, actual: raw.type, 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, &errors, mapping)
default:
errors += [unknownKeyError(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 TOMLKit
@MainActor
func readConfig(forceConfigUrl: URL? = nil) -> Result<(Config, URL), String> {
let customConfigUrl: URL
switch findCustomConfigUrl() {
case .file(let url): customConfigUrl = url
case .noCustomConfigExists: customConfigUrl = defaultConfigUrl
case .ambiguousConfigError(let candidates):
let msg = """
Ambiguous config error. Several configs found:
\(candidates.map(\.path).joined(separator: "\n"))
"""
return .failure(msg)
}
let configUrl: URL = forceConfigUrl ?? customConfigUrl
let (parsedConfig, errors) = (try? String(contentsOf: configUrl)).map { parseConfig($0) } ?? (defaultConfig, [])
if errors.isEmpty {
return .success((parsedConfig, configUrl))
} else {
let msg = """
Failed to parse \(configUrl.absoluteURL.path)
\(errors.map(\.description).joined(separator: "\n\n"))
"""
return .failure(msg)
}
}
enum TomlParseError: Error, CustomStringConvertible, Equatable {
case semantic(_ backtrace: TomlBacktrace, _ message: String)
case syntax(_ message: String)
var description: String {
return switch self {
// todo Make 'split' + flatten normalization prettier
case .semantic(let backtrace, let message): backtrace.isEmptyRoot ? message : "\(backtrace): \(message)"
case .syntax(let message): message
}
}
}
typealias ParsedToml<T> = Result<T, TomlParseError>
extension ParserProtocol {
func transformRawConfig(_ raw: S,
_ value: TOMLValueConvertible,
_ backtrace: TomlBacktrace,
_ errors: inout [TomlParseError]) -> S
{
if let value = parse(value, backtrace, &errors).getOrNil(appendErrorTo: &errors) {
return raw.copy(keyPath, value)
}
return raw
}
}
protocol ParserProtocol<S>: Sendable {
associatedtype T
associatedtype S where S: ConvenienceCopyable
var keyPath: SendableWritableKeyPath<S, T> { get }
var parse: @Sendable (TOMLValueConvertible, TomlBacktrace, inout [TomlParseError]) -> ParsedToml<T> { get }
}
struct Parser<S: ConvenienceCopyable, T>: ParserProtocol {
let keyPath: SendableWritableKeyPath<S, T>
let parse: @Sendable (TOMLValueConvertible, TomlBacktrace, inout [TomlParseError]) -> ParsedToml<T>
init(_ keyPath: SendableWritableKeyPath<S, T>, _ parse: @escaping @Sendable (TOMLValueConvertible, TomlBacktrace, inout [TomlParseError]) -> T) {
self.keyPath = keyPath
self.parse = { raw, backtrace, errors -> ParsedToml<T> in .success(parse(raw, backtrace, &errors)) }
}
init(_ keyPath: SendableWritableKeyPath<S, T>, _ parse: @escaping @Sendable (TOMLValueConvertible, TomlBacktrace) -> ParsedToml<T>) {
self.keyPath = keyPath
self.parse = { raw, backtrace, _ -> ParsedToml<T> in parse(raw, backtrace) }
}
}
private let keyMappingConfigRootKey = "key-mapping"
private let modeConfigRootKey = "mode"
// 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>] = [
"after-login-command": Parser(\.afterLoginCommand, parseAfterLoginCommand),
"after-startup-command": Parser(\.afterStartupCommand) { parseCommandOrCommands($0).toParsedToml($1) },
"on-focus-changed": Parser(\.onFocusChanged) { parseCommandOrCommands($0).toParsedToml($1) },
"on-focused-monitor-changed": Parser(\.onFocusedMonitorChanged) { parseCommandOrCommands($0).toParsedToml($1) },
// "on-focused-workspace-changed": Parser(\.onFocusedWorkspaceChanged, { parseCommandOrCommands($0).toParsedToml($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),
"automatically-unhide-macos-hidden-apps": Parser(\.automaticallyUnhideMacosHiddenApps, parseBool),
"accordion-padding": Parser(\.accordionPadding, parseInt),
"exec-on-workspace-change": Parser(\.execOnWorkspaceChange, parseExecOnWorkspaceChange),
"exec": Parser(\.execConfig, parseExecConfig),
keyMappingConfigRootKey: Parser(\.keyMapping, skipParsing(Config().keyMapping)), // Parsed manually
modeConfigRootKey: Parser(\.modes, skipParsing(Config().modes)), // Parsed manually
"gaps": Parser(\.gaps, parseGaps),
"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 where T == any Command {
fileprivate func toEither() -> Parsed<T> {
return switch self {
case .cmd(let a):
a.info.allowInConfig
? .success(a)
: .failure("Command '\(a.info.kind.rawValue)' cannot be used in config")
case .help(let a): .failure(a)
case .failure(let a): .failure(a)
}
}
}
extension Command {
fileprivate var isMacOsNativeCommand: Bool { // Problem ID-B6E178F2
self is MacosNativeMinimizeCommand || self is MacosNativeFullscreenCommand
}
}
func parseAfterLoginCommand(_ raw: TOMLValueConvertible, _ backtrace: TomlBacktrace) -> ParsedToml<[any Command]> {
if let array = raw.array, 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(.semantic(backtrace, msg))
}
func parseCommandOrCommands(_ raw: TOMLValueConvertible) -> Parsed<[any Command]> {
if let rawString = raw.string {
return parseCommand(rawString).toEither().map { [$0] }
} else if let rawArray = raw.array {
let commands: Parsed<[any Command]> = (0 ..< rawArray.count).mapAllOrFailure { index in
let rawString: String = rawArray[index].string ?? expectedActualTypeError(expected: .string, actual: rawArray[index].type)
return parseCommand(rawString).toEither()
}
return commands.filter("macos-native-* commands are only allowed to be the last commands in the list") {
!$0.dropLast().contains(where: { $0.isMacOsNativeCommand })
}
} else {
return .failure(expectedActualTypeError(expected: [.string, .array], actual: raw.type))
}
}
@MainActor func parseConfig(_ rawToml: String) -> (config: Config, errors: [TomlParseError]) { // todo change return value to Result
let rawTable: TOMLTable
do {
rawTable = try TOMLTable(string: rawToml)
} catch let e as TOMLParseError {
return (defaultConfig, [.syntax(e.debugDescription)])
} catch let e {
return (defaultConfig, [.syntax(e.localizedDescription)])
}
var errors: [TomlParseError] = []
var config = rawTable.parseTable(Config(), configParser, .emptyRoot, &errors)
if let mapping = rawTable[keyMappingConfigRootKey].flatMap({ parseKeyMapping($0, .rootKey(keyMappingConfigRootKey), &errors) }) {
config.keyMapping = mapping
}
if let modes = rawTable[modeConfigRootKey].flatMap({ parseModes($0, .rootKey(modeConfigRootKey), &errors, config.keyMapping.resolve()) }) {
config.modes = modes
}
config.preservedWorkspaceNames = config.modes.values.lazy
.flatMap { (mode: Mode) -> [HotkeyBinding] in Array(mode.bindings.values) }
.flatMap { (binding: HotkeyBinding) -> [String] in
binding.commands.filterIsInstance(of: WorkspaceCommand.self).compactMap { $0.args.target.val.workspaceNameOrNil()?.raw } +
binding.commands.filterIsInstance(of: MoveNodeToWorkspaceCommand.self).compactMap { $0.args.target.val.workspaceNameOrNil()?.raw }
}
+ (config.workspaceToMonitorForceAssignment).keys
if config.enableNormalizationFlattenContainers {
let containsSplitCommand = config.modes.values.lazy.flatMap { $0.bindings.values }
.flatMap { $0.commands }
.contains { $0 is SplitCommand }
if containsSplitCommand {
errors += [.semantic(
.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'.
""",
)]
}
}
return (config, errors)
}
func parseIndentForNestedContainersWithTheSameOrientation(
_ raw: TOMLValueConvertible,
_ backtrace: TomlBacktrace,
) -> ParsedToml<Void> {
let msg = "Deprecated. Please drop it from the config. See https://github.com/nikitabobko/AeroSpace/issues/96"
return .failure(.semantic(backtrace, msg))
}
func parseInt(_ raw: TOMLValueConvertible, _ backtrace: TomlBacktrace) -> ParsedToml<Int> {
raw.int.orFailure(expectedActualTypeError(expected: .int, actual: raw.type, backtrace))
}
func parseString(_ raw: TOMLValueConvertible, _ backtrace: TomlBacktrace) -> ParsedToml<String> {
raw.string.orFailure(expectedActualTypeError(expected: .string, actual: raw.type, backtrace))
}
func parseSimpleType<T>(_ raw: TOMLValueConvertible) -> T? {
(raw.int as? T) ?? (raw.string as? T) ?? (raw.bool as? T)
}
extension TOMLValueConvertible {
func unwrapTableWithSingleKey(expectedKey: String? = nil, _ backtrace: inout TomlBacktrace) -> ParsedToml<(key: String, value: TOMLValueConvertible)> {
guard let table else {
return .failure(expectedActualTypeError(expected: .table, actual: type, backtrace))
}
let singleKeyError: TomlParseError = .semantic(
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, TOMLValueConvertible) = table.count == 1 ? table.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: TOMLValueConvertible, _ backtrace: TomlBacktrace) -> ParsedToml<TOMLArray> {
raw.array.orFailure(expectedActualTypeError(expected: .array, actual: raw.type, backtrace))
}
func parseTable<T: ConvenienceCopyable>(
_ raw: TOMLValueConvertible,
_ initial: T,
_ fieldsParser: [String: any ParserProtocol<T>],
_ backtrace: TomlBacktrace,
_ errors: inout [TomlParseError],
) -> T {
guard let table = raw.table else {
errors.append(expectedActualTypeError(expected: .table, actual: raw.type, backtrace))
return initial
}
return table.parseTable(initial, fieldsParser, backtrace, &errors)
}
private func parseStartupRootContainerLayout(_ raw: TOMLValueConvertible, _ backtrace: TomlBacktrace) -> ParsedToml<Void> {
parseString(raw, backtrace)
.filter(.semantic(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: TOMLValueConvertible, _ backtrace: TomlBacktrace) -> ParsedToml<Layout> {
parseString(raw, backtrace)
.flatMap { $0.parseLayout().orFailure(.semantic(backtrace, "Can't parse layout '\($0)'")) }
}
private func skipParsing<T: Sendable>(_ value: T) -> @Sendable (_ raw: TOMLValueConvertible, _ backtrace: TomlBacktrace) -> ParsedToml<T> {
{ _, _ in .success(value) }
}
private func parseExecOnWorkspaceChange(_ raw: TOMLValueConvertible, _ backtrace: TomlBacktrace) -> ParsedToml<[String]> {
parseTomlArray(raw, backtrace)
.flatMap { arr in
arr.mapAllOrFailure { elem in parseString(elem, backtrace) }
}
}
private func parseDefaultContainerOrientation(_ raw: TOMLValueConvertible, _ backtrace: TomlBacktrace) -> ParsedToml<DefaultContainerOrientation> {
parseString(raw, backtrace).flatMap {
DefaultContainerOrientation(rawValue: $0)
.orFailure(.semantic(backtrace, "Can't parse default container orientation '\($0)'"))
}
}
extension Parsed where Failure == String {
func toParsedToml(_ backtrace: TomlBacktrace) -> ParsedToml<Success> {
mapError { .semantic(backtrace, $0) }
}
}
func parseBool(_ raw: TOMLValueConvertible, _ backtrace: TomlBacktrace) -> ParsedToml<Bool> {
raw.bool.orFailure(expectedActualTypeError(expected: .bool, actual: raw.type, backtrace))
}
indirect enum TomlBacktrace: CustomStringConvertible, Equatable {
case emptyRoot
case rootKey(String)
case key(String)
case index(Int)
case pair(TomlBacktrace, TomlBacktrace)
var description: String {
return switch self {
case .emptyRoot: dieT("Impossible")
case .rootKey(let value): value
case .key(let value): "." + value
case .index(let index): "[\(index)]"
case .pair(let first, let second): first.description + second.description
}
}
var isEmptyRoot: Bool {
return switch self {
case .emptyRoot: true
default: false
}
}
var isRootKey: Bool {
return switch self {
case .rootKey: true
default: false
}
}
static func + (lhs: TomlBacktrace, rhs: TomlBacktrace) -> TomlBacktrace {
if case .emptyRoot = lhs {
if case .key(let newRoot) = rhs {
return .rootKey(newRoot)
} else {
die("Impossible")
}
} else {
return pair(lhs, rhs)
}
}
}
extension TOMLTable {
func parseTable<T: ConvenienceCopyable>(
_ initial: T,
_ fieldsParser: [String: any ParserProtocol<T>],
_ backtrace: TomlBacktrace,
_ errors: inout [TomlParseError],
) -> T {
var raw = initial
for (key, value) in self {
let backtrace: TomlBacktrace = backtrace + .key(key)
if let parser = fieldsParser[key] {
raw = parser.transformRawConfig(raw, value, backtrace, &errors)
} else {
errors.append(unknownKeyError(backtrace))
}
}
return raw
}
}
func unknownKeyError(_ backtrace: TomlBacktrace) -> TomlParseError {
.semantic(backtrace, backtrace.isRootKey ? "Unknown top-level key" : "Unknown key")
}
func expectedActualTypeError(expected: TOMLType, actual: TOMLType, _ backtrace: TomlBacktrace) -> TomlParseError {
.semantic(backtrace, expectedActualTypeError(expected: expected, actual: actual))
}
func expectedActualTypeError(expected: [TOMLType], actual: TOMLType, _ backtrace: TomlBacktrace) -> TomlParseError {
.semantic(backtrace, expectedActualTypeError(expected: expected, actual: actual))
}
```
## /Sources/AppBundle/config/parseExecEnvVariables.swift
```swift path="/Sources/AppBundle/config/parseExecEnvVariables.swift"
import AppKit
import Common
import TOMLKit
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: ConvenienceCopyable, 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: TOMLValueConvertible, _ backtrace: TomlBacktrace, _ errors: inout [TomlParseError]) -> ExecConfig {
parseTable(raw, RawExecConfig(), rawExecConfigParser, backtrace, &errors).expand()
}
private func parseEnvVariables(_ raw: TOMLValueConvertible, _ backtrace: TomlBacktrace, _ errors: inout [TomlParseError]) -> [String: String] {
guard let table = raw.table else {
errors.append(expectedActualTypeError(expected: .array, actual: raw.type, 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" { errors.append(.semantic(backtrace, "Changing 'PWD' is not allowed")) }
guard let rawStr = parseString(value, backtrace).getOrNil(appendErrorTo: &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): errors += _errros.map { .semantic(backtrace, $0) }
}
}
return result
}
```
## /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
if config.startAtLogin {
if isDebug {
print("'start-at-login = true' has no effect in debug builds")
} else {
_ = try? service.register()
}
} else {
_ = try? service.unregister()
}
}
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/util/NSRunningApplicationEx.swift
```swift path="/Sources/AppBundle/util/NSRunningApplicationEx.swift"
import AppKit
extension NSRunningApplication {
var idForDebug: String {
"PID: \(processIdentifier) ID: \(bundleIdentifier ?? executableURL?.description ?? "")"
}
}
```
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.