├── .bundle
└── config
├── .editorconfig
├── .gitattributes
├── .github
├── DISCUSSION_TEMPLATE
│ └── potential-bugs.yml
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── config.yml
│ └── new-issue.yml
└── workflows
│ ├── build.yml
│ └── close-third-party-issues.yml
├── .gitignore
├── .idea
├── codeStyles
│ ├── Project.xml
│ └── codeStyleConfig.xml
└── inspectionProfiles
│ └── Project_Default.xml
├── .sourcekit-lsp
└── config.json
├── .swift-version
├── .swiftformat
├── .swiftlint.yml
├── AeroSpace.xcodeproj
├── project.pbxproj
└── project.xcworkspace
│ └── contents.xcworkspacedata
├── CONTRIBUTING.md
├── Gemfile
├── LICENSE.txt
├── Package.resolved
├── Package.swift
├── README.md
├── ShellParserGenerated
├── .gitignore
├── Package.swift
└── Sources
│ └── ShellParserGenerated
│ ├── ShellLexer.swift
│ └── ShellParser.swift
├── Sources
├── AeroSpaceApp
│ └── AeroSpaceApp.swift
├── AppBundle
│ ├── GlobalObserver.swift
│ ├── command
│ │ ├── CmdEnv.swift
│ │ ├── CmdIo.swift
│ │ ├── Command.swift
│ │ ├── cmdManifest.swift
│ │ ├── cmdResolveTargetOrReportError.swift
│ │ ├── format.swift
│ │ ├── formatToJson.swift
│ │ ├── impl
│ │ │ ├── BalanceSizesCommand.swift
│ │ │ ├── CloseAllWindowsButCurrentCommand.swift
│ │ │ ├── CloseCommand.swift
│ │ │ ├── ConfigCommand.swift
│ │ │ ├── DebugWindowsCommand.swift
│ │ │ ├── EnableCommand.swift
│ │ │ ├── ExecAndForgetCommand.swift
│ │ │ ├── FlattenWorkspaceTreeCommand.swift
│ │ │ ├── FocusBackAndForthCommand.swift
│ │ │ ├── FocusCommand.swift
│ │ │ ├── FocusMonitorCommand.swift
│ │ │ ├── FullscreenCommand.swift
│ │ │ ├── JoinWithCommand.swift
│ │ │ ├── LayoutCommand.swift
│ │ │ ├── ListAppsCommand.swift
│ │ │ ├── ListExecEnvVarsCommand.swift
│ │ │ ├── ListModesCommand.swift
│ │ │ ├── ListMonitorsCommand.swift
│ │ │ ├── ListWindowsCommand.swift
│ │ │ ├── ListWorkspacesCommand.swift
│ │ │ ├── MacosNativeFullscreenCommand.swift
│ │ │ ├── MacosNativeMinimizeCommand.swift
│ │ │ ├── ModeCommand.swift
│ │ │ ├── MoveCommand.swift
│ │ │ ├── MoveMouseCommand.swift
│ │ │ ├── MoveNodeToMonitorCommand.swift
│ │ │ ├── MoveNodeToWorkspaceCommand.swift
│ │ │ ├── MoveWorkspaceToMonitorCommand.swift
│ │ │ ├── ReloadConfigCommand.swift
│ │ │ ├── ResizeCommand.swift
│ │ │ ├── SplitCommand.swift
│ │ │ ├── SummonWorkspaceCommand.swift
│ │ │ ├── TriggerBindingCommand.swift
│ │ │ ├── VolumeCommand.swift
│ │ │ ├── WorkspaceBackAndForthCommand.swift
│ │ │ └── WorkspaceCommand.swift
│ │ └── parseCommand.swift
│ ├── config
│ │ ├── Config.swift
│ │ ├── ConfigFile.swift
│ │ ├── DynamicConfigValue.swift
│ │ ├── HotkeyBinding.swift
│ │ ├── Mode.swift
│ │ ├── keysMap.swift
│ │ ├── parseConfig.swift
│ │ ├── parseExecEnvVariables.swift
│ │ ├── parseGaps.swift
│ │ ├── parseKeyMapping.swift
│ │ ├── parseOnWindowDetected.swift
│ │ ├── parseWorkspaceToMonitorAssignment.swift
│ │ └── startAtLogin.swift
│ ├── focus.swift
│ ├── focusCache.swift
│ ├── getNativeFocusedWindow.swift
│ ├── initAppBundle.swift
│ ├── layout
│ │ ├── layoutRecursive.swift
│ │ └── refresh.swift
│ ├── model
│ │ ├── Json.swift
│ │ ├── Monitor.swift
│ │ ├── MonitorDescriptionEx.swift
│ │ ├── MonitorEx.swift
│ │ └── Rect.swift
│ ├── mouse
│ │ ├── mouse.swift
│ │ ├── moveWithMouse.swift
│ │ └── resizeWithMouse.swift
│ ├── normalizeLayoutReason.swift
│ ├── runLoop.swift
│ ├── server.swift
│ ├── shell
│ │ └── Shell.swift
│ ├── tree
│ │ ├── AbstractApp.swift
│ │ ├── MacApp.swift
│ │ ├── MacWindow.swift
│ │ ├── MacosUnconventionalWindowsContainer.swift
│ │ ├── TilingContainer.swift
│ │ ├── TreeNode.swift
│ │ ├── TreeNodeCases.swift
│ │ ├── TreeNodeEx.swift
│ │ ├── Window.swift
│ │ ├── Workspace.swift
│ │ ├── WorkspaceEx.swift
│ │ ├── frozen
│ │ │ ├── FrozenTreeNode.swift
│ │ │ ├── FrozenWorld.swift
│ │ │ └── closedWindowsCache.swift
│ │ └── normalizeContainers.swift
│ ├── ui
│ │ ├── AppearanceTheme.swift
│ │ ├── ExperimentalUISettings.swift
│ │ ├── MenuBar.swift
│ │ ├── MenuBarLabel.swift
│ │ └── TrayMenuModel.swift
│ └── util
│ │ ├── AeroAny.swift
│ │ ├── ArrayEx.swift
│ │ ├── AxSubscription.swift
│ │ ├── AxUiElementMock.swift
│ │ ├── AxUiElementMockEx.swift
│ │ ├── LazySequenceProtocolEx.swift
│ │ ├── MruStack.swift
│ │ ├── NSRunningApplicationEx.swift
│ │ ├── NsApplicationEx.swift
│ │ ├── SetEx.swift
│ │ ├── ThreadGuardedValue.swift
│ │ ├── accessibility.swift
│ │ ├── appBundleUtil.swift
│ │ ├── axTrustedCheckOptionPrompt.swift
│ │ └── dumpAx.swift
├── AppBundleTests
│ ├── AxWindowKindTest.swift
│ ├── assert.swift
│ ├── command
│ │ ├── BalanceSizesCommandTest.swift
│ │ ├── CloseCommandTest.swift
│ │ ├── ExecCommandTest.swift
│ │ ├── FlattenWorkspaceTreeCommandTest.swift
│ │ ├── FocusCommandTest.swift
│ │ ├── JoinWithCommandTest.swift
│ │ ├── ListAppsTest.swift
│ │ ├── ListModesTest.swift
│ │ ├── ListMonitorsTest.swift
│ │ ├── ListWindowsTest.swift
│ │ ├── ListWorkspacesTest.swift
│ │ ├── MoveCommandTest.swift
│ │ ├── MoveNodeToWorkspaceCommandTest.swift
│ │ ├── ResizeCommandTest.swift
│ │ ├── SplitCommandTest.swift
│ │ ├── SummonWorkspaceCommandTest.swift
│ │ └── WorkspaceCommandTest.swift
│ ├── config
│ │ ├── ConfigTest.swift
│ │ ├── ParseEnvVariablesTest.swift
│ │ └── SplitArgsTest.swift
│ ├── shell
│ │ └── ShellTest.swift
│ ├── testExtensions.swift
│ ├── testUtil.swift
│ └── tree
│ │ ├── TestApp.swift
│ │ ├── TestWindow.swift
│ │ ├── TilingContainer.swift
│ │ └── TreeNodeTest.swift
├── Cli
│ ├── _main.swift
│ ├── cliUtil.swift
│ └── subcommandDescriptionsGenerated.swift
├── Common
│ ├── appMetadata.swift
│ ├── cmdArgs
│ │ ├── ArgParser.swift
│ │ ├── cmdArgsManifest.swift
│ │ ├── cmdArgsStringArrayEx.swift
│ │ ├── impl
│ │ │ ├── BalanceSizesCmdArgs.swift
│ │ │ ├── CloseAllWindowsButCurrentCmdArgs.swift
│ │ │ ├── CloseCmdArgs.swift
│ │ │ ├── ConfigCmdArgs.swift
│ │ │ ├── DebugWindowsCmdArgs.swift
│ │ │ ├── EnableCmdArgs.swift
│ │ │ ├── ExecAndForgetCmdArgs.swift
│ │ │ ├── FlattenWorkspaceTreeCmdArgs.swift
│ │ │ ├── FocusBackAndForthCmdArgs.swift
│ │ │ ├── FocusCmdArgs.swift
│ │ │ ├── FocusMonitorCmdArgs.swift
│ │ │ ├── FullscreenCmdArgs.swift
│ │ │ ├── JoinWithCmdArgs.swift
│ │ │ ├── LayoutCmdArgs.swift
│ │ │ ├── ListAppsCmdArgs.swift
│ │ │ ├── ListExecEnvVarsCmdArgs.swift
│ │ │ ├── ListModesCmdArgs.swift
│ │ │ ├── ListMonitorsCmdArgs.swift
│ │ │ ├── ListWindowsCmdArgs.swift
│ │ │ ├── ListWorkspacesCmdArgs.swift
│ │ │ ├── MacosNativeFullscreenCmdArgs.swift
│ │ │ ├── MacosNativeMinimizeCmdArgs.swift
│ │ │ ├── ModeCmdArgs.swift
│ │ │ ├── MoveCmdArgs.swift
│ │ │ ├── MoveMouseCmdArgs.swift
│ │ │ ├── MoveNodeToMonitorCmdArgs.swift
│ │ │ ├── MoveNodeToWorkspaceCmdArgs.swift
│ │ │ ├── MoveWorkpsaceToMonitorCmdArgs.swift
│ │ │ ├── ReloadConfigCmdArgs.swift
│ │ │ ├── ResizeCmdArgs.swift
│ │ │ ├── SplitCmdArgs.swift
│ │ │ ├── SummonWorkspaceCmdArgs.swift
│ │ │ ├── TriggerBindingCmdArgs.swift
│ │ │ ├── VolumeCmdArgs.swift
│ │ │ ├── WorkspaceBackAndForthCmdArgs.swift
│ │ │ └── WorkspaceCmdArgs.swift
│ │ ├── parseCmdArgs.swift
│ │ ├── parseSpecificCmdArgs.swift
│ │ ├── splitArgs.swift
│ │ └── subcommandParsers.swift
│ ├── cmdHelpGenerated.swift
│ ├── gitHashGenerated.swift
│ ├── macOs13Compatibility.swift
│ ├── model
│ │ ├── AxAppThreadToken.swift
│ │ ├── CardinalDirection.swift
│ │ ├── Init.swift
│ │ ├── MonitorDescription.swift
│ │ ├── Orientation.swift
│ │ ├── WorkspaceName.swift
│ │ └── clientServer.swift
│ ├── util
│ │ ├── AeroAny.swift
│ │ ├── BoolEx.swift
│ │ ├── CollectionEx.swift
│ │ ├── ConvenienceCopyable.swift
│ │ ├── EquatableNoop.swift
│ │ ├── JsonEncoderEx.swift
│ │ ├── Lateinit.swift
│ │ ├── OptionalEx.swift
│ │ ├── ResultEx.swift
│ │ ├── SequenceEx.swift
│ │ ├── StringEx.swift
│ │ ├── StringLogicalSegments.swift
│ │ ├── commonUtil.swift
│ │ └── showMessageInGui.swift
│ └── versionGenerated.swift
└── PrivateApi
│ └── include
│ ├── module.modulemap
│ ├── private.h
│ └── private.m
├── axDumps
├── about_this_mac.json5
├── alacritty_decorations_buttonless.json5
├── calculator.json5
├── choose_1_5_0.json5
├── chrome.json5
├── chrome_extensions_popup.json5
├── chrome_find_in_page.json5
├── chrome_pip.json5
├── finder.json5
├── firefox.json5
├── firefox_extensions_popup.json5
├── firefox_mouse_hover_extensions.json5
├── firefox_mouse_hover_tab.json5
├── firefox_non_native_fullscreen.json5
├── firefox_pinterest_sign_in_with_google.json5
├── firefox_pip.json5
├── ghostty.json5
├── ghostty_window_decoarations_false.json5
├── intellij.json5
├── intellij_background_tasks.json5
├── intellij_context_menu.json5
├── intellij_native_open_window.json5
├── intellij_quick_doc_popup.json5
├── intellij_rebase_dialog.json5
├── jetbrains_toolbox.json5
├── marta.json5
├── mpv_fullscreen.json5
├── mpv_windowed.json5
├── qutebrowser.json5
├── qutebrowser_context_menu.json5
├── safari.json5
├── safari_pinterest_sign_in_with_google.json5
├── spotify.json5
├── sublime_text_4.json5
├── system_settings.json5
├── telegram.json5
├── terminal_app.json5
├── vlc_empty.json5
├── vlc_video_playing.json5
├── vs_code.json5
├── xcode.json5
└── xcode_build_succeeded_popup.json5
├── build-debug.sh
├── build-docs.sh
├── build-release.sh
├── build-shell-completion.sh
├── dev-docs
├── architecture.md
└── development.md
├── docs
├── aerospace-balance-sizes.adoc
├── aerospace-close-all-windows-but-current.adoc
├── aerospace-close.adoc
├── aerospace-config.adoc
├── aerospace-debug-windows.adoc
├── aerospace-enable.adoc
├── aerospace-exec-and-forget.adoc
├── aerospace-flatten-workspace-tree.adoc
├── aerospace-focus-back-and-forth.adoc
├── aerospace-focus-monitor.adoc
├── aerospace-focus.adoc
├── aerospace-fullscreen.adoc
├── aerospace-join-with.adoc
├── aerospace-layout.adoc
├── aerospace-list-apps.adoc
├── aerospace-list-exec-env-vars.adoc
├── aerospace-list-modes.adoc
├── aerospace-list-monitors.adoc
├── aerospace-list-windows.adoc
├── aerospace-list-workspaces.adoc
├── aerospace-macos-native-fullscreen.adoc
├── aerospace-macos-native-minimize.adoc
├── aerospace-mode.adoc
├── aerospace-move-mouse.adoc
├── aerospace-move-node-to-monitor.adoc
├── aerospace-move-node-to-workspace.adoc
├── aerospace-move-workspace-to-monitor.adoc
├── aerospace-move.adoc
├── aerospace-reload-config.adoc
├── aerospace-resize.adoc
├── aerospace-split.adoc
├── aerospace-summon-workspace.adoc
├── aerospace-trigger-binding.adoc
├── aerospace-volume.adoc
├── aerospace-workspace-back-and-forth.adoc
├── aerospace-workspace.adoc
├── aerospace.adoc
├── assets
│ ├── h_accordion.png
│ ├── h_tiles.png
│ ├── icon.png
│ ├── monitor-arrangement-1-bad.svg
│ ├── monitor-arrangement-1-good.svg
│ ├── monitor-arrangement-2-bad.svg
│ ├── monitor-arrangement-2-good.svg
│ ├── tree.png
│ └── v_accordion.png
├── commands.adoc
├── config-examples
│ ├── default-config.toml
│ └── i3-like-config-example.toml
├── goodies.adoc
├── guide.adoc
├── index.html
└── util
│ ├── all-monitors-option.adoc
│ ├── conditional-arguments-header.adoc
│ ├── conditional-examples-header.adoc
│ ├── conditional-exit-code-header.adoc
│ ├── conditional-options-header.adoc
│ ├── conditional-output-format-header.adoc
│ ├── header.adoc
│ ├── man-attributes.adoc
│ ├── man-footer.adoc
│ ├── monitor-option.adoc
│ ├── site-attributes.adoc
│ ├── window-id-flag-desc.adoc
│ └── workspace-flag-desc.adoc
├── generate-shell-parser.sh
├── generate.sh
├── grammar
├── ShellLexer.g4
├── ShellParser.g4
└── commands-bnf-grammar.txt
├── install-from-sources.sh
├── legal
├── LICENSE.txt
├── README.md
└── third-party-license
│ ├── LICENSE-BlueSocket.txt
│ ├── LICENSE-HotKey.txt
│ ├── LICENSE-ISSoundAdditions.txt
│ ├── LICENSE-TOMLKIT.txt
│ ├── LICENSE-antlr.txt
│ ├── LICENSE-swift-collections.txt
│ └── LICENSE-tomlplusplus.txt
├── makefile
├── project.yml
├── resources
├── AeroSpace.entitlements
└── Assets.xcassets
│ ├── AccentColor.colorset
│ └── Contents.json
│ ├── AppIcon.appiconset
│ ├── Contents.json
│ └── icon.png
│ └── Contents.json
├── run-cli.sh
├── run-debug.sh
├── run-tests.sh
└── script
├── build-brew-cask.sh
├── check-uncommitted-files.sh
├── clean-project.sh
├── clean-xcode.sh
├── generate-cmd-help.sh
├── install-dep.sh
├── publish-release.sh
├── reset-accessibility-permission-for-debug.sh
└── setup.sh
/.bundle/config:
--------------------------------------------------------------------------------
1 | ---
2 | BUNDLE_PATH: ".deps/bundler-path"
3 | BUNDLE_DISABLE_SHARED_GEMS: "1"
4 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | # It's better to use "wrap line" in plain text documents
4 | [*.{adoc,md}]
5 | rulers = 1000
6 | max_line_length = 1000
7 |
8 | [*.toml]
9 | rulers = 96
10 | max_line_length = 96
11 |
12 | [*.{swift,sh}]
13 | rulers = 120
14 | max_line_length = 120
15 | indent_style = space
16 | indent_size = 4
17 | charset = utf-8
18 | trim_trailing_whitespace = true
19 | insert_final_newline = true
20 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # GitHub: linguist-generated marks paths that you would like to be ignored for the repository's language statistics and hidden by default in diffs
2 | AeroSpace.xcodeproj/project.pbxproj linguist-generated=true
3 |
--------------------------------------------------------------------------------
/.github/DISCUSSION_TEMPLATE/potential-bugs.yml:
--------------------------------------------------------------------------------
1 | body:
2 | - type: textarea
3 | id: body
4 | attributes:
5 | label: Body
6 | value: |
7 | Steps to reproduce:
8 | 1.
9 | 2.
10 | 3.
11 |
12 | Expected result:
13 | Actual result:
14 |
15 | ### Additional info
16 |
17 | ```shell
18 | $ aerospace -v
19 | ```
20 | validations:
21 | required: true
22 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: [nikitabobko]
2 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/new-issue.yml:
--------------------------------------------------------------------------------
1 | name: New Issue
2 | description: New Issue
3 | body:
4 | - type: markdown
5 | attributes:
6 | value: |
7 | Unfortunately, AeroSpace project doesn't openly accept Issues. See: https://github.com/nikitabobko/AeroSpace/issues/947
8 | - type: checkboxes
9 | id: checkbox
10 | attributes:
11 | label: '_'
12 | options:
13 | - label: |
14 | I read https://github.com/nikitabobko/AeroSpace/issues/947
15 | required: true
16 |
--------------------------------------------------------------------------------
/.github/workflows/close-third-party-issues.yml:
--------------------------------------------------------------------------------
1 | name: close-third-party-issues
2 |
3 | on:
4 | issues:
5 | types: [opened]
6 |
7 | jobs:
8 | close-third-party-issues:
9 | name: Close third party issues
10 | runs-on: ubuntu-latest
11 | permissions:
12 | issues: write
13 | steps:
14 | - name: Close third party issues
15 | run: |
16 | set -e # Exit if one of commands exit with non-zero exit code
17 | set -u # Treat unset variables and parameters other than the special parameters ‘@’ or ‘*’ as an error
18 | set -o pipefail # Any command failed in the pipe fails the whole pipe
19 |
20 | author="$(gh issue view "$ISSUE" --json author --jq '.author.login')"
21 |
22 | close() {
23 | gh issue edit "$ISSUE" --add-label bin
24 | gh issue close "$ISSUE" --comment "Please don't open issues directly, use GitHub Discussions instead. See: https://github.com/nikitabobko/AeroSpace/issues/947"
25 | gh issue lock "$ISSUE"
26 | }
27 |
28 | test "$author" = nikitabobko || close
29 | env:
30 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
31 | GH_REPO: ${{ github.repository }}
32 | ISSUE: ${{ github.event.issue.number }}
33 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.idea
2 | /.debug
3 | /.release
4 | /.shell-completion
5 | /.site
6 | /.man
7 | /Gemfile.lock
8 | # IDK, AppCode randomly creates this EMPTY file. I have no idea what this is
9 | /default.profraw
10 | .DS_Store
11 | /.xcode-build
12 | # Swift package manager
13 | /.build
14 | /.swiftpm
15 | /.vscode
16 | # External dependencies
17 | /.deps
18 |
19 | # For whatever local files that developers might want to keep there (I personally keep a separate `generated-html` git worktree here)
20 | /.local
21 |
22 | # XCode User settings
23 | xcuserdata/
24 | xcshareddata/
25 |
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.sourcekit-lsp/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "backgroundIndexing": false,
3 | "backgroundPreparationMode": "build"
4 | }
5 |
--------------------------------------------------------------------------------
/.swift-version:
--------------------------------------------------------------------------------
1 | 6.1.0
2 |
--------------------------------------------------------------------------------
/.swiftformat:
--------------------------------------------------------------------------------
1 | # https://github.com/nicklockwood/SwiftFormat/blob/main/Rules.md
2 | --exclude ./ShellParserGenerated
3 | --indentcase true
4 | --patternlet inline
5 | --indentstrings true
6 |
7 | # https://github.com/nicklockwood/SwiftFormat/issues/483 fix indentation for expressions nested in ternary
8 | --wrapternary before-operators
9 |
10 | --disable andOperator
11 | --disable blankLinesBetweenScopes
12 | --disable consecutiveSpaces
13 | --disable consistentSwitchCaseSpacing
14 | --disable hoistAwait
15 | --disable hoistTry
16 | --disable preferKeyPath
17 | --disable redundantInit
18 | --disable redundantNilInit
19 | --disable redundantParens
20 | --disable redundantRawValues
21 | --disable redundantReturn
22 | --disable redundantSelf
23 | --disable redundantStaticSelf
24 | --disable redundantType
25 | --disable sortImports
26 | --disable spaceAroundComments
27 | --disable spaceInsideComments
28 | --disable void
29 | --disable wrapArguments
30 | --disable wrapMultilineConditionalAssignment
31 |
32 | # This rule is cool but buggy. It feels like heuristics are used instead of real code analysis
33 | --disable unusedArguments
34 |
--------------------------------------------------------------------------------
/AeroSpace.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | ruby '~> 3.0' # >= 3.0 and < 4.0
3 |
4 | source "https://rubygems.org"
5 | gem 'asciidoctor', '2.0.23'
6 | gem 'pygments.rb', '3.0'
7 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Nikita Bobko
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "originHash" : "ce497cf9fcf14272fedb221cc48e1102e7d3c1bb2ab918cedfbfa9120f32fca3",
3 | "pins" : [
4 | {
5 | "identity" : "antlr4",
6 | "kind" : "remoteSourceControl",
7 | "location" : "https://github.com/antlr/antlr4",
8 | "state" : {
9 | "revision" : "7ed420ff2c78d62883875c442d75f32e73bc86c8",
10 | "version" : "4.13.1"
11 | }
12 | },
13 | {
14 | "identity" : "bluesocket",
15 | "kind" : "remoteSourceControl",
16 | "location" : "https://github.com/Kitura/BlueSocket.git",
17 | "state" : {
18 | "revision" : "7b23a867008e0027bfd6f4d398d44720707bc8ca",
19 | "version" : "2.0.4"
20 | }
21 | },
22 | {
23 | "identity" : "hotkey",
24 | "kind" : "remoteSourceControl",
25 | "location" : "https://github.com/soffes/HotKey",
26 | "state" : {
27 | "revision" : "a3cf605d7a96f6ff50e04fcb6dea6e2613cfcbe4",
28 | "version" : "0.2.1"
29 | }
30 | },
31 | {
32 | "identity" : "issoundadditions",
33 | "kind" : "remoteSourceControl",
34 | "location" : "https://github.com/InerziaSoft/ISSoundAdditions",
35 | "state" : {
36 | "revision" : "4b555f0354e6c280917bae8a598a258efe87ab98",
37 | "version" : "2.0.1"
38 | }
39 | },
40 | {
41 | "identity" : "swift-collections",
42 | "kind" : "remoteSourceControl",
43 | "location" : "https://github.com/apple/swift-collections",
44 | "state" : {
45 | "revision" : "671108c96644956dddcd89dd59c203dcdb36cec7",
46 | "version" : "1.1.4"
47 | }
48 | },
49 | {
50 | "identity" : "tomlkit",
51 | "kind" : "remoteSourceControl",
52 | "location" : "https://github.com/LebJe/TOMLKit",
53 | "state" : {
54 | "revision" : "404c4dd011743461bff12d00a5118d0ed59d630c",
55 | "version" : "0.5.5"
56 | }
57 | }
58 | ],
59 | "version" : 3
60 | }
61 |
--------------------------------------------------------------------------------
/ShellParserGenerated/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | xcuserdata/
5 | DerivedData/
6 | .swiftpm/configuration/registries.json
7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
8 | .netrc
9 |
--------------------------------------------------------------------------------
/ShellParserGenerated/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.9
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "ShellParserGenerated",
8 | products: [
9 | // Products define the executables and libraries a package produces, making them visible to other packages.
10 | .library(
11 | name: "ShellParserGenerated",
12 | targets: ["ShellParserGenerated"]
13 | ),
14 | ],
15 | dependencies: [
16 | .package(url: "https://github.com/antlr/antlr4", exact: "4.13.1"),
17 | ],
18 | targets: [
19 | // Targets are the basic building blocks of a package, defining a module or a test suite.
20 | // Targets can depend on other targets in this package and products from dependencies.
21 | .target(
22 | name: "ShellParserGenerated",
23 | dependencies: [
24 | .product(name: "Antlr4Static", package: "antlr4"),
25 | ]
26 | ),
27 | ]
28 | )
29 |
--------------------------------------------------------------------------------
/Sources/AeroSpaceApp/AeroSpaceApp.swift:
--------------------------------------------------------------------------------
1 | import AppBundle
2 | import SwiftUI
3 |
4 | // This file is shared between SPM and xcode project
5 |
6 | @MainActor // macOS 13
7 | @main
8 | struct AeroSpaceApp: App {
9 | @MainActor // macOS 13
10 | @StateObject var viewModel = TrayMenuModel.shared
11 |
12 | init() {
13 | initAppBundle()
14 | }
15 |
16 | @MainActor // macOS 13
17 | var body: some Scene {
18 | menuBar(viewModel: viewModel)
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/AppBundle/command/CmdEnv.swift:
--------------------------------------------------------------------------------
1 | import Common
2 |
3 | struct CmdEnv: ConvenienceCopyable { // todo forward env from cli to server
4 | var windowId: UInt32?
5 | var workspaceName: String?
6 | var pwd: String?
7 |
8 | static var defaultEnv: CmdEnv { CmdEnv(windowId: nil, workspaceName: nil, pwd: nil) }
9 | init(
10 | windowId: UInt32?,
11 | workspaceName: String?,
12 | pwd: String?
13 | ) {
14 | self.windowId = windowId
15 | self.workspaceName = workspaceName
16 | self.pwd = pwd
17 | }
18 |
19 | func withFocus(_ focus: LiveFocus) -> CmdEnv {
20 | switch focus.asLeaf {
21 | case .window(let wd): .defaultEnv.copy(\.windowId, wd.windowId)
22 | case .emptyWorkspace(let ws): .defaultEnv.copy(\.workspaceName, ws.name)
23 | }
24 | }
25 |
26 | @MainActor
27 | var asMap: [String: String] {
28 | var result = config.execConfig.envVariables
29 | if let pwd {
30 | result["PWD"] = pwd
31 | }
32 | if let windowId {
33 | result[AEROSPACE_WINDOW_ID] = windowId.description
34 | }
35 | if let workspaceName {
36 | result[AEROSPACE_WORKSPACE] = workspaceName.description
37 | }
38 | return result
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/Sources/AppBundle/command/CmdIo.swift:
--------------------------------------------------------------------------------
1 | class CmdStdin {
2 | private var input: String = ""
3 | init(_ input: String) {
4 | self.input = input
5 | }
6 | static var emptyStdin: CmdStdin { .init("") }
7 |
8 | func readAll() -> String {
9 | let result = input
10 | input = ""
11 | return result
12 | }
13 | }
14 |
15 | class CmdIo {
16 | private var stdin: CmdStdin
17 | var stdout: [String] = []
18 | var stderr: [String] = []
19 |
20 | init(stdin: CmdStdin) { self.stdin = stdin }
21 |
22 | @discardableResult func out(_ msg: String) -> Bool { stdout.append(msg); return true }
23 | @discardableResult func err(_ msg: String) -> Bool { stderr.append(msg); return false }
24 | @discardableResult func out(_ msg: [String]) -> Bool { stdout += msg; return true }
25 | @discardableResult func err(_ msg: [String]) -> Bool { stderr += msg; return false }
26 |
27 | func readStdin() -> String { stdin.readAll() }
28 | }
29 |
30 | struct CmdResult {
31 | let stdout: [String]
32 | let stderr: [String]
33 | let exitCode: Int32
34 | }
35 |
--------------------------------------------------------------------------------
/Sources/AppBundle/command/Command.swift:
--------------------------------------------------------------------------------
1 | import AppKit
2 | import Common
3 |
4 | protocol Command: AeroAny, Equatable, Sendable {
5 | associatedtype T where T: CmdArgs
6 | var args: T { get }
7 | @MainActor
8 | func run(_ env: CmdEnv, _ io: CmdIo) async throws -> Bool
9 | }
10 |
11 | extension Command {
12 | static func == (lhs: Self, rhs: Self) -> Bool {
13 | return lhs.args.equals(rhs.args)
14 | }
15 |
16 | nonisolated func equals(_ other: any Command) -> Bool {
17 | (other as? Self).flatMap { self == $0 } ?? false
18 | }
19 | }
20 |
21 | extension Command {
22 | var info: CmdStaticInfo { T.info }
23 | }
24 |
25 | extension Command {
26 | @MainActor
27 | @discardableResult
28 | func run(_ env: CmdEnv, _ stdin: CmdStdin) async throws -> CmdResult {
29 | return try await [self].runCmdSeq(env, stdin)
30 | }
31 |
32 | var isExec: Bool { self is ExecAndForgetCommand }
33 | }
34 |
35 | // There are 4 entry points for running commands:
36 | // 1. config keybindings
37 | // 2. CLI requests to server
38 | // 3. on-window-detected callback
39 | // 4. Tray icon buttons
40 | extension [Command] {
41 | @MainActor
42 | func runCmdSeq(_ env: CmdEnv, _ io: sending CmdIo) async throws -> Bool {
43 | var isSucc = true
44 | for command in self {
45 | isSucc = try await command.run(env, io) && isSucc
46 | refreshModel()
47 | }
48 | return isSucc
49 | }
50 |
51 | @MainActor
52 | func runCmdSeq(_ env: CmdEnv, _ stdin: CmdStdin) async throws -> CmdResult {
53 | let io: CmdIo = CmdIo(stdin: stdin)
54 | let isSucc = try await runCmdSeq(env, io)
55 | return CmdResult(stdout: io.stdout, stderr: io.stderr, exitCode: isSucc ? 0 : 1)
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/Sources/AppBundle/command/cmdResolveTargetOrReportError.swift:
--------------------------------------------------------------------------------
1 | import Common
2 |
3 | extension CmdArgs {
4 | @MainActor
5 | var workspace: Workspace? {
6 | if let workspaceName { Workspace.get(byName: workspaceName.raw) } else { nil }
7 | }
8 |
9 | @MainActor
10 | func resolveTargetOrReportError(_ env: CmdEnv, _ io: CmdIo) -> LiveFocus? {
11 | // Flags
12 | if let windowId {
13 | if let wi = Window.get(byId: windowId) {
14 | return wi.toLiveFocusOrReportError(io)
15 | } else {
16 | io.err("Invalid \(windowId) passed to --window-id")
17 | return nil
18 | }
19 | }
20 | if let workspace {
21 | return workspace.toLiveFocus()
22 | }
23 | // Env
24 | if let windowId = env.windowId {
25 | if let wi = Window.get(byId: windowId) {
26 | return wi.toLiveFocusOrReportError(io)
27 | } else {
28 | io.err("Invalid \(windowId) specified in \(AEROSPACE_WINDOW_ID) env variable")
29 | return nil
30 | }
31 | }
32 | if let wsName = env.workspaceName {
33 | return Workspace.get(byName: wsName).toLiveFocus()
34 | }
35 | // Real Focus
36 | return focus
37 | }
38 | }
39 |
40 | extension Window {
41 | @MainActor
42 | func toLiveFocusOrReportError(_ io: CmdIo) -> LiveFocus? {
43 | if let result = toLiveFocusOrNil() {
44 | return result
45 | } else {
46 | io.err("Window \(windowId) doesn't belong to any monitor. And thus can't even define a focused workspace")
47 | return nil
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/Sources/AppBundle/command/formatToJson.swift:
--------------------------------------------------------------------------------
1 | import Common
2 | import Foundation
3 |
4 | extension [AeroObj] {
5 | @MainActor
6 | func formatToJson(_ format: [StringInterToken], ignoreRightPaddingVar: Bool) -> Result {
7 | var list: [[String: Primitive]] = []
8 | for richObj in self {
9 | var rawObj: [String: Primitive] = [:]
10 | for token in format {
11 | switch token {
12 | case .interVar(PlainInterVar.rightPadding.rawValue) where ignoreRightPaddingVar:
13 | break
14 | case .literal:
15 | break // should be spaces
16 | case .interVar(let varName):
17 | switch varName.expandFormatVar(obj: richObj) {
18 | case .success(let expanded): rawObj[varName] = expanded
19 | case .failure(let error): return .failure(error)
20 | }
21 | }
22 | }
23 | list.append(rawObj)
24 | }
25 | return JSONEncoder.aeroSpaceDefault.encodeToString(list).map(Result.success)
26 | ?? .failure("Can't encode '\(list)' to JSON")
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Sources/AppBundle/command/impl/BalanceSizesCommand.swift:
--------------------------------------------------------------------------------
1 | import AppKit
2 | import Common
3 | import Foundation
4 |
5 | struct BalanceSizesCommand: Command {
6 | let args: BalanceSizesCmdArgs
7 |
8 | func run(_ env: CmdEnv, _ io: CmdIo) -> Bool {
9 | guard let target = args.resolveTargetOrReportError(env, io) else { return false }
10 | balance(target.workspace.rootTilingContainer)
11 | return true
12 | }
13 | }
14 |
15 | @MainActor
16 | private func balance(_ parent: TilingContainer) {
17 | for child in parent.children {
18 | switch parent.layout {
19 | case .tiles: child.setWeight(parent.orientation, 1)
20 | case .accordion: break // Do nothing
21 | }
22 | if let child = child as? TilingContainer {
23 | balance(child)
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Sources/AppBundle/command/impl/CloseAllWindowsButCurrentCommand.swift:
--------------------------------------------------------------------------------
1 | import AppKit
2 | import Common
3 |
4 | struct CloseAllWindowsButCurrentCommand: Command {
5 | let args: CloseAllWindowsButCurrentCmdArgs
6 |
7 | func run(_ env: CmdEnv, _ io: CmdIo) async throws -> Bool {
8 | guard let target = args.resolveTargetOrReportError(env, io) else { return false }
9 | guard let focused = target.windowOrNil else {
10 | return io.err("Empty workspace")
11 | }
12 | guard let workspace = focused.nodeWorkspace else {
13 | return io.err("Focused window '\(focused.windowId)' doesn't belong to workspace")
14 | }
15 | var result = true
16 | for window in workspace.allLeafWindowsRecursive where window != focused {
17 | result = try await CloseCommand(args: args.closeArgs).run(env.copy(\.windowId, window.windowId), io) && result
18 | }
19 | return result
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Sources/AppBundle/command/impl/CloseCommand.swift:
--------------------------------------------------------------------------------
1 | import AppKit
2 | import Common
3 |
4 | struct CloseCommand: Command {
5 | let args: CloseCmdArgs
6 |
7 | func run(_ env: CmdEnv, _ io: CmdIo) async throws -> Bool {
8 | try await allowOnlyCancellationError {
9 | guard let target = args.resolveTargetOrReportError(env, io) else { return false }
10 | guard let window = target.windowOrNil else {
11 | return io.err("Empty workspace")
12 | }
13 | // Access ax directly. Not cool :(
14 | if try await args.quitIfLastWindow.andAsync({ @MainActor @Sendable in try await window.macAppUnsafe.getAxWindowsCount() == 1 }) {
15 | let app = window.macAppUnsafe
16 | if app.nsApp.terminate() {
17 | for workspace in Workspace.all {
18 | for window in workspace.allLeafWindowsRecursive where window.app.pid == app.pid {
19 | (window as! MacWindow).garbageCollect(skipClosedWindowsCache: true)
20 | }
21 | }
22 | return true
23 | } else {
24 | return io.err("Failed to quit '\(window.app.name ?? "Unknown app")'")
25 | }
26 | } else {
27 | window.closeAxWindow()
28 | return true
29 | }
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/Sources/AppBundle/command/impl/EnableCommand.swift:
--------------------------------------------------------------------------------
1 | import AppKit
2 | import Common
3 |
4 | struct EnableCommand: Command {
5 | let args: EnableCmdArgs
6 |
7 | func run(_ env: CmdEnv, _ io: CmdIo) async throws -> Bool {
8 | let prevState = TrayMenuModel.shared.isEnabled
9 | let newState: Bool = switch args.targetState.val {
10 | case .on: true
11 | case .off: false
12 | case .toggle: !TrayMenuModel.shared.isEnabled
13 | }
14 | if newState == prevState {
15 | io.out((newState ? "Already enabled" : "Already disabled") +
16 | "Tip: use --fail-if-noop to exit with non-zero code")
17 | return !args.failIfNoop
18 | }
19 |
20 | TrayMenuModel.shared.isEnabled = newState
21 | if newState {
22 | for workspace in Workspace.all {
23 | for window in workspace.allLeafWindowsRecursive where window.isFloating {
24 | window.lastFloatingSize = try await window.getAxSize() ?? window.lastFloatingSize
25 | }
26 | }
27 | activateMode(mainModeId)
28 | } else {
29 | activateMode(nil)
30 | }
31 | return true
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/Sources/AppBundle/command/impl/ExecAndForgetCommand.swift:
--------------------------------------------------------------------------------
1 | import AppKit
2 | import Common
3 |
4 | struct ExecAndForgetCommand: Command {
5 | let args: ExecAndForgetCmdArgs
6 |
7 | func run(_ env: CmdEnv, _ io: CmdIo) -> Bool {
8 | // todo shall exec-and-forget fork exec session?
9 | // It doesn't throw if exit code is non-zero
10 | let process = Process()
11 | process.environment = config.execConfig.envVariables
12 | process.executableURL = URL(filePath: "/bin/bash")
13 | process.arguments = ["-c", args.bashScript]
14 | return Result { try process.run() }.isSuccess
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Sources/AppBundle/command/impl/FlattenWorkspaceTreeCommand.swift:
--------------------------------------------------------------------------------
1 | import AppKit
2 | import Common
3 |
4 | struct FlattenWorkspaceTreeCommand: Command {
5 | let args: FlattenWorkspaceTreeCmdArgs
6 |
7 | func run(_ env: CmdEnv, _ io: CmdIo) -> Bool {
8 | guard let target = args.resolveTargetOrReportError(env, io) else { return false }
9 | let workspace = target.workspace
10 | let windows = workspace.rootTilingContainer.allLeafWindowsRecursive
11 | for window in windows {
12 | window.bind(to: workspace.rootTilingContainer, adaptiveWeight: 1, index: INDEX_BIND_LAST)
13 | }
14 | return true
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Sources/AppBundle/command/impl/FocusBackAndForthCommand.swift:
--------------------------------------------------------------------------------
1 | import AppKit
2 | import Common
3 |
4 | struct FocusBackAndForthCommand: Command {
5 | let args: FocusBackAndForthCmdArgs
6 |
7 | func run(_ env: CmdEnv, _ io: CmdIo) -> Bool {
8 | if let prevFocus {
9 | return setFocus(to: prevFocus)
10 | } else {
11 | return io.err("Prev window has been closed")
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/Sources/AppBundle/command/impl/FullscreenCommand.swift:
--------------------------------------------------------------------------------
1 | import AppKit
2 | import Common
3 |
4 | struct FullscreenCommand: Command {
5 | let args: FullscreenCmdArgs
6 |
7 | func run(_ env: CmdEnv, _ io: CmdIo) -> Bool {
8 | guard let target = args.resolveTargetOrReportError(env, io) else { return false }
9 | guard let window = target.windowOrNil else {
10 | return io.err(noWindowIsFocused)
11 | }
12 | let newState: Bool = switch args.toggle {
13 | case .on: true
14 | case .off: false
15 | case .toggle: !window.isFullscreen
16 | }
17 | if newState == window.isFullscreen {
18 | io.err((newState ? "Already fullscreen. " : "Already not fullscreen. ") +
19 | "Tip: use --fail-if-noop to exit with non-zero code")
20 | return !args.failIfNoop
21 | }
22 | window.isFullscreen = newState
23 | window.noOuterGapsInFullscreen = args.noOuterGaps
24 |
25 | // Focus on its own workspace
26 | window.markAsMostRecentChild()
27 | return true
28 | }
29 | }
30 |
31 | let noWindowIsFocused = "No window is focused"
32 |
--------------------------------------------------------------------------------
/Sources/AppBundle/command/impl/JoinWithCommand.swift:
--------------------------------------------------------------------------------
1 | import AppKit
2 | import Common
3 |
4 | struct JoinWithCommand: Command {
5 | let args: JoinWithCmdArgs
6 |
7 | func run(_ env: CmdEnv, _ io: CmdIo) -> Bool {
8 | let direction = args.direction.val
9 | guard let target = args.resolveTargetOrReportError(env, io) else { return false }
10 | guard let currentWindow = target.windowOrNil else {
11 | return io.err(noWindowIsFocused)
12 | }
13 | guard let (parent, ownIndex) = currentWindow.closestParent(hasChildrenInDirection: direction, withLayout: nil) else {
14 | return io.err("No windows in specified direction")
15 | }
16 | let joinWithTarget = parent.children[ownIndex + direction.focusOffset]
17 | let prevBinding = joinWithTarget.unbindFromParent()
18 | let newParent = TilingContainer(
19 | parent: parent,
20 | adaptiveWeight: prevBinding.adaptiveWeight,
21 | parent.orientation.opposite,
22 | .tiles,
23 | index: prevBinding.index
24 | )
25 | currentWindow.unbindFromParent()
26 |
27 | joinWithTarget.bind(to: newParent, adaptiveWeight: WEIGHT_AUTO, index: 0)
28 | currentWindow.bind(to: newParent, adaptiveWeight: WEIGHT_AUTO, index: direction.isPositive ? 0 : INDEX_BIND_LAST)
29 | return true
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Sources/AppBundle/command/impl/ListAppsCommand.swift:
--------------------------------------------------------------------------------
1 | import AppKit
2 | import Common
3 |
4 | struct ListAppsCommand: Command {
5 | let args: ListAppsCmdArgs
6 |
7 | func run(_ env: CmdEnv, _ io: CmdIo) -> Bool {
8 | var result = Array(MacApp.allAppsMap.values)
9 | if let hidden = args.macosHidden {
10 | result = result.filter { $0.nsApp.isHidden == hidden }
11 | }
12 |
13 | if args.outputOnlyCount {
14 | return io.out("\(result.count)")
15 | } else {
16 | let list = result.map { AeroObj.app($0) }
17 | if args.json {
18 | return switch list.formatToJson(args.format, ignoreRightPaddingVar: args._format.isEmpty) {
19 | case .success(let json): io.out(json)
20 | case .failure(let msg): io.err(msg)
21 | }
22 | } else {
23 | return switch list.format(args.format) {
24 | case .success(let lines): io.out(lines)
25 | case .failure(let msg): io.err(msg)
26 | }
27 | }
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Sources/AppBundle/command/impl/ListExecEnvVarsCommand.swift:
--------------------------------------------------------------------------------
1 | import AppKit
2 | import Common
3 |
4 | struct ListExecEnvVarsCommand: Command {
5 | let args: ListExecEnvVarsCmdArgs
6 |
7 | func run(_ env: CmdEnv, _ io: CmdIo) -> Bool {
8 | for (key, value) in config.execConfig.envVariables {
9 | io.out("\(key)=\(value)")
10 | }
11 | return true
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Sources/AppBundle/command/impl/ListModesCommand.swift:
--------------------------------------------------------------------------------
1 | import AppKit
2 | import Common
3 |
4 | struct ListModesCommand: Command {
5 | let args: ListModesCmdArgs
6 |
7 | func run(_ env: CmdEnv, _ io: CmdIo) -> Bool {
8 | if args.current {
9 | return io.out(activeMode ?? mainModeId)
10 | } else {
11 | let modeNames: [String] = config.modes.map { $0.key }
12 | return io.out(modeNames)
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Sources/AppBundle/command/impl/ListMonitorsCommand.swift:
--------------------------------------------------------------------------------
1 | import AppKit
2 | import Common
3 |
4 | struct ListMonitorsCommand: Command {
5 | let args: ListMonitorsCmdArgs
6 |
7 | func run(_ env: CmdEnv, _ io: CmdIo) -> Bool {
8 | let focus = focus
9 | var result = sortedMonitors
10 | if let focused = args.focused {
11 | result = result.filter { (monitor) in (monitor.activeWorkspace == focus.workspace) == focused }
12 | }
13 | if let mouse = args.mouse {
14 | let mouseWorkspace = mouseLocation.monitorApproximation.activeWorkspace
15 | result = result.filter { (monitor) in (monitor.activeWorkspace == mouseWorkspace) == mouse }
16 | }
17 |
18 | if args.outputOnlyCount {
19 | return io.out("\(result.count)")
20 | } else {
21 | let list = result.map { AeroObj.monitor($0) }
22 | if args.json {
23 | return switch list.formatToJson(args.format, ignoreRightPaddingVar: args._format.isEmpty) {
24 | case .success(let json): io.out(json)
25 | case .failure(let msg): io.err(msg)
26 | }
27 | } else {
28 | return switch list.format(args.format) {
29 | case .success(let lines): io.out(lines)
30 | case .failure(let msg): io.err(msg)
31 | }
32 | }
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Sources/AppBundle/command/impl/MacosNativeFullscreenCommand.swift:
--------------------------------------------------------------------------------
1 | import AppKit
2 | import Common
3 |
4 | /// Problem ID-B6E178F2: It's not first-class citizen command in AeroSpace model, since it interacts with macOS API directly.
5 | /// Consecutive macos-native-fullscreen commands may not works as expected (because macOS may report correct state with a
6 | /// delay), or may flicker
7 | ///
8 | /// The same applies to macos-native-minimize command
9 | struct MacosNativeFullscreenCommand: Command {
10 | let args: MacosNativeFullscreenCmdArgs
11 |
12 | func run(_ env: CmdEnv, _ io: CmdIo) async throws -> Bool {
13 | guard let target = args.resolveTargetOrReportError(env, io) else { return false }
14 | guard let window = target.windowOrNil else {
15 | return io.err(noWindowIsFocused)
16 | }
17 | let prevState = try await window.isMacosFullscreen
18 | let newState: Bool = switch args.toggle {
19 | case .on: true
20 | case .off: false
21 | case .toggle: !prevState
22 | }
23 | if newState == prevState {
24 | io.err((newState ? "Already fullscreen. " : "Already not fullscreen. ") +
25 | "Tip: use --fail-if-noop to exit with non-zero exit code")
26 | return !args.failIfNoop
27 | }
28 | window.asMacWindow().setNativeFullscreen(newState)
29 | guard let workspace = window.visualWorkspace else {
30 | return io.err(windowIsntPartOfTree(window))
31 | }
32 | if newState { // Enter fullscreen
33 | window.bind(to: workspace.macOsNativeFullscreenWindowsContainer, adaptiveWeight: 1, index: INDEX_BIND_LAST)
34 | } else { // Exit fullscreen
35 | switch window.layoutReason {
36 | case .macos(let prevParentKind):
37 | try await exitMacOsNativeUnconventionalState(window: window, prevParentKind: prevParentKind, workspace: workspace)
38 | default:
39 | try await window.relayoutWindow(on: workspace)
40 | }
41 | }
42 | return true
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/Sources/AppBundle/command/impl/MacosNativeMinimizeCommand.swift:
--------------------------------------------------------------------------------
1 | import AppKit
2 | import Common
3 |
4 | /// See: MacosNativeFullscreenCommand. Problem ID-B6E178F2
5 | struct MacosNativeMinimizeCommand: Command {
6 | let args: MacosNativeMinimizeCmdArgs
7 |
8 | func run(_ env: CmdEnv, _ io: CmdIo) async throws -> Bool {
9 | // resolveTargetOrReportError on already minimized windows will alwyas fail
10 | // It would be easier if minimized windows were part of the workspace in tree hierarchy
11 | guard let target = args.resolveTargetOrReportError(env, io) else { return false }
12 | guard let window = target.windowOrNil else {
13 | return io.err(noWindowIsFocused)
14 | }
15 | let newState: Bool = try await !window.isMacosMinimized
16 | window.asMacWindow().setNativeMinimized(newState)
17 | if newState { // minimize
18 | window.bind(to: macosMinimizedWindowsContainer, adaptiveWeight: 1, index: INDEX_BIND_LAST)
19 | return true
20 | } else { // unminimize
21 | return io.err("The command is uncapable of unminimizing windows yet. Sorry") // dead code. should never be possible, see the comment above
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Sources/AppBundle/command/impl/ModeCommand.swift:
--------------------------------------------------------------------------------
1 | import AppKit
2 | import Common
3 |
4 | struct ModeCommand: Command {
5 | let args: ModeCmdArgs
6 |
7 | func run(_ env: CmdEnv, _ io: CmdIo) -> Bool {
8 | activateMode(args.targetMode.val)
9 | return true
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Sources/AppBundle/command/impl/MoveNodeToMonitorCommand.swift:
--------------------------------------------------------------------------------
1 | import AppKit
2 | import Common
3 |
4 | struct MoveNodeToMonitorCommand: Command {
5 | let args: MoveNodeToMonitorCmdArgs
6 |
7 | func run(_ env: CmdEnv, _ io: CmdIo) -> Bool {
8 | guard let target = args.resolveTargetOrReportError(env, io) else { return false }
9 | guard let window = target.windowOrNil else {
10 | return io.err(noWindowIsFocused)
11 | }
12 | guard let currentMonitor = window.nodeMonitor else {
13 | return io.err(windowIsntPartOfTree(window))
14 | }
15 | switch args.target.val.resolve(currentMonitor, wrapAround: args.wrapAround) {
16 | case .success(let targetMonitor):
17 | if let wName = WorkspaceName.parse(targetMonitor.activeWorkspace.name).getOrNil(appendErrorTo: &io.stderr) {
18 | let moveNodeToWorkspace = args.moveNodeToWorkspace.copy(\.target, .initialized(.direct(wName)))
19 | return MoveNodeToWorkspaceCommand(args: moveNodeToWorkspace).run(env, io)
20 | } else {
21 | return false
22 | }
23 | case .failure(let msg):
24 | return io.err(msg)
25 | }
26 | }
27 | }
28 |
29 | func windowIsntPartOfTree(_ window: Window) -> String {
30 | "Window \(window.windowId) is not part of tree (minimized or hidden)"
31 | }
32 |
--------------------------------------------------------------------------------
/Sources/AppBundle/command/impl/MoveNodeToWorkspaceCommand.swift:
--------------------------------------------------------------------------------
1 | import Common
2 |
3 | struct MoveNodeToWorkspaceCommand: Command {
4 | let args: MoveNodeToWorkspaceCmdArgs
5 |
6 | func run(_ env: CmdEnv, _ io: CmdIo) -> Bool {
7 | guard let target = args.resolveTargetOrReportError(env, io) else { return false }
8 | guard let window = target.windowOrNil else { return io.err(noWindowIsFocused) }
9 | let subjectWs = window.nodeWorkspace
10 | let targetWorkspace: Workspace
11 | switch args.target.val {
12 | case .relative(let isNext):
13 | guard let subjectWs else { return io.err("Window \(window.windowId) doesn't belong to any workspace") }
14 | let ws = getNextPrevWorkspace(
15 | current: subjectWs,
16 | isNext: isNext,
17 | wrapAround: args.wrapAround,
18 | stdin: io.readStdin(),
19 | target: target
20 | )
21 | guard let ws else { return io.err("Can't resolve next or prev workspace") }
22 | targetWorkspace = ws
23 | case .direct(let name):
24 | targetWorkspace = Workspace.get(byName: name.raw)
25 | }
26 | if subjectWs == targetWorkspace {
27 | io.err("Window '\(window.windowId)' already belongs to workspace '\(targetWorkspace.name)'. Tip: use --fail-if-noop to exit with non-zero code")
28 | return !args.failIfNoop
29 | }
30 | let targetContainer: NonLeafTreeNodeObject = window.isFloating ? targetWorkspace : targetWorkspace.rootTilingContainer
31 |
32 | window.bind(to: targetContainer, adaptiveWeight: WEIGHT_AUTO, index: INDEX_BIND_LAST)
33 | return args.focusFollowsWindow ? window.focusWindow() : true
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Sources/AppBundle/command/impl/MoveWorkspaceToMonitorCommand.swift:
--------------------------------------------------------------------------------
1 | import AppKit
2 | import Common
3 |
4 | struct MoveWorkspaceToMonitorCommand: Command {
5 | let args: MoveWorkspaceToMonitorCmdArgs
6 |
7 | func run(_ env: CmdEnv, _ io: CmdIo) -> Bool {
8 | guard let target = args.resolveTargetOrReportError(env, io) else { return false }
9 | let focusedWorkspace = target.workspace
10 | let prevMonitor = focusedWorkspace.workspaceMonitor
11 |
12 | switch args.target.val.resolve(target.workspace.workspaceMonitor, wrapAround: args.wrapAround) {
13 | case .success(let targetMonitor):
14 | if targetMonitor.monitorId == prevMonitor.monitorId {
15 | return true
16 | }
17 | if targetMonitor.setActiveWorkspace(focusedWorkspace) {
18 | let stubWorkspace = getStubWorkspace(for: prevMonitor)
19 | check(
20 | prevMonitor.setActiveWorkspace(stubWorkspace),
21 | "getStubWorkspace generated incompatible stub workspace (\(stubWorkspace)) for the monitor (\(prevMonitor)"
22 | )
23 | return true
24 | } else {
25 | return io.err(
26 | "Can't move workspace '\(focusedWorkspace.name)' to monitor '\(targetMonitor.name)'. workspace-to-monitor-force-assignment doesn't allow it"
27 | )
28 | }
29 | case .failure(let msg):
30 | return io.err(msg)
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/Sources/AppBundle/command/impl/ReloadConfigCommand.swift:
--------------------------------------------------------------------------------
1 | import AppKit
2 | import Common
3 |
4 | struct ReloadConfigCommand: Command {
5 | let args: ReloadConfigCmdArgs
6 |
7 | func run(_ env: CmdEnv, _ io: CmdIo) -> Bool {
8 | var stdout = ""
9 | let isOk = reloadConfig(args: args, stdout: &stdout)
10 | if !stdout.isEmpty {
11 | io.out(stdout)
12 | }
13 | return isOk
14 | }
15 | }
16 |
17 | @MainActor func reloadConfig(forceConfigUrl: URL? = nil) -> Bool {
18 | var devNull = ""
19 | return reloadConfig(forceConfigUrl: forceConfigUrl, stdout: &devNull)
20 | }
21 |
22 | @MainActor func reloadConfig(
23 | args: ReloadConfigCmdArgs = ReloadConfigCmdArgs(rawArgs: []),
24 | forceConfigUrl: URL? = nil,
25 | stdout: inout String
26 | ) -> Bool {
27 | switch readConfig(forceConfigUrl: forceConfigUrl) {
28 | case .success(let (parsedConfig, url)):
29 | if !args.dryRun {
30 | resetHotKeys()
31 | config = parsedConfig
32 | configUrl = url
33 | activateMode(activeMode)
34 | syncStartAtLogin()
35 | }
36 | return true
37 | case .failure(let msg):
38 | stdout.append(msg)
39 | if !args.noGui {
40 | showMessageInGui(
41 | filenameIfConsoleApp: nil,
42 | title: "AeroSpace Config Error",
43 | message: msg
44 | )
45 | }
46 | return false
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/Sources/AppBundle/command/impl/SummonWorkspaceCommand.swift:
--------------------------------------------------------------------------------
1 | import AppKit
2 | import Common
3 |
4 | struct SummonWorkspaceCommand: Command {
5 | let args: SummonWorkspaceCmdArgs
6 |
7 | func run(_ env: CmdEnv, _ io: CmdIo) -> Bool {
8 | let workspace = Workspace.get(byName: args.target.val.raw)
9 | let monitor = focus.workspace.workspaceMonitor
10 | if monitor.activeWorkspace == workspace {
11 | io.err("Workspace '\(workspace.name)' is already visible on the focused monitor. Tip: use --fail-if-noop to exit with non-zero code")
12 | return !args.failIfNoop
13 | }
14 | if monitor.setActiveWorkspace(workspace) {
15 | return workspace.focusWorkspace()
16 | } else {
17 | return io.err("Can't move workspace '\(workspace.name)' to monitor '\(monitor.name)'. workspace-to-monitor-force-assignment doesn't allow it")
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/AppBundle/command/impl/TriggerBindingCommand.swift:
--------------------------------------------------------------------------------
1 | import AppKit
2 | import Common
3 |
4 | struct TriggerBindingCommand: Command {
5 | let args: TriggerBindingCmdArgs
6 |
7 | func run(_ env: CmdEnv, _ io: CmdIo) async throws -> Bool {
8 | return if let mode = config.modes[args.mode] {
9 | if let binding = mode.bindings.values.first(where: { $0.descriptionWithKeyNotation == args.binding.val }) {
10 | // refreshSession is not needed since commands are already run in refreshSession
11 | try await binding.commands.runCmdSeq(env, io)
12 | } else {
13 | io.err("Binding '\(args.binding.val)' is not presented in mode '\(args.mode)'")
14 | }
15 | } else {
16 | io.err("Mode '\(args.mode)' doesn't exist. " +
17 | "Available modes: \(config.modes.keys.joined(separator: ","))")
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/AppBundle/command/impl/VolumeCommand.swift:
--------------------------------------------------------------------------------
1 | import AppKit
2 | import Common
3 | import ISSoundAdditions
4 |
5 | struct VolumeCommand: Command {
6 | let args: VolumeCmdArgs
7 |
8 | func run(_ env: CmdEnv, _ io: CmdIo) -> Bool {
9 | switch args.action.val {
10 | case .up:
11 | Sound.output.increaseVolume(by: 0.0625, autoMuteUnmute: true)
12 | case .down:
13 | Sound.output.decreaseVolume(by: 0.0625, autoMuteUnmute: true)
14 | case .muteToggle:
15 | Sound.output.isMuted.toggle()
16 | case .muteOn:
17 | Sound.output.isMuted = true
18 | case .muteOff:
19 | Sound.output.isMuted = false
20 | case .set(let int):
21 | Sound.output.setVolume(Float(int) / 100, autoMuteUnmute: true)
22 | }
23 | return true
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Sources/AppBundle/command/impl/WorkspaceBackAndForthCommand.swift:
--------------------------------------------------------------------------------
1 | import AppKit
2 | import Common
3 |
4 | struct WorkspaceBackAndForthCommand: Command {
5 | let args: WorkspaceBackAndForthCmdArgs
6 |
7 | func run(_ env: CmdEnv, _ io: CmdIo) -> Bool {
8 | return prevFocusedWorkspace?.focusWorkspace() != nil
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/Sources/AppBundle/command/parseCommand.swift:
--------------------------------------------------------------------------------
1 | import Common
2 | import TOMLKit
3 |
4 | func parseCommand(_ raw: String) -> ParsedCmd {
5 | if raw.starts(with: "exec-and-forget") {
6 | return .cmd(ExecAndForgetCommand(args: ExecAndForgetCmdArgs(bashScript: raw.removePrefix("exec-and-forget"))))
7 | }
8 | return switch raw.splitArgs() {
9 | case .success(let args): parseCommand(args)
10 | case .failure(let fail): .failure(fail)
11 | }
12 | }
13 |
14 | func parseCommand(_ args: [String]) -> ParsedCmd {
15 | parseCmdArgs(args).map { $0.toCommand() }
16 | }
17 |
18 | func expectedActualTypeError(expected: TOMLType, actual: TOMLType) -> String {
19 | "Expected type is '\(expected)'. But actual type is '\(actual)'"
20 | }
21 |
22 | func expectedActualTypeError(expected: [TOMLType], actual: TOMLType) -> String {
23 | if let single = expected.singleOrNil() {
24 | return expectedActualTypeError(expected: single, actual: actual)
25 | } else {
26 | return "Expected types are \(expected.map { "'\($0.description)'" }.joined(separator: " or ")). But actual type is '\(actual)'"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Sources/AppBundle/config/ConfigFile.swift:
--------------------------------------------------------------------------------
1 | import Common
2 | import Foundation
3 |
4 | let configDotfileName = isDebug ? ".aerospace-debug.toml" : ".aerospace.toml"
5 | func findCustomConfigUrl() -> ConfigFile {
6 | let fileName = isDebug ? "aerospace-debug.toml" : "aerospace.toml"
7 | let xdgConfigHome = ProcessInfo.processInfo.environment["XDG_CONFIG_HOME"]?.lets { URL(filePath: $0) }
8 | ?? FileManager.default.homeDirectoryForCurrentUser.appending(path: ".config/")
9 | let candidates: [URL] = if let configLocation = serverArgs.configLocation {
10 | [URL(filePath: configLocation)]
11 | } else {
12 | [
13 | FileManager.default.homeDirectoryForCurrentUser.appending(path: configDotfileName),
14 | xdgConfigHome.appending(path: "aerospace").appending(path: fileName),
15 | ]
16 | }
17 | let existingCandidates: [URL] = candidates.filter { (candidate: URL) in FileManager.default.fileExists(atPath: candidate.path) }
18 | let count = existingCandidates.count
19 | return switch count {
20 | case 0: .noCustomConfigExists
21 | case 1: .file(existingCandidates.first!)
22 | default: .ambiguousConfigError(existingCandidates)
23 | }
24 | }
25 |
26 | enum ConfigFile {
27 | case file(URL), ambiguousConfigError(_ candidates: [URL]), noCustomConfigExists
28 |
29 | var urlOrNil: URL? {
30 | return switch self {
31 | case .file(let url): url
32 | case .ambiguousConfigError, .noCustomConfigExists: nil
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Sources/AppBundle/config/Mode.swift:
--------------------------------------------------------------------------------
1 | import Common
2 | import HotKey
3 | import TOMLKit
4 |
5 | struct Mode: ConvenienceCopyable, Equatable, Sendable {
6 | /// User visible name. Optional. todo drop it?
7 | var name: String?
8 | var bindings: [String: HotkeyBinding]
9 |
10 | static let zero = Mode(name: nil, bindings: [:])
11 | }
12 |
13 | func parseModes(_ raw: TOMLValueConvertible, _ backtrace: TomlBacktrace, _ errors: inout [TomlParseError], _ mapping: [String: Key]) -> [String: Mode] {
14 | guard let rawTable = raw.table else {
15 | errors += [expectedActualTypeError(expected: .table, actual: raw.type, backtrace)]
16 | return [:]
17 | }
18 | var result: [String: Mode] = [:]
19 | for (key, value) in rawTable {
20 | result[key] = parseMode(value, backtrace + .key(key), &errors, mapping)
21 | }
22 | if !result.keys.contains(mainModeId) {
23 | errors += [.semantic(backtrace, "Please specify '\(mainModeId)' mode")]
24 | }
25 | return result
26 | }
27 |
28 | func parseMode(_ raw: TOMLValueConvertible, _ backtrace: TomlBacktrace, _ errors: inout [TomlParseError], _ mapping: [String: Key]) -> Mode {
29 | guard let rawTable: TOMLTable = raw.table else {
30 | errors += [expectedActualTypeError(expected: .table, actual: raw.type, backtrace)]
31 | return .zero
32 | }
33 |
34 | var result: Mode = .zero
35 | for (key, value) in rawTable {
36 | let backtrace = backtrace + .key(key)
37 | switch key {
38 | case "binding":
39 | result.bindings = parseBindings(value, backtrace, &errors, mapping)
40 | default:
41 | errors += [unknownKeyError(backtrace)]
42 | }
43 | }
44 | return result
45 | }
46 |
--------------------------------------------------------------------------------
/Sources/AppBundle/config/parseWorkspaceToMonitorAssignment.swift:
--------------------------------------------------------------------------------
1 | import Common
2 | import TOMLKit
3 |
4 | func parseWorkspaceToMonitorAssignment(_ raw: TOMLValueConvertible, _ backtrace: TomlBacktrace, _ errors: inout [TomlParseError]) -> [String: [MonitorDescription]] {
5 | guard let rawTable = raw.table else {
6 | errors += [expectedActualTypeError(expected: .table, actual: raw.type, backtrace)]
7 | return [:]
8 | }
9 | var result: [String: [MonitorDescription]] = [:]
10 | for (workspaceName, rawMonitorDescription) in rawTable {
11 | result[workspaceName] = parseMonitorDescriptions(rawMonitorDescription, backtrace + .key(workspaceName), &errors)
12 | }
13 | return result
14 | }
15 |
16 | func parseMonitorDescriptions(_ raw: TOMLValueConvertible, _ backtrace: TomlBacktrace, _ errors: inout [TomlParseError]) -> [MonitorDescription] {
17 | if let array = raw.array {
18 | return array.enumerated()
19 | .map { (index, rawDesc) in parseMonitorDescription(rawDesc, backtrace + .index(index)).getOrNil(appendErrorTo: &errors) }
20 | .filterNotNil()
21 | } else {
22 | return parseMonitorDescription(raw, backtrace).getOrNil(appendErrorTo: &errors).asList()
23 | }
24 | }
25 |
26 | func parseMonitorDescription(_ raw: TOMLValueConvertible, _ backtrace: TomlBacktrace) -> ParsedToml {
27 | let rawString: String
28 | if let string = raw.string {
29 | rawString = string
30 | } else if let int = raw.int {
31 | rawString = String(int)
32 | } else {
33 | return .failure(expectedActualTypeError(expected: [.string, .int], actual: raw.type, backtrace))
34 | }
35 |
36 | return parseMonitorDescription(rawString).toParsedToml(backtrace)
37 | }
38 |
--------------------------------------------------------------------------------
/Sources/AppBundle/config/startAtLogin.swift:
--------------------------------------------------------------------------------
1 | import AppKit
2 | import Common
3 |
4 | @MainActor
5 | func syncStartAtLogin() {
6 | let launchAgentsDir = FileManager.default.homeDirectoryForCurrentUser.appending(component: "Library/LaunchAgents/")
7 | Result { try FileManager.default.createDirectory(at: launchAgentsDir, withIntermediateDirectories: true) }.getOrDie()
8 | let url: URL = launchAgentsDir.appending(path: "bobko.aerospace.plist")
9 | if config.startAtLogin {
10 | let plist =
11 | """
12 |
13 |
14 |
15 |
16 | Label
17 | \(aeroSpaceAppId)
18 | ProgramArguments
19 |
20 | \(URL(filePath: CommandLine.arguments.first ?? dieT("Can't get first argument")).absoluteURL.path)
21 | --started-at-login
22 |
23 | RunAtLoad
24 |
25 |
26 |
27 | """
28 | if plist != (try? String(contentsOf: url)) {
29 | Result { try plist.write(to: url, atomically: false, encoding: .utf8) }.getOrDie("Can't write to \(url) ")
30 | }
31 | } else {
32 | try? FileManager.default.removeItem(at: url)
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/Sources/AppBundle/focusCache.swift:
--------------------------------------------------------------------------------
1 | @MainActor private var lastKnownNativeFocusedWindowId: UInt32? = nil
2 |
3 | /// The data should flow (from nativeFocused to focused) and
4 | /// (from nativeFocused to lastKnownNativeFocusedWindowId)
5 | /// Alternative names: takeFocusFromMacOs, syncFocusFromMacOs
6 | @MainActor func updateFocusCache(_ nativeFocused: Window?) {
7 | if nativeFocused?.windowId != lastKnownNativeFocusedWindowId {
8 | _ = nativeFocused?.focusWindow()
9 | lastKnownNativeFocusedWindowId = nativeFocused?.windowId
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Sources/AppBundle/getNativeFocusedWindow.swift:
--------------------------------------------------------------------------------
1 | import AppKit
2 | import Common
3 |
4 | @MainActor
5 | var appForTests: (any AbstractApp)? = nil
6 |
7 | @MainActor
8 | private var focusedApp: (any AbstractApp)? {
9 | get async throws {
10 | if isUnitTest {
11 | return appForTests
12 | } else {
13 | check(appForTests == nil)
14 | return try await NSWorkspace.shared.frontmostApplication.flatMapAsyncMainActor(MacApp.getOrRegister)
15 | }
16 | }
17 | }
18 |
19 | @MainActor
20 | func getNativeFocusedWindow() async throws -> Window? {
21 | try await focusedApp?.getFocusedWindow()
22 | }
23 |
--------------------------------------------------------------------------------
/Sources/AppBundle/model/MonitorDescriptionEx.swift:
--------------------------------------------------------------------------------
1 | import Common
2 |
3 | extension MonitorDescription {
4 | func resolveMonitor(sortedMonitors: [Monitor]) -> Monitor? {
5 | return switch self {
6 | case .sequenceNumber(let number): sortedMonitors.getOrNil(atIndex: number - 1)
7 | case .main: mainMonitor
8 | case .pattern(_, let regex): sortedMonitors.first { monitor in monitor.name.contains(regex.val) }
9 | case .secondary:
10 | sortedMonitors.takeIf { $0.count == 2 }?
11 | .first { $0.rect.topLeftCorner != mainMonitor.rect.topLeftCorner }
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/Sources/AppBundle/model/MonitorEx.swift:
--------------------------------------------------------------------------------
1 | extension Monitor {
2 | @MainActor
3 | var visibleRectPaddedByOuterGaps: Rect {
4 | let topLeft = visibleRect.topLeftCorner
5 | let gaps = ResolvedGaps(gaps: config.gaps, monitor: self)
6 | return Rect(
7 | topLeftX: topLeft.x + gaps.outer.left.toDouble(),
8 | topLeftY: topLeft.y + gaps.outer.top.toDouble(),
9 | width: visibleRect.width - gaps.outer.left.toDouble() - gaps.outer.right.toDouble(),
10 | height: visibleRect.height - gaps.outer.top.toDouble() - gaps.outer.bottom.toDouble()
11 | )
12 | }
13 |
14 | /// todo make 1-based
15 | /// 0-based index
16 | var monitorId: Int? {
17 | let sorted = sortedMonitors
18 | let origin = self.rect.topLeftCorner
19 | return sorted.firstIndex { $0.rect.topLeftCorner == origin }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Sources/AppBundle/model/Rect.swift:
--------------------------------------------------------------------------------
1 | import AppKit
2 | import Common
3 |
4 | struct Rect: ConvenienceCopyable {
5 | var topLeftX: CGFloat
6 | var topLeftY: CGFloat
7 | var width: CGFloat
8 | var height: CGFloat
9 | }
10 |
11 | extension CGRect {
12 | func monitorFrameNormalized() -> Rect {
13 | let mainMonitorHeight: CGFloat = mainMonitor.height
14 | let rect = toRect()
15 | return rect.copy(\.topLeftY, mainMonitorHeight - rect.topLeftY)
16 | }
17 | }
18 |
19 | extension CGRect {
20 | func toRect() -> Rect {
21 | Rect(topLeftX: minX, topLeftY: maxY, width: width, height: height)
22 | }
23 | }
24 |
25 | extension Rect {
26 | func contains(_ point: CGPoint) -> Bool {
27 | let x = point.x
28 | let y = point.y
29 | return (minX ..< maxX).contains(x) && (minY ..< maxY).contains(y)
30 | }
31 |
32 | var center: CGPoint {
33 | CGPoint(x: topLeftX + width / 2, y: topLeftY + height / 2)
34 | }
35 |
36 | var topLeftCorner: CGPoint { CGPoint(x: topLeftX, y: topLeftY) }
37 | var topRightCorner: CGPoint { CGPoint(x: maxX, y: minY) }
38 | var bottomRightCorner: CGPoint { CGPoint(x: maxX, y: maxY) }
39 | var bottomLeftCorner: CGPoint { CGPoint(x: minX, y: maxY) }
40 |
41 | var minY: CGFloat { topLeftY }
42 | var maxY: CGFloat { topLeftY + height }
43 | var minX: CGFloat { topLeftX }
44 | var maxX: CGFloat { topLeftX + width }
45 |
46 | var size: CGSize { CGSize(width: width, height: height) }
47 |
48 | func getDimension(_ orientation: Orientation) -> CGFloat { orientation == .h ? width : height }
49 | }
50 |
--------------------------------------------------------------------------------
/Sources/AppBundle/mouse/mouse.swift:
--------------------------------------------------------------------------------
1 | import AppKit
2 |
3 | @MainActor var currentlyManipulatedWithMouseWindowId: UInt32? = nil
4 | var isLeftMouseButtonDown: Bool { NSEvent.pressedMouseButtons == 1 }
5 |
6 | @MainActor
7 | func isManipulatedWithMouse(_ window: Window) async throws -> Bool {
8 | try await (!window.isHiddenInCorner && // Don't allow to resize/move windows of hidden workspaces
9 | isLeftMouseButtonDown &&
10 | (currentlyManipulatedWithMouseWindowId == nil || window.windowId == currentlyManipulatedWithMouseWindowId))
11 | .andAsync { @Sendable @MainActor in try await getNativeFocusedWindow() == window }
12 | }
13 |
14 | /// Same motivation as in monitorFrameNormalized
15 | var mouseLocation: CGPoint {
16 | let mainMonitorHeight: CGFloat = mainMonitor.height
17 | let location = NSEvent.mouseLocation
18 | return location.copy(\.y, mainMonitorHeight - location.y)
19 | }
20 |
--------------------------------------------------------------------------------
/Sources/AppBundle/tree/AbstractApp.swift:
--------------------------------------------------------------------------------
1 | import Common
2 |
3 | protocol AbstractApp: AnyObject, Hashable, AeroAny {
4 | var pid: Int32 { get }
5 | var bundleId: String? { get }
6 |
7 | @MainActor func getFocusedWindow() async throws -> Window?
8 | var name: String? { get }
9 | var execPath: String? { get }
10 | var bundlePath: String? { get }
11 | }
12 |
13 | extension AbstractApp {
14 | static func == (lhs: Self, rhs: Self) -> Bool {
15 | if lhs.pid == rhs.pid {
16 | check(lhs === rhs)
17 | return true
18 | } else {
19 | check(lhs !== rhs)
20 | return false
21 | }
22 | }
23 |
24 | func hash(into hasher: inout Hasher) {
25 | hasher.combine(pid)
26 | }
27 | }
28 |
29 | extension Window {
30 | var macAppUnsafe: MacApp { app as! MacApp }
31 | }
32 |
--------------------------------------------------------------------------------
/Sources/AppBundle/tree/MacosUnconventionalWindowsContainer.swift:
--------------------------------------------------------------------------------
1 | import Common
2 |
3 | class MacosFullscreenWindowsContainer: TreeNode, NonLeafTreeNodeObject {
4 | @MainActor
5 | init(parent: Workspace) {
6 | super.init(parent: parent, adaptiveWeight: 1, index: INDEX_BIND_LAST)
7 | }
8 | }
9 |
10 | /// The container for macOS windows of hidden apps
11 | class MacosHiddenAppsWindowsContainer: TreeNode, NonLeafTreeNodeObject {
12 | @MainActor
13 | init(parent: Workspace) {
14 | super.init(parent: parent, adaptiveWeight: 1, index: INDEX_BIND_LAST)
15 | }
16 | }
17 |
18 | @MainActor let macosMinimizedWindowsContainer = MacosMinimizedWindowsContainer()
19 | class MacosMinimizedWindowsContainer: TreeNode, NonLeafTreeNodeObject {
20 | @MainActor
21 | fileprivate init() {
22 | super.init(parent: NilTreeNode.instance, adaptiveWeight: 1, index: INDEX_BIND_LAST)
23 | }
24 | }
25 |
26 | @MainActor let macosPopupWindowsContainer = MacosPopupWindowsContainer()
27 | /// The container for macOS objects that are windows from AX perspective but from human perspective they are not even
28 | /// dialogs. E.g. Sonoma (macOS 14) keyboard layout switch
29 | class MacosPopupWindowsContainer: TreeNode, NonLeafTreeNodeObject {
30 | @MainActor
31 | fileprivate init() {
32 | super.init(parent: NilTreeNode.instance, adaptiveWeight: 1, index: INDEX_BIND_LAST)
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/Sources/AppBundle/tree/frozen/FrozenTreeNode.swift:
--------------------------------------------------------------------------------
1 | import AppKit
2 | import Common
3 |
4 | enum FrozenTreeNode: Sendable {
5 | case container(FrozenContainer)
6 | case window(FrozenWindow)
7 | }
8 |
9 | struct FrozenContainer: Sendable {
10 | let children: [FrozenTreeNode]
11 | let layout: Layout
12 | let orientation: Orientation
13 | let weight: CGFloat
14 |
15 | @MainActor init(_ container: TilingContainer) {
16 | children = container.children.map {
17 | switch $0.nodeCases {
18 | case .window(let w): .window(FrozenWindow(w))
19 | case .tilingContainer(let c): .container(FrozenContainer(c))
20 | case .workspace,
21 | .macosMinimizedWindowsContainer,
22 | .macosHiddenAppsWindowsContainer,
23 | .macosFullscreenWindowsContainer,
24 | .macosPopupWindowsContainer:
25 | illegalChildParentRelation(child: $0, parent: container)
26 | }
27 | }
28 | layout = container.layout
29 | orientation = container.orientation
30 | weight = getWeightOrNil(container) ?? 1
31 | }
32 | }
33 |
34 | struct FrozenWindow: Sendable {
35 | let id: UInt32
36 | let weight: CGFloat
37 |
38 | @MainActor init(_ window: Window) {
39 | id = window.windowId
40 | weight = getWeightOrNil(window) ?? 1
41 | }
42 | }
43 |
44 | @MainActor private func getWeightOrNil(_ node: TreeNode) -> CGFloat? {
45 | ((node.parent as? TilingContainer)?.orientation).map { node.getWeight($0) }
46 | }
47 |
--------------------------------------------------------------------------------
/Sources/AppBundle/tree/frozen/FrozenWorld.swift:
--------------------------------------------------------------------------------
1 | struct FrozenWorld {
2 | let workspaces: [FrozenWorkspace]
3 | let monitors: [FrozenMonitor]
4 | let windowIds: Set
5 |
6 | init(workspaces: [FrozenWorkspace], monitors: [FrozenMonitor]) {
7 | self.workspaces = workspaces
8 | self.monitors = monitors
9 | self.windowIds = workspaces.flatMap { collectAllWindowIds(workspace: $0) }.toSet()
10 | }
11 | }
12 |
13 | private func collectAllWindowIds(workspace: FrozenWorkspace) -> [UInt32] {
14 | workspace.floatingWindows.map { $0.id } +
15 | workspace.macosUnconventionalWindows.map { $0.id } +
16 | collectAllWindowIdsRecursive(node: .container(workspace.rootTilingNode))
17 | }
18 |
19 | private func collectAllWindowIdsRecursive(node: FrozenTreeNode) -> [UInt32] {
20 | switch node {
21 | case .window(let w): [w.id]
22 | case .container(let c):
23 | c.children.reduce(into: [UInt32]()) { partialResult, elem in
24 | partialResult += collectAllWindowIdsRecursive(node: elem)
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Sources/AppBundle/tree/normalizeContainers.swift:
--------------------------------------------------------------------------------
1 | extension Workspace {
2 | @MainActor func normalizeContainers() {
3 | rootTilingContainer.unbindEmptyAndAutoFlatten() // Beware! rootTilingContainer may change after this line of code
4 | if config.enableNormalizationOppositeOrientationForNestedContainers {
5 | rootTilingContainer.normalizeOppositeOrientationForNestedContainers()
6 | }
7 | }
8 | }
9 |
10 | private extension TilingContainer {
11 | @MainActor func unbindEmptyAndAutoFlatten() {
12 | if let child = children.singleOrNil(), config.enableNormalizationFlattenContainers && (child is TilingContainer || !isRootContainer) {
13 | child.unbindFromParent()
14 | let mru = parent?.mostRecentChild
15 | let previousBinding = unbindFromParent()
16 | child.bind(to: previousBinding.parent, adaptiveWeight: previousBinding.adaptiveWeight, index: previousBinding.index)
17 | (child as? TilingContainer)?.unbindEmptyAndAutoFlatten()
18 | if mru != self {
19 | mru?.markAsMostRecentChild()
20 | } else {
21 | child.markAsMostRecentChild()
22 | }
23 | } else {
24 | for child in children {
25 | (child as? TilingContainer)?.unbindEmptyAndAutoFlatten()
26 | }
27 | if children.isEmpty && !isRootContainer {
28 | unbindFromParent()
29 | }
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/Sources/AppBundle/ui/AppearanceTheme.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import Common
3 |
4 | enum AppearanceTheme {
5 | case light
6 | case dark
7 |
8 | /// System Settings -> Appearance -> Light/Dark
9 | /// This is the theme representing how the UI should look inside the app (this might be different than the menu bar color)
10 | @MainActor
11 | static var current: AppearanceTheme {
12 | let name = NSApplication.shared.effectiveAppearance.name
13 | let isDarkAppearance = name == .vibrantDark ||
14 | name == .darkAqua ||
15 | name == .accessibilityHighContrastDarkAqua ||
16 | name == .accessibilityHighContrastVibrantDark
17 | return isDarkAppearance ? .dark : .light
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Sources/AppBundle/util/AeroAny.swift:
--------------------------------------------------------------------------------
1 | import AppKit
2 | import Common
3 |
4 | extension TreeNode: AeroAny {}
5 | extension CGFloat: AeroAny {}
6 | extension Rect: AeroAny {}
7 | extension AXUIElement: AeroAny {}
8 | extension CGPoint: AeroAny {}
9 |
--------------------------------------------------------------------------------
/Sources/AppBundle/util/ArrayEx.swift:
--------------------------------------------------------------------------------
1 | import Common
2 |
3 | extension Array {
4 | func singleOrNil(where predicate: (Self.Element) throws -> Bool) rethrows -> Self.Element? {
5 | var found: Self.Element? = nil
6 | for elem in self where try predicate(elem) {
7 | if found == nil {
8 | found = elem
9 | } else {
10 | return nil
11 | }
12 | }
13 | return found
14 | }
15 |
16 | func firstOrDie(where predicate: (Self.Element) throws -> Bool) rethrows -> Self.Element {
17 | try first(where: predicate) ?? dieT("Can't find the element")
18 | }
19 | }
20 |
21 | extension Array where Self.Element: Equatable {
22 | @discardableResult
23 | mutating func remove(element: Self.Element) -> Int? {
24 | if let index = firstIndex(of: element) {
25 | remove(at: index)
26 | return index
27 | } else {
28 | return nil
29 | }
30 | }
31 | }
32 |
33 | func - (lhs: [T], rhs: [T]) -> [T] where T: Hashable {
34 | let r = rhs.toSet()
35 | return lhs.filter { !r.contains($0) }
36 | }
37 |
--------------------------------------------------------------------------------
/Sources/AppBundle/util/AxUiElementMock.swift:
--------------------------------------------------------------------------------
1 | import AppKit
2 | import Common
3 |
4 | /// Alternative name: AttrAddressibleStorage
5 | protocol AxUiElementMock {
6 | func get(_ attr: Attr) -> Attr.T?
7 | func containingWindowId() -> CGWindowID?
8 | }
9 |
10 | extension AxUiElementMock {
11 | var cast: AXUIElement { self as! AXUIElement }
12 | }
13 |
--------------------------------------------------------------------------------
/Sources/AppBundle/util/LazySequenceProtocolEx.swift:
--------------------------------------------------------------------------------
1 | extension LazySequenceProtocol {
2 | func filterNotNil() -> LazyMapSequence.Elements, Unwrapped> where Element == Unwrapped? {
3 | filter { $0 != nil }.map { $0! }
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/Sources/AppBundle/util/MruStack.swift:
--------------------------------------------------------------------------------
1 | /// Stack with most recently element on top
2 | class MruStack: Sequence {
3 | typealias Element = T
4 |
5 | private var mruNode: Node? = nil
6 |
7 | func makeIterator() -> MruStackIterator {
8 | MruStackIterator(mruNode)
9 | }
10 |
11 | var mostRecent: T? { mruNode?.value }
12 |
13 | func pushOrRaise(_ value: T) {
14 | remove(value)
15 | mruNode = Node(value, mruNode)
16 | }
17 |
18 | @discardableResult
19 | func remove(_ value: T) -> Bool {
20 | var prev: Node? = nil
21 | var current = mruNode
22 | while let cur = current {
23 | if cur.value == value {
24 | if let prev {
25 | prev.next = cur.next
26 | } else {
27 | mruNode = current?.next
28 | }
29 | cur.next = nil
30 | return true
31 | }
32 | prev = cur
33 | current = cur.next
34 | }
35 | return false
36 | }
37 | }
38 |
39 | extension MruStack where T: Hashable {
40 | var mruIndexMap: [T: Int] {
41 | var result: [T: Int] = [:]
42 | for (index, value) in enumerated() {
43 | result[value] = index
44 | }
45 | return result
46 | }
47 | }
48 |
49 | struct MruStackIterator: IteratorProtocol {
50 | typealias Element = T
51 | private var current: Node?
52 |
53 | fileprivate init(_ current: Node?) {
54 | self.current = current
55 | }
56 |
57 | mutating func next() -> T? {
58 | let result = current?.value
59 | current = current?.next
60 | return result
61 | }
62 | }
63 |
64 | private class Node {
65 | var next: Node? = nil
66 | let value: T
67 |
68 | init(_ value: T, _ next: Node?) {
69 | self.value = value
70 | self.next = next
71 | }
72 |
73 | init(_ value: T) {
74 | self.value = value
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/Sources/AppBundle/util/NSRunningApplicationEx.swift:
--------------------------------------------------------------------------------
1 | import AppKit
2 |
3 | extension NSRunningApplication {
4 | var idForDebug: String {
5 | "PID: \(processIdentifier) ID: \(bundleIdentifier ?? executableURL?.description ?? "")"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/Sources/AppBundle/util/NsApplicationEx.swift:
--------------------------------------------------------------------------------
1 | import AppKit
2 |
3 | extension NSApplication.ActivationPolicy {
4 | var prettyDescription: String {
5 | switch self {
6 | case .accessory: "accessory"
7 | case .prohibited: " prohibited"
8 | case .regular: "regular"
9 | @unknown default: "unknown \(self.rawValue)"
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Sources/AppBundle/util/SetEx.swift:
--------------------------------------------------------------------------------
1 | extension Set {
2 | func toArray() -> [Element] { Array(self) }
3 |
4 | @inlinable static func += (lhs: inout Set, rhs: any Sequence) {
5 | lhs.formUnion(rhs)
6 | }
7 |
8 | @inlinable static func -= (lhs: inout Set, rhs: any Sequence) {
9 | lhs.subtract(rhs)
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Sources/AppBundle/util/ThreadGuardedValue.swift:
--------------------------------------------------------------------------------
1 | import Common
2 |
3 | final class ThreadGuardedValue: Sendable {
4 | private nonisolated(unsafe) var _threadGuarded: Value?
5 | private let threadToken: AxAppThreadToken = axTaskLocalAppThreadToken ?? dieT("axTaskLocalAppThreadToken is not initialized")
6 | init(_ value: Value) { self._threadGuarded = value }
7 | var threadGuarded: Value {
8 | get {
9 | threadToken.checkEquals(axTaskLocalAppThreadToken)
10 | return _threadGuarded ?? dieT("Value is already destroyed")
11 | }
12 | set(newValue) {
13 | threadToken.checkEquals(axTaskLocalAppThreadToken)
14 | _threadGuarded = newValue
15 | }
16 | }
17 | func destroy() {
18 | threadToken.checkEquals(axTaskLocalAppThreadToken)
19 | _threadGuarded = nil
20 | }
21 | deinit {
22 | check(_threadGuarded == nil, "The Value must be explicitly destroyed on the appropriate thread before deinit")
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Sources/AppBundle/util/axTrustedCheckOptionPrompt.swift:
--------------------------------------------------------------------------------
1 | @preconcurrency import ApplicationServices
2 |
3 | let axTrustedCheckOptionPrompt: String = kAXTrustedCheckOptionPrompt.takeRetainedValue() as String
4 |
--------------------------------------------------------------------------------
/Sources/AppBundleTests/command/BalanceSizesCommandTest.swift:
--------------------------------------------------------------------------------
1 | @testable import AppBundle
2 | import Common
3 | import XCTest
4 |
5 | @MainActor
6 | final class BalanceSizesCommandTest: XCTestCase {
7 | override func setUp() async throws { setUpWorkspacesForTests() }
8 |
9 | func testBalanceSizesCommand() async throws {
10 | let workspace = Workspace.get(byName: name).apply { wsp in
11 | wsp.rootTilingContainer.apply {
12 | TestWindow.new(id: 1, parent: $0).setWeight(wsp.rootTilingContainer.orientation, 1)
13 | TestWindow.new(id: 2, parent: $0).setWeight(wsp.rootTilingContainer.orientation, 2)
14 | TestWindow.new(id: 3, parent: $0).setWeight(wsp.rootTilingContainer.orientation, 3)
15 | }
16 | }
17 |
18 | try await BalanceSizesCommand(args: BalanceSizesCmdArgs(rawArgs: []))
19 | .run(.defaultEnv.copy(\.workspaceName, name), .emptyStdin)
20 |
21 | for window in workspace.rootTilingContainer.children {
22 | assertEquals(window.getWeight(workspace.rootTilingContainer.orientation), 1)
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Sources/AppBundleTests/command/CloseCommandTest.swift:
--------------------------------------------------------------------------------
1 | @testable import AppBundle
2 | import Common
3 | import XCTest
4 |
5 | @MainActor
6 | final class CloseCommandTest: XCTestCase {
7 | override func setUp() async throws { setUpWorkspacesForTests() }
8 |
9 | func testSimple() async throws {
10 | Workspace.get(byName: name).rootTilingContainer.apply {
11 | _ = TestWindow.new(id: 1, parent: $0).focusWindow()
12 | TestWindow.new(id: 2, parent: $0)
13 | }
14 |
15 | assertEquals(focus.windowOrNil?.windowId, 1)
16 | assertEquals(focus.workspace.rootTilingContainer.children.count, 2)
17 |
18 | try await CloseCommand(args: CloseCmdArgs(rawArgs: [])).run(.defaultEnv, .emptyStdin)
19 |
20 | assertEquals(focus.windowOrNil?.windowId, 2)
21 | assertEquals(focus.workspace.rootTilingContainer.children.count, 1)
22 | }
23 |
24 | func testCloseViaWindowIdFlag() async throws {
25 | Workspace.get(byName: name).rootTilingContainer.apply {
26 | _ = TestWindow.new(id: 1, parent: $0).focusWindow()
27 | TestWindow.new(id: 2, parent: $0)
28 | }
29 |
30 | assertEquals(focus.windowOrNil?.windowId, 1)
31 | assertEquals(focus.workspace.rootTilingContainer.children.count, 2)
32 |
33 | try await CloseCommand(args: CloseCmdArgs(rawArgs: []).copy(\.windowId, 2)).run(.defaultEnv, .emptyStdin)
34 |
35 | assertEquals(focus.windowOrNil?.windowId, 1)
36 | assertEquals(focus.workspace.rootTilingContainer.children.count, 1)
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Sources/AppBundleTests/command/ExecCommandTest.swift:
--------------------------------------------------------------------------------
1 | @testable import AppBundle
2 | import Common
3 | import XCTest
4 |
5 | @MainActor
6 | final class ExecCommandTest: XCTestCase {
7 | override func setUp() async throws { setUpWorkspacesForTests() }
8 |
9 | func testParseExecCommand() {
10 | testParseCommandSucc("exec-and-forget echo 'foo'", ExecAndForgetCmdArgs(bashScript: " echo 'foo'"))
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Sources/AppBundleTests/command/FlattenWorkspaceTreeCommandTest.swift:
--------------------------------------------------------------------------------
1 | @testable import AppBundle
2 | import Common
3 | import XCTest
4 |
5 | @MainActor
6 | final class FlattenWorkspaceTreeCommandTest: XCTestCase {
7 | override func setUp() async throws { setUpWorkspacesForTests() }
8 |
9 | func testSimple() async throws {
10 | let workspace = Workspace.get(byName: name).apply {
11 | $0.rootTilingContainer.apply {
12 | TestWindow.new(id: 1, parent: $0)
13 | TilingContainer.newHTiles(parent: $0, adaptiveWeight: 1).apply {
14 | TestWindow.new(id: 2, parent: $0)
15 | }
16 | }
17 | TestWindow.new(id: 3, parent: $0) // floating
18 | }
19 | assertEquals(workspace.focusWorkspace(), true)
20 |
21 | try await FlattenWorkspaceTreeCommand(args: FlattenWorkspaceTreeCmdArgs(rawArgs: [])).run(.defaultEnv, .emptyStdin)
22 | workspace.normalizeContainers()
23 | assertEquals(workspace.layoutDescription, .workspace([.h_tiles([.window(1), .window(2)]), .window(3)]))
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Sources/AppBundleTests/command/JoinWithCommandTest.swift:
--------------------------------------------------------------------------------
1 | @testable import AppBundle
2 | import Common
3 | import XCTest
4 |
5 | @MainActor
6 | final class JoinWithCommandTest: XCTestCase {
7 | override func setUp() async throws { setUpWorkspacesForTests() }
8 |
9 | func testMoveIn() async throws {
10 | let root = Workspace.get(byName: name).rootTilingContainer.apply {
11 | TestWindow.new(id: 0, parent: $0)
12 | assertEquals(TestWindow.new(id: 1, parent: $0).focusWindow(), true)
13 | TestWindow.new(id: 2, parent: $0)
14 | }
15 |
16 | try await JoinWithCommand(args: JoinWithCmdArgs(rawArgs: [], direction: .right)).run(.defaultEnv, .emptyStdin)
17 | assertEquals(root.layoutDescription, .h_tiles([
18 | .window(0),
19 | .v_tiles([
20 | .window(1),
21 | .window(2),
22 | ]),
23 | ]))
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Sources/AppBundleTests/command/ListAppsTest.swift:
--------------------------------------------------------------------------------
1 | @testable import AppBundle
2 | import Common
3 | import XCTest
4 |
5 | final class ListAppsTest: XCTestCase {
6 | func testParse() {
7 | assertNotNil(parseCommand("list-apps --macos-native-hidden").cmdOrNil)
8 | assertNotNil(parseCommand("list-apps --macos-native-hidden no").cmdOrNil)
9 | assertNotNil(parseCommand("list-apps --format %{app-bundle-id}").cmdOrNil)
10 | assertNotNil(parseCommand("list-apps --count").cmdOrNil)
11 | assertEquals(parseCommand("list-apps --format %{app-bundle-id} --count").errorOrNil, "ERROR: Conflicting options: --count, --format")
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Sources/AppBundleTests/command/ListModesTest.swift:
--------------------------------------------------------------------------------
1 | @testable import AppBundle
2 | import Common
3 | import XCTest
4 |
5 | final class ListModesTest: XCTestCase {
6 | func testParseListModesCommand() {
7 | testParseCommandSucc("list-modes", ListModesCmdArgs(rawArgs: []))
8 | testParseCommandSucc("list-modes --current", ListModesCmdArgs(rawArgs: []).copy(\.current, true))
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/Sources/AppBundleTests/command/ListMonitorsTest.swift:
--------------------------------------------------------------------------------
1 | @testable import AppBundle
2 | import Common
3 | import XCTest
4 |
5 | final class ListMonitorsTest: XCTestCase {
6 | func testParseListMonitorsCommand() {
7 | testParseCommandSucc("list-monitors", ListMonitorsCmdArgs(rawArgs: []))
8 | testParseCommandSucc("list-monitors --focused", ListMonitorsCmdArgs(rawArgs: []).copy(\.focused, true))
9 | testParseCommandSucc("list-monitors --count", ListMonitorsCmdArgs(rawArgs: []).copy(\.outputOnlyCount, true))
10 | assertEquals(parseCommand("list-monitors --format %{monitor-id} --count").errorOrNil, "ERROR: Conflicting options: --count, --format")
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Sources/AppBundleTests/command/ListWorkspacesTest.swift:
--------------------------------------------------------------------------------
1 | @testable import AppBundle
2 | import Common
3 | import XCTest
4 |
5 | final class ListWorkspacesTest: XCTestCase {
6 | func testParse() {
7 | assertNotNil(parseCommand("list-workspaces --all").cmdOrNil)
8 | assertNil(parseCommand("list-workspaces --all --visible").cmdOrNil)
9 | assertNil(parseCommand("list-workspaces --focused --visible").cmdOrNil)
10 | assertNil(parseCommand("list-workspaces --focused --all").cmdOrNil)
11 | assertNil(parseCommand("list-workspaces --visible").cmdOrNil)
12 | assertNotNil(parseCommand("list-workspaces --visible --monitor 2").cmdOrNil)
13 | assertNotNil(parseCommand("list-workspaces --monitor focused").cmdOrNil)
14 | assertNil(parseCommand("list-workspaces --focused --monitor 2").cmdOrNil)
15 | assertNotNil(parseCommand("list-workspaces --all --format %{workspace}").cmdOrNil)
16 | assertEquals(parseCommand("list-workspaces --all --format %{workspace} --count").errorOrNil, "ERROR: Conflicting options: --count, --format")
17 | assertEquals(parseCommand("list-workspaces --empty").errorOrNil, "Mandatory option is not specified (--all|--focused|--monitor)")
18 | assertEquals(parseCommand("list-workspaces --all --focused --monitor mouse").errorOrNil, "ERROR: Conflicting options: --all, --focused, --monitor")
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/AppBundleTests/command/ResizeCommandTest.swift:
--------------------------------------------------------------------------------
1 | @testable import AppBundle
2 | import Common
3 | import XCTest
4 |
5 | final class ResizeCommandTest: XCTestCase {
6 | func testParseCommand() {
7 | testParseCommandSucc("resize smart +10", ResizeCmdArgs(rawArgs: [], dimension: .smart, units: .add(10)))
8 | testParseCommandSucc("resize smart -10", ResizeCmdArgs(rawArgs: [], dimension: .smart, units: .subtract(10)))
9 | testParseCommandSucc("resize smart 10", ResizeCmdArgs(rawArgs: [], dimension: .smart, units: .set(10)))
10 |
11 | testParseCommandSucc("resize smart-opposite +10", ResizeCmdArgs(rawArgs: [], dimension: .smartOpposite, units: .add(10)))
12 | testParseCommandSucc("resize smart-opposite -10", ResizeCmdArgs(rawArgs: [], dimension: .smartOpposite, units: .subtract(10)))
13 | testParseCommandSucc("resize smart-opposite 10", ResizeCmdArgs(rawArgs: [], dimension: .smartOpposite, units: .set(10)))
14 |
15 | testParseCommandSucc("resize height 10", ResizeCmdArgs(rawArgs: [], dimension: .height, units: .set(10)))
16 | testParseCommandSucc("resize width 10", ResizeCmdArgs(rawArgs: [], dimension: .width, units: .set(10)))
17 |
18 | testParseCommandFail("resize s 10", msg: """
19 | ERROR: Can't parse 's'.
20 | Possible values: (width|height|smart|smart-opposite)
21 | """)
22 | testParseCommandFail("resize smart foo", msg: "ERROR: argument must be a number")
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Sources/AppBundleTests/command/SummonWorkspaceCommandTest.swift:
--------------------------------------------------------------------------------
1 | @testable import AppBundle
2 | import Common
3 | import XCTest
4 |
5 | @MainActor
6 | final class SummonWorkspaceCommandTest: XCTestCase {
7 | override func setUp() async throws { setUpWorkspacesForTests() }
8 |
9 | func testParse() {
10 | assertEquals(parseCommand("summon-workspace").errorOrNil, "ERROR: Argument '' is mandatory")
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Sources/AppBundleTests/command/WorkspaceCommandTest.swift:
--------------------------------------------------------------------------------
1 | @testable import AppBundle
2 | import Common
3 | import XCTest
4 |
5 | @MainActor
6 | final class WorkspaceCommandTest: XCTestCase {
7 | override func setUp() async throws { setUpWorkspacesForTests() }
8 |
9 | func testParseWorkspaceCommand() {
10 | testParseCommandFail("workspace my mail", msg: "ERROR: Unknown argument 'mail'")
11 | testParseCommandFail("workspace 'my mail'", msg: "ERROR: Whitespace characters are forbidden in workspace names")
12 | assertEquals(parseCommand("workspace").errorOrNil, "ERROR: Argument '(|next|prev)' is mandatory")
13 | testParseCommandSucc("workspace next", WorkspaceCmdArgs(target: .relative(true)))
14 | testParseCommandSucc("workspace --auto-back-and-forth W", WorkspaceCmdArgs(target: .direct(.parse("W").getOrDie()), autoBackAndForth: true))
15 | assertEquals(parseCommand("workspace --wrap-around W").errorOrNil, "--wrapAround requires using (prev|next) argument")
16 | assertEquals(parseCommand("workspace --auto-back-and-forth next").errorOrNil, "--auto-back-and-forth is incompatible with (next|prev)")
17 | testParseCommandSucc("workspace next --wrap-around", WorkspaceCmdArgs(target: .relative(true), wrapAround: true))
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Sources/AppBundleTests/config/SplitArgsTest.swift:
--------------------------------------------------------------------------------
1 | @testable import AppBundle
2 | import Common
3 | import XCTest
4 |
5 | final class SplitArgsTest: XCTestCase {
6 | func testSplit() {
7 | testSucSplit("echo foo", expected: ["echo", "foo"])
8 | testSucSplit("echo 'foo'", expected: ["echo", "foo"])
9 | testSucSplit("'echo' foo", expected: ["echo", "foo"])
10 | testSucSplit("echo \"'\"", expected: ["echo", "'"])
11 | testSucSplit("echo '\"'", expected: ["echo", "\""])
12 | testSucSplit(" echo ' foo bar'", expected: ["echo", " foo bar"])
13 |
14 | testFailSplit("echo 'foo")
15 | testFailSplit("echo foo'")
16 | }
17 | }
18 |
19 | private func testSucSplit(_ str: String, expected: [String]) {
20 | let result = str.splitArgs()
21 | switch result {
22 | case .success(let actual): assertEquals(actual, expected)
23 | case .failure: XCTFail("\(str) split is not successful")
24 | }
25 | }
26 |
27 | private func testFailSplit(_ str: String) {
28 | let result = str.splitArgs()
29 | switch result {
30 | case .success: XCTFail("\(str) is expected to fail to split")
31 | case .failure: break
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/Sources/AppBundleTests/testExtensions.swift:
--------------------------------------------------------------------------------
1 | @testable import AppBundle
2 | import Common
3 | import Foundation
4 | import TOMLKit
5 |
6 | extension [TomlParseError] {
7 | var descriptions: [String] { map(\.description) }
8 | }
9 |
--------------------------------------------------------------------------------
/Sources/AppBundleTests/tree/TestApp.swift:
--------------------------------------------------------------------------------
1 | @testable import AppBundle
2 | import Common
3 |
4 | final class TestApp: AbstractApp {
5 | let pid: Int32
6 | let bundleId: String?
7 | let name: String?
8 | let execPath: String? = nil
9 | let bundlePath: String? = nil
10 | @MainActor
11 | static let shared = TestApp()
12 |
13 | private init() {
14 | self.pid = 0
15 | self.bundleId = "bobko.AeroSpace.test-app"
16 | self.name = bundleId
17 | }
18 |
19 | var _windows: [Window] = []
20 | var windows: [Window] {
21 | get { _windows }
22 | set {
23 | if let focusedWindow {
24 | check(newValue.contains(focusedWindow))
25 | }
26 | _windows = newValue
27 | }
28 | }
29 | @MainActor func detectNewWindowsAndGetIds() async throws -> [UInt32] {
30 | return windows.map { $0.windowId }
31 | }
32 |
33 | private var _focusedWindow: Window? = nil
34 | var focusedWindow: Window? {
35 | get { _focusedWindow }
36 | set {
37 | if let window = newValue {
38 | check(windows.contains(window))
39 | }
40 | _focusedWindow = newValue
41 | }
42 | }
43 | @MainActor func getFocusedWindow() -> Window? { _focusedWindow }
44 | }
45 |
--------------------------------------------------------------------------------
/Sources/AppBundleTests/tree/TestWindow.swift:
--------------------------------------------------------------------------------
1 | @testable import AppBundle
2 | import AppKit
3 |
4 | final class TestWindow: Window, CustomStringConvertible {
5 | private var _rect: Rect?
6 |
7 | @MainActor
8 | private init(_ id: UInt32, _ parent: NonLeafTreeNodeObject, _ adaptiveWeight: CGFloat, _ rect: Rect?) {
9 | _rect = rect
10 | super.init(id: id, TestApp.shared, lastFloatingSize: nil, parent: parent, adaptiveWeight: adaptiveWeight, index: INDEX_BIND_LAST)
11 | }
12 |
13 | @discardableResult
14 | @MainActor
15 | static func new(id: UInt32, parent: NonLeafTreeNodeObject, adaptiveWeight: CGFloat = 1, rect: Rect? = nil) -> TestWindow {
16 | let wi = TestWindow(id, parent, adaptiveWeight, rect)
17 | TestApp.shared._windows.append(wi)
18 | return wi
19 | }
20 |
21 | nonisolated var description: String { "TestWindow(\(windowId))" }
22 |
23 | @MainActor
24 | override func nativeFocus() {
25 | appForTests = TestApp.shared
26 | TestApp.shared.focusedWindow = self
27 | }
28 |
29 | override func closeAxWindow() {
30 | unbindFromParent()
31 | }
32 |
33 | override var title: String { description }
34 |
35 | @MainActor override func getAxRect() async throws -> Rect? { // todo change to not Optional
36 | _rect
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Sources/AppBundleTests/tree/TilingContainer.swift:
--------------------------------------------------------------------------------
1 | @testable import AppBundle
2 | import AppKit
3 |
4 | extension TilingContainer {
5 | @MainActor
6 | static func newHTiles(parent: NonLeafTreeNodeObject, adaptiveWeight: CGFloat) -> TilingContainer {
7 | newHTiles(parent: parent, adaptiveWeight: adaptiveWeight, index: INDEX_BIND_LAST)
8 | }
9 |
10 | @MainActor
11 | static func newVTiles(parent: NonLeafTreeNodeObject, adaptiveWeight: CGFloat) -> TilingContainer {
12 | newVTiles(parent: parent, adaptiveWeight: adaptiveWeight, index: INDEX_BIND_LAST)
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/Sources/Cli/cliUtil.swift:
--------------------------------------------------------------------------------
1 | import Common
2 | import Darwin
3 | import Foundation
4 |
5 | let cliClientVersionAndHash: String = "\(aeroSpaceAppVersion) \(gitHash)"
6 |
7 | func hasStdin() -> Bool {
8 | isatty(STDIN_FILENO) != 1
9 | }
10 |
--------------------------------------------------------------------------------
/Sources/Common/appMetadata.swift:
--------------------------------------------------------------------------------
1 | #if DEBUG
2 | public let aeroSpaceAppId: String = "bobko.aerospace.debug"
3 | public let aeroSpaceAppName: String = "AeroSpace-Debug"
4 | #else
5 | public let aeroSpaceAppId: String = "bobko.aerospace"
6 | public let aeroSpaceAppName: String = "AeroSpace"
7 | #endif
8 |
--------------------------------------------------------------------------------
/Sources/Common/cmdArgs/cmdArgsStringArrayEx.swift:
--------------------------------------------------------------------------------
1 | extension [String] {
2 | mutating func next() -> String {
3 | nextOrNil() ?? dieT("args is empty")
4 | }
5 |
6 | mutating func nextNonFlagOrNil() -> String? {
7 | first?.starts(with: "-") == true ? nil : nextOrNil()
8 | }
9 |
10 | mutating func allNextNonFlagArgs() -> [String] {
11 | var args: [String] = []
12 | while let nextArg = nextNonFlagOrNil() {
13 | args.append(nextArg)
14 | }
15 | return args
16 | }
17 |
18 | private mutating func nextOrNil() -> String? {
19 | let result = first
20 | self = Array(dropFirst())
21 | return result
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Sources/Common/cmdArgs/impl/BalanceSizesCmdArgs.swift:
--------------------------------------------------------------------------------
1 | public struct BalanceSizesCmdArgs: CmdArgs {
2 | public let rawArgs: EquatableNoop<[String]>
3 | public init(rawArgs: [String]) { self.rawArgs = .init(rawArgs) }
4 | public static let parser: CmdParser = cmdParser(
5 | kind: .balanceSizes,
6 | allowInConfig: true,
7 | help: balance_sizes_help_generated,
8 | options: [
9 | "--workspace": optionalWorkspaceFlag(),
10 | ],
11 | arguments: []
12 | )
13 |
14 | public var windowId: UInt32?
15 | public var workspaceName: WorkspaceName?
16 | }
17 |
--------------------------------------------------------------------------------
/Sources/Common/cmdArgs/impl/CloseAllWindowsButCurrentCmdArgs.swift:
--------------------------------------------------------------------------------
1 | public struct CloseAllWindowsButCurrentCmdArgs: CmdArgs {
2 | public let rawArgs: EquatableNoop<[String]>
3 | public init(rawArgs: [String]) { self.rawArgs = .init(rawArgs) }
4 | public static let parser: CmdParser = cmdParser(
5 | kind: .closeAllWindowsButCurrent,
6 | allowInConfig: true,
7 | help: close_all_windows_but_current_help_generated,
8 | options: [
9 | "--quit-if-last-window": trueBoolFlag(\.closeArgs.quitIfLastWindow),
10 | ],
11 | arguments: []
12 | )
13 |
14 | public var closeArgs = CloseCmdArgs(rawArgs: [])
15 | public var windowId: UInt32?
16 | public var workspaceName: WorkspaceName?
17 | }
18 |
19 | public func parseCloseAllWindowsButCurrentCmdArgs(_ args: [String]) -> ParsedCmd {
20 | parseSpecificCmdArgs(CloseAllWindowsButCurrentCmdArgs(rawArgs: .init(args)), args)
21 | }
22 |
--------------------------------------------------------------------------------
/Sources/Common/cmdArgs/impl/CloseCmdArgs.swift:
--------------------------------------------------------------------------------
1 | public struct CloseCmdArgs: CmdArgs {
2 | public let rawArgs: EquatableNoop<[String]>
3 | public init(rawArgs: [String]) { self.rawArgs = .init(rawArgs) }
4 | public static let parser: CmdParser = cmdParser(
5 | kind: .close,
6 | allowInConfig: true,
7 | help: close_help_generated,
8 | options: [
9 | "--quit-if-last-window": trueBoolFlag(\.quitIfLastWindow),
10 | "--window-id": optionalWindowIdFlag(),
11 | ],
12 | arguments: []
13 | )
14 |
15 | public var quitIfLastWindow: Bool = false
16 | public var windowId: UInt32?
17 | public var workspaceName: WorkspaceName?
18 | }
19 |
--------------------------------------------------------------------------------
/Sources/Common/cmdArgs/impl/DebugWindowsCmdArgs.swift:
--------------------------------------------------------------------------------
1 | public struct DebugWindowsCmdArgs: CmdArgs {
2 | public let rawArgs: EquatableNoop<[String]>
3 | public init(rawArgs: EquatableNoop<[String]>) { self.rawArgs = rawArgs }
4 | public static let parser: CmdParser = cmdParser(
5 | kind: .debugWindows,
6 | allowInConfig: false,
7 | help: debug_windows_help_generated,
8 | options: [
9 | "--window-id": ArgParser(\.windowId, upcastArgParserFun(parseArgWithUInt32)),
10 | ],
11 | arguments: []
12 | )
13 |
14 | public var windowId: UInt32?
15 | public var workspaceName: WorkspaceName?
16 | }
17 |
--------------------------------------------------------------------------------
/Sources/Common/cmdArgs/impl/EnableCmdArgs.swift:
--------------------------------------------------------------------------------
1 | public struct EnableCmdArgs: CmdArgs {
2 | public let rawArgs: EquatableNoop<[String]>
3 | fileprivate init(rawArgs: [String]) { self.rawArgs = .init(rawArgs) }
4 | public static let parser: CmdParser = cmdParser(
5 | kind: .enable,
6 | allowInConfig: true,
7 | help: enable_help_generated,
8 | options: [
9 | "--fail-if-noop": trueBoolFlag(\.failIfNoop),
10 | ],
11 | arguments: [newArgParser(\.targetState, parseState, mandatoryArgPlaceholder: EnableCmdArgs.State.unionLiteral)]
12 | )
13 | public var windowId: UInt32?
14 | public var workspaceName: WorkspaceName?
15 | public var targetState: Lateinit = .uninitialized
16 | public var failIfNoop: Bool = false
17 |
18 | public init(rawArgs: [String], targetState: State) {
19 | self.rawArgs = .init(rawArgs)
20 | self.targetState = .initialized(targetState)
21 | }
22 |
23 | public enum State: String, CaseIterable, Sendable {
24 | case on, off, toggle
25 | }
26 | }
27 |
28 | public func parseEnableCmdArgs(_ args: [String]) -> ParsedCmd {
29 | return parseSpecificCmdArgs(EnableCmdArgs(rawArgs: args), args)
30 | .filterNot("--fail-if-noop is incompatible with 'toggle' argument") { $0.targetState.val == .toggle && $0.failIfNoop }
31 | }
32 |
33 | private func parseState(arg: String, nextArgs: inout [String]) -> Parsed {
34 | parseEnum(arg, EnableCmdArgs.State.self)
35 | }
36 |
--------------------------------------------------------------------------------
/Sources/Common/cmdArgs/impl/ExecAndForgetCmdArgs.swift:
--------------------------------------------------------------------------------
1 | public struct ExecAndForgetCmdArgs: CmdArgs {
2 | public var rawArgs: EquatableNoop<[String]> { .init([bashScript]) }
3 | public static let parser: CmdParser = cmdParser(
4 | kind: .execAndForget,
5 | allowInConfig: true,
6 | help: exec_and_forget_help_generated,
7 | options: [:],
8 | arguments: []
9 | )
10 |
11 | public init(bashScript: String) {
12 | self.bashScript = bashScript
13 | }
14 |
15 | public let bashScript: String
16 | public var windowId: UInt32?
17 | public var workspaceName: WorkspaceName?
18 | }
19 |
--------------------------------------------------------------------------------
/Sources/Common/cmdArgs/impl/FlattenWorkspaceTreeCmdArgs.swift:
--------------------------------------------------------------------------------
1 | public struct FlattenWorkspaceTreeCmdArgs: CmdArgs {
2 | public let rawArgs: EquatableNoop<[String]>
3 | public init(rawArgs: [String]) { self.rawArgs = .init(rawArgs) }
4 | public static let parser: CmdParser = cmdParser(
5 | kind: .flattenWorkspaceTree,
6 | allowInConfig: true,
7 | help: flatten_workspace_tree_help_generated,
8 | options: [
9 | "--workspace": optionalWorkspaceFlag(),
10 | ],
11 | arguments: []
12 | )
13 |
14 | public var windowId: UInt32?
15 | public var workspaceName: WorkspaceName?
16 | }
17 |
--------------------------------------------------------------------------------
/Sources/Common/cmdArgs/impl/FocusBackAndForthCmdArgs.swift:
--------------------------------------------------------------------------------
1 | public struct FocusBackAndForthCmdArgs: CmdArgs {
2 | public let rawArgs: EquatableNoop<[String]>
3 | public init(rawArgs: [String]) { self.rawArgs = .init(rawArgs) }
4 | public static let parser: CmdParser = cmdParser(
5 | kind: .focusBackAndForth,
6 | allowInConfig: true,
7 | help: focus_back_and_forth_help_generated,
8 | options: [:],
9 | arguments: []
10 | )
11 |
12 | public var windowId: UInt32?
13 | public var workspaceName: WorkspaceName?
14 | }
15 |
--------------------------------------------------------------------------------
/Sources/Common/cmdArgs/impl/FullscreenCmdArgs.swift:
--------------------------------------------------------------------------------
1 | public struct FullscreenCmdArgs: CmdArgs {
2 | public let rawArgs: EquatableNoop<[String]>
3 | fileprivate init(rawArgs: [String]) { self.rawArgs = .init(rawArgs) }
4 | public static let parser: CmdParser = cmdParser(
5 | kind: .fullscreen,
6 | allowInConfig: true,
7 | help: fullscreen_help_generated,
8 | options: [
9 | "--no-outer-gaps": trueBoolFlag(\.noOuterGaps),
10 | "--fail-if-noop": trueBoolFlag(\.failIfNoop),
11 | "--window-id": optionalWindowIdFlag(),
12 | ],
13 | arguments: [ArgParser(\.toggle, parseToggleEnum)]
14 | )
15 |
16 | public var toggle: ToggleEnum = .toggle
17 | public var noOuterGaps: Bool = false
18 | public var failIfNoop: Bool = false
19 | public var windowId: UInt32?
20 | public var workspaceName: WorkspaceName?
21 | }
22 |
23 | public func parseFullscreenCmdArgs(_ args: [String]) -> ParsedCmd {
24 | parseSpecificCmdArgs(FullscreenCmdArgs(rawArgs: args), args)
25 | .filterNot("--no-outer-gaps is incompatible with 'off' argument") { $0.toggle == .off && $0.noOuterGaps }
26 | .filter("--fail-if-noop requires 'on' or 'off' argument") { $0.failIfNoop.implies($0.toggle == .on || $0.toggle == .off) }
27 | }
28 |
--------------------------------------------------------------------------------
/Sources/Common/cmdArgs/impl/JoinWithCmdArgs.swift:
--------------------------------------------------------------------------------
1 | public struct JoinWithCmdArgs: CmdArgs {
2 | public let rawArgs: EquatableNoop<[String]>
3 | init(rawArgs: [String]) { self.rawArgs = .init(rawArgs) }
4 | public static let parser: CmdParser = cmdParser(
5 | kind: .joinWith,
6 | allowInConfig: true,
7 | help: join_with_help_generated,
8 | options: [
9 | "--window-id": optionalWindowIdFlag(),
10 | ],
11 | arguments: [newArgParser(\.direction, parseCardinalDirectionArg, mandatoryArgPlaceholder: CardinalDirection.unionLiteral)]
12 | )
13 |
14 | public var direction: Lateinit = .uninitialized
15 | public var windowId: UInt32?
16 | public var workspaceName: WorkspaceName?
17 |
18 | public init(rawArgs: [String], direction: CardinalDirection) {
19 | self.rawArgs = .init(rawArgs)
20 | self.direction = .initialized(direction)
21 | }
22 | }
23 |
24 | public func parseJoinWithCmdArgs(_ args: [String]) -> ParsedCmd {
25 | parseSpecificCmdArgs(JoinWithCmdArgs(rawArgs: args), args)
26 | }
27 |
--------------------------------------------------------------------------------
/Sources/Common/cmdArgs/impl/ListExecEnvVarsCmdArgs.swift:
--------------------------------------------------------------------------------
1 | public struct ListExecEnvVarsCmdArgs: CmdArgs {
2 | public let rawArgs: EquatableNoop<[String]>
3 | public init(rawArgs: [String]) { self.rawArgs = .init(rawArgs) }
4 | public static let parser: CmdParser = cmdParser(
5 | kind: .listExecEnvVars,
6 | allowInConfig: true,
7 | help: list_exec_env_vars_help_generated,
8 | options: [:],
9 | arguments: []
10 | )
11 |
12 | public var windowId: UInt32?
13 | public var workspaceName: WorkspaceName?
14 | }
15 |
--------------------------------------------------------------------------------
/Sources/Common/cmdArgs/impl/ListModesCmdArgs.swift:
--------------------------------------------------------------------------------
1 | public struct ListModesCmdArgs: CmdArgs {
2 | public let rawArgs: EquatableNoop<[String]>
3 | public init(rawArgs: [String]) {
4 | self.rawArgs = .init(rawArgs)
5 | }
6 | public static let parser: CmdParser = cmdParser(
7 | kind: .listModes,
8 | allowInConfig: false,
9 | help: list_modes_help_generated,
10 | options: [
11 | "--current": trueBoolFlag(\.current),
12 | ],
13 | arguments: []
14 | )
15 |
16 | public var windowId: UInt32? // unused
17 | public var workspaceName: WorkspaceName? // unused
18 | public var current: Bool = false
19 | }
20 |
21 | public func parseListModesCmdArgs(_ args: [String]) -> ParsedCmd {
22 | parseSpecificCmdArgs(ListModesCmdArgs(rawArgs: args), args)
23 | }
24 |
--------------------------------------------------------------------------------
/Sources/Common/cmdArgs/impl/ListMonitorsCmdArgs.swift:
--------------------------------------------------------------------------------
1 | public struct ListMonitorsCmdArgs: CmdArgs {
2 | public let rawArgs: EquatableNoop<[String]>
3 | public init(rawArgs: [String]) { self.rawArgs = .init(rawArgs) }
4 | public static let parser: CmdParser = cmdParser(
5 | kind: .listMonitors,
6 | allowInConfig: false,
7 | help: list_monitors_help_generated,
8 | options: [
9 | "--focused": boolFlag(\.focused),
10 | "--mouse": boolFlag(\.mouse),
11 |
12 | // Formatting flags
13 | "--format": ArgParser(\._format, parseFormat),
14 | "--count": trueBoolFlag(\.outputOnlyCount),
15 | "--json": trueBoolFlag(\.json),
16 | ],
17 | arguments: [],
18 | conflictingOptions: [
19 | ["--count", "--format"],
20 | ["--count", "--json"],
21 | ]
22 | )
23 |
24 | public var windowId: UInt32?
25 | public var workspaceName: WorkspaceName?
26 | public var focused: Bool?
27 | public var mouse: Bool?
28 | public var _format: [StringInterToken] = []
29 | public var outputOnlyCount: Bool = false
30 | public var json: Bool = false
31 | }
32 |
33 | public extension ListMonitorsCmdArgs {
34 | var format: [StringInterToken] {
35 | _format.isEmpty
36 | ? [
37 | .interVar("monitor-id"), .interVar("right-padding"), .literal(" | "),
38 | .interVar("monitor-name"),
39 | ]
40 | : _format
41 | }
42 | }
43 |
44 | public func parseListMonitorsCmdArgs(_ args: [String]) -> ParsedCmd {
45 | parseSpecificCmdArgs(ListMonitorsCmdArgs(rawArgs: args), args)
46 | .flatMap { if $0.json, let msg = getErrorIfFormatIsIncompatibleWithJson($0._format) { .failure(msg) } else { .cmd($0) } }
47 | }
48 |
--------------------------------------------------------------------------------
/Sources/Common/cmdArgs/impl/MacosNativeFullscreenCmdArgs.swift:
--------------------------------------------------------------------------------
1 | public struct MacosNativeFullscreenCmdArgs: CmdArgs {
2 | public let rawArgs: EquatableNoop<[String]>
3 | public init(rawArgs: [String]) { self.rawArgs = .init(rawArgs) }
4 | public static let parser: CmdParser = cmdParser(
5 | kind: .macosNativeFullscreen,
6 | allowInConfig: true,
7 | help: macos_native_fullscreen_help_generated,
8 | options: [
9 | "--fail-if-noop": trueBoolFlag(\.failIfNoop),
10 | "--window-id": optionalWindowIdFlag(),
11 | ],
12 | arguments: [ArgParser(\.toggle, parseToggleEnum)]
13 | )
14 |
15 | public var toggle: ToggleEnum = .toggle
16 | public var failIfNoop: Bool = false
17 | public var windowId: UInt32?
18 | public var workspaceName: WorkspaceName?
19 | }
20 |
21 | public func parseMacosNativeFullscreenCmdArgs(_ args: [String]) -> ParsedCmd {
22 | parseSpecificCmdArgs(MacosNativeFullscreenCmdArgs(rawArgs: args), args)
23 | .filter("--fail-if-noop requires 'on' or 'off' argument") { $0.failIfNoop.implies($0.toggle == .on || $0.toggle == .off) }
24 | }
25 |
26 | public enum ToggleEnum: Sendable {
27 | case on, off, toggle
28 | }
29 |
30 | func parseToggleEnum(arg: String, nextArgs: inout [String]) -> Parsed {
31 | return switch arg {
32 | case "on": .success(.on)
33 | case "off": .success(.off)
34 | default: .failure("Can't parse '\(arg)'. Possible values: on|off")
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Sources/Common/cmdArgs/impl/MacosNativeMinimizeCmdArgs.swift:
--------------------------------------------------------------------------------
1 | public struct MacosNativeMinimizeCmdArgs: CmdArgs {
2 | public let rawArgs: EquatableNoop<[String]>
3 | public init(rawArgs: [String]) { self.rawArgs = .init(rawArgs) }
4 | public static let parser: CmdParser = cmdParser(
5 | kind: .macosNativeMinimize,
6 | allowInConfig: true,
7 | help: macos_native_minimize_help_generated,
8 | options: [
9 | "--window-id": optionalWindowIdFlag(),
10 | ],
11 | arguments: []
12 | )
13 |
14 | public var windowId: UInt32?
15 | public var workspaceName: WorkspaceName?
16 | }
17 |
--------------------------------------------------------------------------------
/Sources/Common/cmdArgs/impl/ModeCmdArgs.swift:
--------------------------------------------------------------------------------
1 | public struct ModeCmdArgs: CmdArgs {
2 | public let rawArgs: EquatableNoop<[String]>
3 | public init(rawArgs: [String]) { self.rawArgs = .init(rawArgs) }
4 | public static let parser: CmdParser = cmdParser(
5 | kind: .mode,
6 | allowInConfig: true,
7 | help: mode_help_generated,
8 | options: [:],
9 | arguments: [newArgParser(\.targetMode, parseTargetMode, mandatoryArgPlaceholder: "")]
10 | )
11 |
12 | public var targetMode: Lateinit = .uninitialized
13 | public var windowId: UInt32?
14 | public var workspaceName: WorkspaceName?
15 | }
16 |
17 | private func parseTargetMode(arg: String, nextArgs: inout [String]) -> Parsed {
18 | .success(arg)
19 | }
20 |
--------------------------------------------------------------------------------
/Sources/Common/cmdArgs/impl/MoveCmdArgs.swift:
--------------------------------------------------------------------------------
1 | public struct MoveCmdArgs: CmdArgs {
2 | public let rawArgs: EquatableNoop<[String]>
3 | fileprivate init(rawArgs: [String]) { self.rawArgs = .init(rawArgs) }
4 | public static let parser: CmdParser = cmdParser(
5 | kind: .move,
6 | allowInConfig: true,
7 | help: move_help_generated,
8 | options: [
9 | "--window-id": optionalWindowIdFlag(),
10 | ],
11 | arguments: [newArgParser(\.direction, parseCardinalDirectionArg, mandatoryArgPlaceholder: CardinalDirection.unionLiteral)]
12 | )
13 |
14 | public var direction: Lateinit = .uninitialized
15 | public var windowId: UInt32?
16 | public var workspaceName: WorkspaceName?
17 |
18 | public init(rawArgs: [String], _ direction: CardinalDirection) {
19 | self.rawArgs = .init(rawArgs)
20 | self.direction = .initialized(direction)
21 | }
22 | }
23 |
24 | public func parseMoveCmdArgs(_ args: [String]) -> ParsedCmd {
25 | parseSpecificCmdArgs(MoveCmdArgs(rawArgs: args), args)
26 | }
27 |
--------------------------------------------------------------------------------
/Sources/Common/cmdArgs/impl/MoveMouseCmdArgs.swift:
--------------------------------------------------------------------------------
1 | public struct MoveMouseCmdArgs: CmdArgs {
2 | public let rawArgs: EquatableNoop<[String]>
3 | init(rawArgs: [String]) { self.rawArgs = .init(rawArgs) }
4 | public static let parser: CmdParser = cmdParser(
5 | kind: .moveMouse,
6 | allowInConfig: true,
7 | help: move_mouse_help_generated,
8 | options: [
9 | "--fail-if-noop": trueBoolFlag(\.failIfNoop),
10 | ],
11 | arguments: [newArgParser(\.mouseTarget, parseMouseTarget, mandatoryArgPlaceholder: "")]
12 | )
13 |
14 | public var failIfNoop: Bool = false
15 | public var mouseTarget: Lateinit = .uninitialized
16 | public var windowId: UInt32?
17 | public var workspaceName: WorkspaceName?
18 | }
19 |
20 | func parseMouseTarget(arg: String, nextArgs: inout [String]) -> Parsed {
21 | parseEnum(arg, MouseTarget.self)
22 | }
23 |
24 | public func parseMoveMouseCmdArgs(_ args: [String]) -> ParsedCmd {
25 | parseSpecificCmdArgs(MoveMouseCmdArgs(rawArgs: args), args)
26 | .filter("--fail-if-noop is only compatible with window-lazy-center or monitor-lazy-center") {
27 | $0.failIfNoop.implies($0.mouseTarget.val == .windowLazyCenter || $0.mouseTarget.val == .monitorLazyCenter)
28 | }
29 | }
30 |
31 | public enum MouseTarget: String, CaseIterable, Sendable {
32 | case monitorLazyCenter = "monitor-lazy-center"
33 | case monitorForceCenter = "monitor-force-center"
34 |
35 | case windowLazyCenter = "window-lazy-center"
36 | case windowForceCenter = "window-force-center"
37 | }
38 |
--------------------------------------------------------------------------------
/Sources/Common/cmdArgs/impl/MoveNodeToMonitorCmdArgs.swift:
--------------------------------------------------------------------------------
1 | public struct MoveNodeToMonitorCmdArgs: CmdArgs {
2 | public let rawArgs: EquatableNoop<[String]>
3 | fileprivate init(rawArgs: [String]) { self.rawArgs = .init(rawArgs) }
4 | public static let parser: CmdParser = cmdParser(
5 | kind: .moveNodeToMonitor,
6 | allowInConfig: true,
7 | help: move_node_to_monitor_help_generated,
8 | options: [
9 | // "Own" option
10 | "--wrap-around": trueBoolFlag(\.wrapAround),
11 |
12 | // Forward to moveNodeToWorkspace
13 | "--window-id": optionalWindowIdFlag(),
14 | "--focus-follows-window": trueBoolFlag(\.moveNodeToWorkspace.focusFollowsWindow),
15 | "--fail-if-noop": trueBoolFlag(\.moveNodeToWorkspace.failIfNoop),
16 | ],
17 | arguments: [newArgParser(\.target, parseTarget, mandatoryArgPlaceholder: "(left|down|up|right|next|prev|)")]
18 | )
19 |
20 | public var workspaceName: WorkspaceName?
21 | public var windowId: UInt32? { // Forward to moveNodeToWorkspace
22 | get { moveNodeToWorkspace.windowId }
23 | set(newValue) { moveNodeToWorkspace.windowId = newValue }
24 | }
25 |
26 | public var moveNodeToWorkspace = MoveNodeToWorkspaceCmdArgs(rawArgs: [])
27 | public var wrapAround: Bool = false
28 | public var target: Lateinit = .uninitialized
29 | }
30 |
31 | public func parseMoveNodeToMonitorCmdArgs(_ args: [String]) -> ParsedCmd {
32 | parseSpecificCmdArgs(MoveNodeToMonitorCmdArgs(rawArgs: args), args)
33 | .filter("--wrap-around is incompatible with argument") { $0.wrapAround.implies(!$0.target.val.isPatterns) }
34 | }
35 |
--------------------------------------------------------------------------------
/Sources/Common/cmdArgs/impl/MoveNodeToWorkspaceCmdArgs.swift:
--------------------------------------------------------------------------------
1 | public struct MoveNodeToWorkspaceCmdArgs: CmdArgs {
2 | public let rawArgs: EquatableNoop<[String]>
3 | public static let parser: CmdParser = cmdParser(
4 | kind: .moveNodeToWorkspace,
5 | allowInConfig: true,
6 | help: move_node_to_workspace_help_generated,
7 | options: [
8 | "--wrap-around": optionalTrueBoolFlag(\._wrapAround),
9 | "--fail-if-noop": trueBoolFlag(\.failIfNoop),
10 | "--window-id": optionalWindowIdFlag(),
11 | "--focus-follows-window": trueBoolFlag(\.focusFollowsWindow),
12 | ],
13 | arguments: [newArgParser(\.target, parseWorkspaceTarget, mandatoryArgPlaceholder: workspaceTargetPlaceholder)]
14 | )
15 |
16 | public var _wrapAround: Bool?
17 | public var failIfNoop: Bool = false
18 | public var focusFollowsWindow: Bool = false
19 | public var windowId: UInt32?
20 | public var workspaceName: WorkspaceName?
21 | public var target: Lateinit = .uninitialized
22 |
23 | public init(rawArgs: [String]) {
24 | self.rawArgs = .init(rawArgs)
25 | }
26 | }
27 |
28 | public extension MoveNodeToWorkspaceCmdArgs {
29 | var wrapAround: Bool { _wrapAround ?? false }
30 | }
31 |
32 | func implication(ifTrue: Bool, mustHold: @autoclosure () -> Bool) -> Bool { !ifTrue || mustHold() }
33 |
34 | public func parseMoveNodeToWorkspaceCmdArgs(_ args: [String]) -> ParsedCmd {
35 | parseSpecificCmdArgs(MoveNodeToWorkspaceCmdArgs(rawArgs: .init(args)), args)
36 | .filter("--wrapAround requires using (prev|next) argument") { ($0._wrapAround != nil).implies($0.target.val.isRelatve) }
37 | .filterNot("--fail-if-noop is incompatible with (next|prev)") { $0.failIfNoop && $0.target.val.isRelatve }
38 | .filterNot("--window-id is incompatible with (next|prev)") { $0.windowId != nil && $0.target.val.isRelatve }
39 | }
40 |
--------------------------------------------------------------------------------
/Sources/Common/cmdArgs/impl/MoveWorkpsaceToMonitorCmdArgs.swift:
--------------------------------------------------------------------------------
1 | public struct MoveWorkspaceToMonitorCmdArgs: CmdArgs {
2 | public let rawArgs: EquatableNoop<[String]>
3 | public init(rawArgs: [String]) { self.rawArgs = .init(rawArgs) }
4 | public static let parser: CmdParser = cmdParser(
5 | kind: .moveWorkspaceToMonitor,
6 | allowInConfig: true,
7 | help: move_workspace_to_monitor_help_generated,
8 | options: [
9 | "--wrap-around": trueBoolFlag(\.wrapAround),
10 | "--workspace": optionalWorkspaceFlag(),
11 | ],
12 | arguments: [
13 | newArgParser(
14 | \.target,
15 | parseTarget,
16 | mandatoryArgPlaceholder: "(left|down|up|right|next|prev|)"
17 | ),
18 | ]
19 | )
20 |
21 | public var windowId: UInt32?
22 | public var workspaceName: WorkspaceName?
23 | public var wrapAround: Bool = false
24 | public var target: Lateinit = .uninitialized
25 | }
26 |
27 | public func parseWorkspaceToMonitorCmdArgs(_ args: [String]) -> ParsedCmd {
28 | parseSpecificCmdArgs(MoveWorkspaceToMonitorCmdArgs(rawArgs: args), args)
29 | .filter("--wrap-around is incompatible with argument") {
30 | $0.wrapAround.implies(!$0.target.val.isPatterns)
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/Sources/Common/cmdArgs/impl/ReloadConfigCmdArgs.swift:
--------------------------------------------------------------------------------
1 | public struct ReloadConfigCmdArgs: CmdArgs {
2 | public let rawArgs: EquatableNoop<[String]>
3 | public init(rawArgs: [String]) { self.rawArgs = .init(rawArgs) }
4 | public static let parser: CmdParser = cmdParser(
5 | kind: .reloadConfig,
6 | allowInConfig: true,
7 | help: reload_config_help_generated,
8 | options: [
9 | "--no-gui": trueBoolFlag(\.noGui),
10 | "--dry-run": trueBoolFlag(\.dryRun),
11 | ],
12 | arguments: []
13 | )
14 |
15 | public var noGui: Bool = false
16 | public var dryRun: Bool = false
17 | public var windowId: UInt32?
18 | public var workspaceName: WorkspaceName?
19 | }
20 |
--------------------------------------------------------------------------------
/Sources/Common/cmdArgs/impl/SplitCmdArgs.swift:
--------------------------------------------------------------------------------
1 | public struct SplitCmdArgs: CmdArgs {
2 | public let rawArgs: EquatableNoop<[String]>
3 | fileprivate init(rawArgs: [String]) { self.rawArgs = .init(rawArgs) }
4 | public static let parser: CmdParser = cmdParser(
5 | kind: .split,
6 | allowInConfig: true,
7 | help: split_help_generated,
8 | options: [
9 | "--window-id": optionalWindowIdFlag(),
10 | ],
11 | arguments: [newArgParser(\.arg, parseSplitArg, mandatoryArgPlaceholder: SplitArg.unionLiteral)]
12 | )
13 |
14 | public var arg: Lateinit = .uninitialized
15 | public var windowId: UInt32?
16 | public var workspaceName: WorkspaceName?
17 |
18 | public init(rawArgs: [String], _ arg: SplitArg) {
19 | self.rawArgs = .init(rawArgs)
20 | self.arg = .initialized(arg)
21 | }
22 |
23 | public enum SplitArg: String, CaseIterable, Sendable {
24 | case horizontal, vertical, opposite
25 | }
26 | }
27 |
28 | public func parseSplitCmdArgs(_ args: [String]) -> ParsedCmd {
29 | parseSpecificCmdArgs(SplitCmdArgs(rawArgs: args), args)
30 | }
31 |
32 | private func parseSplitArg(arg: String, nextArgs: inout [String]) -> Parsed {
33 | parseEnum(arg, SplitCmdArgs.SplitArg.self)
34 | }
35 |
--------------------------------------------------------------------------------
/Sources/Common/cmdArgs/impl/SummonWorkspaceCmdArgs.swift:
--------------------------------------------------------------------------------
1 | public struct SummonWorkspaceCmdArgs: CmdArgs {
2 | public let rawArgs: EquatableNoop<[String]>
3 | public init(rawArgs: [String]) { self.rawArgs = .init(rawArgs) }
4 | public static let parser: CmdParser = cmdParser(
5 | kind: .summonWorkspace,
6 | allowInConfig: true,
7 | help: summon_workspace_help_generated,
8 | options: [
9 | "--fail-if-noop": trueBoolFlag(\.failIfNoop),
10 | ],
11 | arguments: [newArgParser(\.target, parseWorkspaceName, mandatoryArgPlaceholder: "")]
12 | )
13 |
14 | public var windowId: UInt32? // unused
15 | public var workspaceName: WorkspaceName? // unused
16 |
17 | public var target: Lateinit = .uninitialized
18 | public var failIfNoop: Bool = false
19 | }
20 |
21 | private func parseWorkspaceName(arg: String, nextArgs: inout [String]) -> Parsed {
22 | WorkspaceName.parse(arg)
23 | }
24 |
--------------------------------------------------------------------------------
/Sources/Common/cmdArgs/impl/TriggerBindingCmdArgs.swift:
--------------------------------------------------------------------------------
1 | public struct TriggerBindingCmdArgs: CmdArgs {
2 | public let rawArgs: EquatableNoop<[String]>
3 | public static let parser: CmdParser = cmdParser(
4 | kind: .triggerBinding,
5 | allowInConfig: true,
6 | help: trigger_binding_help_generated,
7 | options: [
8 | "--mode": singleValueOption(\._mode, "") { $0 },
9 | ],
10 | arguments: [newArgParser(\.binding, { arg, _ in .success(arg) }, mandatoryArgPlaceholder: "")]
11 | )
12 |
13 | public var _mode: String? = nil
14 | public var binding: Lateinit = .uninitialized
15 | public var windowId: UInt32?
16 | public var workspaceName: WorkspaceName?
17 | }
18 |
19 | public extension TriggerBindingCmdArgs {
20 | var mode: String { _mode! }
21 | }
22 |
23 | public func parseTriggerBindingCmdArgs(_ args: [String]) -> ParsedCmd {
24 | parseSpecificCmdArgs(TriggerBindingCmdArgs(rawArgs: .init(args)), args)
25 | .filter("--mode flag is mandatory") { $0._mode != nil }
26 | }
27 |
--------------------------------------------------------------------------------
/Sources/Common/cmdArgs/impl/VolumeCmdArgs.swift:
--------------------------------------------------------------------------------
1 | public struct VolumeCmdArgs: CmdArgs {
2 | public let rawArgs: EquatableNoop<[String]>
3 | public init(rawArgs: [String]) { self.rawArgs = .init(rawArgs) }
4 | public static let parser: CmdParser = cmdParser(
5 | kind: .volume,
6 | allowInConfig: true,
7 | help: volume_help_generated,
8 | options: [:],
9 | arguments: [newArgParser(\.action, parseVolumeAction, mandatoryArgPlaceholder: VolumeAction.argsUnion)]
10 | )
11 |
12 | public var windowId: UInt32?
13 | public var workspaceName: WorkspaceName?
14 |
15 | public var action: Lateinit = .uninitialized
16 | }
17 |
18 | public enum VolumeAction: Equatable, Sendable {
19 | case up, down, muteToggle, muteOn, muteOff
20 | case set(Int)
21 |
22 | static let argsUnion: String = "(up|down|mute-toggle|mute-on|mute-off|set)"
23 | }
24 |
25 | func parseVolumeAction(arg: String, nextArgs: inout [String]) -> Parsed {
26 | switch arg {
27 | case "up": return .success(.up)
28 | case "down": return .success(.down)
29 | case "mute-toggle": return .success(.muteToggle)
30 | case "mute-off": return .success(.muteOff)
31 | case "mute-on": return .success(.muteOn)
32 | case "set":
33 | guard let arg = nextArgs.nextNonFlagOrNil() else { return .failure("set argument must be followed by ") }
34 | guard let int = Int(arg) else { return .failure("Can't parse number '\(arg)'") }
35 | if !(0 ... 100).contains(int) { return .failure("\(int) must be in range from 0 to 100") }
36 | return .success(.set(int))
37 | default:
38 | return .failure("Unknown argument '\(arg)'. Possible values: \(VolumeAction.argsUnion)")
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/Sources/Common/cmdArgs/impl/WorkspaceBackAndForthCmdArgs.swift:
--------------------------------------------------------------------------------
1 | public struct WorkspaceBackAndForthCmdArgs: CmdArgs {
2 | public let rawArgs: EquatableNoop<[String]>
3 | public init(rawArgs: [String]) { self.rawArgs = .init(rawArgs) }
4 | public static let parser: CmdParser = cmdParser(
5 | kind: .workspaceBackAndForth,
6 | allowInConfig: true,
7 | help: workspace_back_and_forth_help_generated,
8 | options: [:],
9 | arguments: []
10 | )
11 |
12 | public var windowId: UInt32?
13 | public var workspaceName: WorkspaceName?
14 | }
15 |
--------------------------------------------------------------------------------
/Sources/Common/cmdArgs/subcommandParsers.swift:
--------------------------------------------------------------------------------
1 | let subcommandParsers: [String: any SubCommandParserProtocol] = initSubcommands()
2 |
3 | protocol SubCommandParserProtocol: Sendable {
4 | associatedtype T where T: CmdArgs
5 | var _parse: @Sendable ([String]) -> ParsedCmd { get }
6 | }
7 |
8 | extension SubCommandParserProtocol {
9 | func parse(args: [String]) -> ParsedCmd {
10 | _parse(args).map { $0 }
11 | }
12 | }
13 |
14 | struct SubCommandParser: SubCommandParserProtocol, Sendable {
15 | let _parse: @Sendable ([String]) -> ParsedCmd
16 |
17 | init(_ parser: @escaping @Sendable ([String]) -> ParsedCmd) {
18 | _parse = parser
19 | }
20 |
21 | init(_ raw: @escaping @Sendable (EquatableNoop<[String]>) -> T) {
22 | _parse = { args in parseSpecificCmdArgs(raw(.init(args)), args) }
23 | }
24 |
25 | init(_ raw: @escaping @Sendable ([String]) -> T) {
26 | _parse = { args in parseSpecificCmdArgs(raw(args), args) }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Sources/Common/gitHashGenerated.swift:
--------------------------------------------------------------------------------
1 | // FILE IS GENERATED BY generate.sh AND AUTO-UPDATED BY build-release.sh
2 | public let gitHash = "SNAPSHOT"
3 | public let gitShortHash = "SNAPSHOT"
4 |
--------------------------------------------------------------------------------
/Sources/Common/macOs13Compatibility.swift:
--------------------------------------------------------------------------------
1 | import AppKit
2 | import os
3 |
4 | // This file allows to compile against macOS 13 SDK & Xcode 15
5 | extension NSRunningApplication: @unchecked @retroactive Sendable {}
6 | extension OSSignposter: @unchecked @retroactive Sendable {}
7 |
--------------------------------------------------------------------------------
/Sources/Common/model/AxAppThreadToken.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | @TaskLocal
4 | public var axTaskLocalAppThreadToken: AxAppThreadToken? = nil
5 |
6 | public struct AxAppThreadToken: Sendable, Equatable, CustomStringConvertible {
7 | public let pid: pid_t
8 | public let idForDebug: String
9 |
10 | public init(pid: pid_t, idForDebug: String) {
11 | self.pid = pid
12 | self.idForDebug = idForDebug
13 | }
14 |
15 | public static func == (lhs: Self, rhs: Self) -> Bool { lhs.pid == rhs.pid }
16 |
17 | public func checkEquals(_ other: AxAppThreadToken?) {
18 | check(self == other, "\(self) != \(other.prettyDescription)")
19 | }
20 |
21 | public var description: String { idForDebug }
22 | }
23 |
--------------------------------------------------------------------------------
/Sources/Common/model/CardinalDirection.swift:
--------------------------------------------------------------------------------
1 | public enum CardinalDirection: String, CaseIterable, Equatable, Sendable {
2 | case left, down, up, right
3 | }
4 |
5 | public extension CardinalDirection {
6 | var orientation: Orientation { self == .up || self == .down ? .v : .h }
7 | var isPositive: Bool { self == .down || self == .right }
8 | var opposite: CardinalDirection {
9 | return switch self {
10 | case .left: .right
11 | case .down: .up
12 | case .up: .down
13 | case .right: .left
14 | }
15 | }
16 | var focusOffset: Int { isPositive ? 1 : -1 }
17 | var insertionOffset: Int { isPositive ? 1 : 0 }
18 | }
19 |
--------------------------------------------------------------------------------
/Sources/Common/model/Init.swift:
--------------------------------------------------------------------------------
1 | public nonisolated(unsafe) var isCli = true
2 | public var isServer: Bool { !isCli }
3 |
4 | public nonisolated(unsafe) var terminationHandler: TerminationHandler = EmptyTerminationHandler()
5 |
6 | struct EmptyTerminationHandler: TerminationHandler {
7 | func beforeTermination() {}
8 | }
9 |
10 | @MainActor
11 | public protocol TerminationHandler: Sendable {
12 | func beforeTermination() async throws
13 | }
14 |
--------------------------------------------------------------------------------
/Sources/Common/model/Orientation.swift:
--------------------------------------------------------------------------------
1 | public enum Orientation: Sendable {
2 | /// Windows are planced along the **horizontal** line
3 | /// x-axis
4 | case h
5 | /// Windows are planced along the **vertical** line
6 | /// y-axis
7 | case v
8 | }
9 |
10 | public extension Orientation {
11 | var opposite: Orientation { self == .h ? .v : .h }
12 | }
13 |
--------------------------------------------------------------------------------
/Sources/Common/model/WorkspaceName.swift:
--------------------------------------------------------------------------------
1 | public struct WorkspaceName: Equatable, Sendable {
2 | public let raw: String
3 |
4 | private init(_ raw: String) {
5 | self.raw = raw
6 | }
7 |
8 | public static func parse(_ raw: String) -> Parsed {
9 | // reserved names
10 | if raw == "focused" || raw == "non-focused" ||
11 | raw == "visible" || raw == "invisible" || raw == "non-visible" ||
12 | raw == "active" || raw == "non-active" || raw == "inactive" ||
13 | raw == "back-and-forth" || raw == "back_and_forth" || raw == "previous" ||
14 | raw == "prev" || raw == "next" ||
15 | raw == "monitor" || raw == "workspace" ||
16 | raw == "monitors" || raw == "workspaces" ||
17 | raw == "all" || raw == "none" ||
18 | raw == "mouse" || raw == "target"
19 | {
20 | return .failure("'\(raw)' is a reserved workspace name")
21 | }
22 | if raw.contains(",") {
23 | return .failure("Workspace names are not allowed to contain comma")
24 | }
25 | if raw.starts(with: "_") {
26 | return .failure("Workspace names starting with underscore are reserved for future use")
27 | }
28 | if raw.starts(with: "-") {
29 | // The syntax conflicts with CLI options. E.g. list-windows --workspace -foo
30 | return .failure("Workspace names starting with dash are disallowed")
31 | }
32 | if raw.rangeOfCharacter(from: .whitespacesAndNewlines) != nil {
33 | return .failure("Whitespace characters are forbidden in workspace names")
34 | }
35 | return .success(WorkspaceName(raw))
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Sources/Common/model/clientServer.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public struct ServerAnswer: Codable, Sendable {
4 | public let exitCode: Int32
5 | public let stdout: String
6 | public let stderr: String
7 | public let serverVersionAndHash: String
8 |
9 | public init(
10 | exitCode: Int32,
11 | stdout: String = "",
12 | stderr: String = "",
13 | serverVersionAndHash: String
14 | ) {
15 | self.exitCode = exitCode
16 | self.stdout = stdout
17 | self.stderr = stderr
18 | self.serverVersionAndHash = serverVersionAndHash
19 | }
20 | }
21 |
22 | public struct ClientRequest: Codable, Sendable {
23 | public let command: String // Unused. keep it for API compatibility with old servers for a couple of version
24 | public let args: [String]
25 | public let stdin: String
26 |
27 | public init(
28 | args: [String],
29 | stdin: String
30 | ) {
31 | if args.contains(where: { $0.rangeOfCharacter(from: .whitespacesAndNewlines) != nil || $0.contains("\"") || $0.contains("\'") }) {
32 | self.command = "" // Old server won't understand it anyway
33 | } else {
34 | self.command = args.joined(separator: " ")
35 | }
36 | self.args = args
37 | self.stdin = stdin
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Sources/Common/util/AeroAny.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public protocol AeroAny {}
4 |
5 | public extension AeroAny {
6 | @discardableResult
7 | @inlinable
8 | func apply(_ block: (Self) -> Void) -> Self {
9 | block(self)
10 | return self
11 | }
12 |
13 | @discardableResult
14 | @inlinable
15 | func also(_ block: (Self) -> Void) -> Self {
16 | block(self)
17 | return self
18 | }
19 |
20 | @inlinable func takeIf(_ predicate: (Self) -> Bool) -> Self? { predicate(self) ? self : nil }
21 | @inlinable func lets(_ body: (Self) -> R) -> R { body(self) }
22 | }
23 |
24 | extension Int: AeroAny {}
25 | extension String: AeroAny {}
26 | extension Character: AeroAny {}
27 | extension Regex: AeroAny {}
28 | extension Array: AeroAny {}
29 | extension URL: AeroAny {}
30 |
--------------------------------------------------------------------------------
/Sources/Common/util/BoolEx.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | // https://forums.swift.org/t/using-async-call-in-boolean-expression/52943
4 | // https://github.com/swiftlang/swift/issues/56869
5 | // https://forums.swift.org/t/potential-false-positive-sending-risks-causing-data-races/78859
6 | public extension Bool {
7 | @inlinable
8 | func andAsync(_ rhs: () async throws -> Bool) async rethrows -> Bool {
9 | if self {
10 | return try await rhs()
11 | }
12 | return false
13 | }
14 |
15 | @inlinable
16 | func orAsync(_ rhs: () async throws -> Bool) async rethrows -> Bool {
17 | if self {
18 | return true
19 | }
20 | return try await rhs()
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Sources/Common/util/CollectionEx.swift:
--------------------------------------------------------------------------------
1 | public extension Collection {
2 | func singleOrNil() -> Element? {
3 | count == 1 ? first : nil
4 | }
5 |
6 | func getOrNil(atIndex index: Index) -> Element? {
7 | indices.contains(index) ? self[index] : nil
8 | }
9 | }
10 |
11 | public extension Collection where Index == Int {
12 | func get(wrappingIndex: Int) -> Element? { isEmpty ? nil : self[(count + wrappingIndex) % count] }
13 | }
14 |
--------------------------------------------------------------------------------
/Sources/Common/util/ConvenienceCopyable.swift:
--------------------------------------------------------------------------------
1 | public protocol ConvenienceCopyable {}
2 |
3 | public extension ConvenienceCopyable {
4 | func copy(_ key: WritableKeyPath, _ value: T) -> Self {
5 | var copy = self
6 | copy[keyPath: key] = value
7 | return copy
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/Sources/Common/util/EquatableNoop.swift:
--------------------------------------------------------------------------------
1 | public struct EquatableNoop: Equatable {
2 | public var value: Value
3 | public init(_ value: Value) { self.value = value }
4 | public static func == (lhs: EquatableNoop, rhs: EquatableNoop) -> Bool { true }
5 | }
6 |
7 | extension EquatableNoop: Sendable where Value: Sendable {}
8 |
--------------------------------------------------------------------------------
/Sources/Common/util/JsonEncoderEx.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public extension JSONEncoder {
4 | static var aeroSpaceDefault: JSONEncoder {
5 | let encoder = JSONEncoder()
6 | encoder.outputFormatting = [.prettyPrinted, .withoutEscapingSlashes, .sortedKeys]
7 | return encoder
8 | }
9 |
10 | func encodeToString(_ value: Encodable) -> String? {
11 | guard let data = Result(catching: { try encode(value) }).getOrNil() else { return nil }
12 | return String(data: data, encoding: .utf8)
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/Sources/Common/util/Lateinit.swift:
--------------------------------------------------------------------------------
1 | // "Happy path" Optional
2 | public enum Lateinit {
3 | case initialized(T)
4 | case uninitialized
5 |
6 | public var val: T {
7 | switch self {
8 | case .initialized(let value): return value
9 | case .uninitialized: die("Property is not initialized")
10 | }
11 | }
12 |
13 | public var isInitialized: Bool {
14 | return switch self {
15 | case .initialized: true
16 | case .uninitialized: false
17 | }
18 | }
19 | }
20 |
21 | extension Lateinit: Equatable where T: Equatable {
22 | public static func == (lhs: Self, rhs: Self) -> Bool {
23 | lhs.isInitialized && rhs.isInitialized && lhs.val == rhs.val ||
24 | lhs.isInitialized == rhs.isInitialized
25 | }
26 | }
27 |
28 | extension Lateinit: Sendable where T: Sendable {}
29 |
--------------------------------------------------------------------------------
/Sources/Common/util/OptionalEx.swift:
--------------------------------------------------------------------------------
1 | public extension Optional {
2 | func orElse(_ other: () -> Wrapped) -> Wrapped { self ?? other() }
3 |
4 | func orFailure(_ or: @autoclosure () -> F) -> Result {
5 | if let ok = self {
6 | return .success(ok)
7 | } else {
8 | return .failure(or())
9 | }
10 | }
11 |
12 | func mapAsync(_ transform: (Wrapped) async throws(E) -> U) async throws(E) -> U? where E: Error, U: ~Copyable {
13 | if let ok = self {
14 | return try await transform(ok)
15 | } else {
16 | return nil
17 | }
18 | }
19 |
20 | // todo cleanup in future Swift versions
21 | @MainActor
22 | func mapAsyncMainActor(_ transform: @MainActor (Wrapped) async throws(E) -> U) async throws(E) -> U? where E: Error, U: ~Copyable {
23 | if let ok = self {
24 | return try await transform(ok)
25 | } else {
26 | return nil
27 | }
28 | }
29 |
30 | func flatMapAsync(_ transform: (Wrapped) async throws(E) -> U?) async throws(E) -> U? where E: Error, U: ~Copyable {
31 | if let ok = self {
32 | return try await transform(ok)
33 | } else {
34 | return nil
35 | }
36 | }
37 |
38 | // todo cleanup in future Swift versions
39 | @MainActor
40 | func flatMapAsyncMainActor(_ transform: @MainActor (Wrapped) async throws(E) -> U?) async throws(E) -> U? where E: Error, U: ~Copyable {
41 | if let ok = self {
42 | return try await transform(ok)
43 | } else {
44 | return nil
45 | }
46 | }
47 |
48 | func asList() -> [Wrapped] {
49 | if let ok = self {
50 | return [ok]
51 | } else {
52 | return []
53 | }
54 | }
55 |
56 | var prettyDescription: String {
57 | if let unwrapped = self {
58 | return String(describing: unwrapped)
59 | }
60 | return "nil"
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/Sources/Common/util/ResultEx.swift:
--------------------------------------------------------------------------------
1 | public extension Result {
2 | func getOrNil(appendErrorTo errors: inout [Failure]) -> Success? {
3 | switch self {
4 | case .success(let success):
5 | return success
6 | case .failure(let error):
7 | errors += [error]
8 | return nil
9 | }
10 | }
11 |
12 | func filter(_ failure: @autoclosure () -> Failure, _ predicate: (Success) -> Bool) -> Self {
13 | flatMap { succ in predicate(succ) ? .success(succ) : .failure(failure()) }
14 | }
15 |
16 | func getOrNil() -> Success? {
17 | return switch self {
18 | case .success(let success): success
19 | case .failure: nil
20 | }
21 | }
22 |
23 | func getOrNils() -> (Success?, Failure?) {
24 | return switch self {
25 | case .success(let success): (success, nil)
26 | case .failure(let failure): (nil, failure)
27 | }
28 | }
29 |
30 | var errorOrNil: Failure? {
31 | return switch self {
32 | case .success: nil
33 | case .failure(let f): f
34 | }
35 | }
36 |
37 | var isSuccess: Bool {
38 | switch self {
39 | case .success: true
40 | case .failure: false
41 | }
42 | }
43 | }
44 |
45 | public extension Result {
46 | @discardableResult
47 | func getOrDie(
48 | _ msgPrefix: String = "",
49 | file: String = #fileID,
50 | line: Int = #line,
51 | column: Int = #column,
52 | function: String = #function
53 | ) -> Success {
54 | switch self {
55 | case .success(let suc):
56 | return suc
57 | case .failure(let e):
58 | die(msgPrefix + e.localizedDescription, file: file, line: line, column: column, function: function)
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/Sources/Common/util/StringLogicalSegments.swift:
--------------------------------------------------------------------------------
1 | public typealias StringLogicalSegments = [StringLogicalSegment]
2 | public extension StringLogicalSegments {
3 | static func < (lhs: Self, rhs: Self) -> Bool {
4 | for (a, b) in zip(lhs, rhs) {
5 | if a < b {
6 | return true
7 | }
8 | if a > b {
9 | return false
10 | }
11 | }
12 | if lhs.count != rhs.count {
13 | return lhs.count < rhs.count
14 | }
15 | return false
16 | }
17 | }
18 |
19 | public enum StringLogicalSegment: Comparable, Equatable, Sendable {
20 | case string(String)
21 | case number(Int)
22 |
23 | public static func < (lhs: Self, rhs: Self) -> Bool {
24 | switch (lhs, rhs) {
25 | case (.string(let a), .string(let b)): a < b
26 | case (.number(let a), .number(let b)): a < b
27 | case (.number, _): true
28 | case (.string, _): false
29 | }
30 | }
31 | }
32 |
33 | public extension String {
34 | func toLogicalSegments() -> StringLogicalSegments {
35 | var currentSegment: String = ""
36 | var isPrevNumber: Bool = false // Initial value doesn't matter
37 | var result: [String] = []
38 | for char in self {
39 | let isCurNumber = Int(char.description) != nil
40 | if isCurNumber != isPrevNumber && !currentSegment.isEmpty {
41 | result.append(currentSegment)
42 | currentSegment = ""
43 | }
44 | currentSegment.append(char)
45 | isPrevNumber = isCurNumber
46 | }
47 | if !currentSegment.isEmpty {
48 | result.append(currentSegment)
49 | }
50 | return result.map { Int($0).flatMap(StringLogicalSegment.number) ?? .string($0) }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/Sources/Common/util/showMessageInGui.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | // todo refactor. showMessageInGui in common code looks weird
4 | public func showMessageInGui(filenameIfConsoleApp: String?, title: String, message: String) {
5 | let titleAndMessage = "##### \(title) #####\n\n" + message
6 | if isCli {
7 | print(titleAndMessage)
8 | } else if let filenameIfConsoleApp {
9 | let cachesDir = URL(filePath: "/tmp/bobko.aerospace/")
10 | try! FileManager.default.createDirectory(at: cachesDir, withIntermediateDirectories: true)
11 | let file = cachesDir.appending(component: filenameIfConsoleApp)
12 | try! (titleAndMessage + "\n").write(to: file, atomically: true, encoding: .utf8)
13 |
14 | file.absoluteURL.open(with: URL(filePath: "/System/Applications/Utilities/Console.app"))
15 | } else {
16 | try! Process.run(URL(filePath: "/usr/bin/osascript"),
17 | arguments: [
18 | "-e",
19 | """
20 | display dialog "\(message.replacing("\"", with: "\\\""))" with title "\(title)"
21 | """,
22 | ]
23 | )
24 | // === Alternatives ===
25 | // let myPopup = NSAlert()
26 | // myPopup.messageText = message
27 | // myPopup.alertStyle = NSAlert.Style.informational
28 | // myPopup.addButton(withTitle: "OK")
29 | // myPopup.runModal()
30 |
31 | // let alert = UIAlertController(title: "Alert", message: message, preferredStyle: UIAlertControllerStyle.alert)
32 | // alert.addAction(UIAlertAction(title: "Click", style: UIAlertActionStyle.default, handler: nil))
33 | // self.present(alert, animated: true, completion: nil)
34 |
35 | // file.absoluteURL.open(with: URL(filePath: "/System/Applications/Utilities/Console.app"))
36 | // file.absoluteURL.open(with: URL(filePath: "/System/Applications/TextEdit.app"))
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Sources/Common/versionGenerated.swift:
--------------------------------------------------------------------------------
1 | // FILE IS GENERATED BY generate.sh
2 | public let aeroSpaceAppVersion = "0.0.0-SNAPSHOT"
3 |
--------------------------------------------------------------------------------
/Sources/PrivateApi/include/module.modulemap:
--------------------------------------------------------------------------------
1 | module PrivateApi {
2 | header "private.h"
3 | export *
4 | }
5 |
--------------------------------------------------------------------------------
/Sources/PrivateApi/include/private.h:
--------------------------------------------------------------------------------
1 | #ifndef private_header_h
2 | #define private_header_h
3 |
4 | #import
5 |
6 | // Potential alternative 1?
7 | // func allWindowsOnCurrentMacOsSpace() {
8 | // let options = CGWindowListOption(arrayLiteral: .excludeDesktopElements, .optionOnScreenOnly)
9 | // let windowsListInfo = CGWindowListCopyWindowInfo(options, CGWindowID(0))
10 | // let infoList = windowsListInfo as! [[String:Any]]
11 | // let windows = infoList.filter { $0["kCGWindowLayer"] as! Int == 0 }
12 | // print(windows.count)
13 | // for window in windows {
14 | // print(window)
15 | // print("Name: \(window["kCGWindowOwnerName"].unsafelyUnwrapped)")
16 | // print("PID: \(window["kCGWindowOwnerPID"].unsafelyUnwrapped)")
17 | // print("window ID: \(window["kCGWindowNumber"])")
18 | // print("---")
19 | // }
20 | // }
21 | //
22 | // Alternative 2:
23 | // @_silgen_name("_AXUIElementGetWindow")
24 | // @discardableResult
25 | // func _AXUIElementGetWindow(_ axUiElement: AXUIElement, _ id: inout CGWindowID) -> AXError
26 | AXError _AXUIElementGetWindow(AXUIElementRef element, uint32_t *identifier);
27 |
28 | #endif
29 |
--------------------------------------------------------------------------------
/Sources/PrivateApi/include/private.m:
--------------------------------------------------------------------------------
1 | // This file exists purely because xcode doesn't like header only targets, SPM is fine with them
2 | #import "private.h"
3 |
--------------------------------------------------------------------------------
/build-debug.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | cd "$(dirname "$0")"
3 | source ./script/setup.sh
4 |
5 | ./generate.sh --ignore-xcodeproj
6 | swift build
7 | swift build --target AppBundleTests # swift build doesn't build test targets by default :(
8 |
9 | rm -rf .debug && mkdir .debug
10 | cp -r .build/debug/aerospace .debug
11 | cp -r .build/debug/AeroSpaceApp .debug
12 |
--------------------------------------------------------------------------------
/build-docs.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | cd "$(dirname "$0")"
3 | source ./script/setup.sh
4 |
5 | ./script/install-dep.sh --bundler
6 |
7 | rm -rf .site && mkdir .site
8 | rm -rf .man && mkdir .man
9 |
10 | cp-docs() {
11 | cp -r ./docs/*.adoc "$1"
12 | cp -r ./docs/assets "$1"
13 | cp -r ./docs/util "$1"
14 | cp -r ./docs/config-examples "$1"
15 | }
16 |
17 | build-site() {
18 | cp-docs ./.site
19 | cp ./docs/index.html ./.site
20 |
21 | cd .site
22 | # Delete "aerospace " prefifx in synopsis
23 | sed -E -i '' '/tag::synopsis/, /end::synopsis/ s/^(aerospace | {10})//' aerospace*
24 | bundler exec asciidoctor ./guide.adoc ./commands.adoc ./goodies.adoc
25 | cp goodies.html goodness.html # backwards compatibility
26 | rm -rf ./*.adoc
27 | cd - > /dev/null
28 |
29 | git rev-parse HEAD > .site/version.html
30 | if ! test -z "$(git status --porcelain)"; then
31 | echo "git working directory is dirty" >> .site/version.html
32 | fi
33 | }
34 |
35 | build-man() {
36 | cp-docs .man
37 | cd .man
38 | bundler exec asciidoctor -b manpage aerospace*.adoc
39 | rm -rf -- *.adoc
40 | cd - > /dev/null
41 | }
42 |
43 | build-site
44 | build-man
45 |
--------------------------------------------------------------------------------
/build-shell-completion.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | cd "$(dirname "$0")"
3 | source ./script/setup.sh
4 |
5 | ./script/install-dep.sh --complgen
6 |
7 | rm -rf .shell-completion && mkdir -p \
8 | .shell-completion/zsh \
9 | .shell-completion/fish \
10 | .shell-completion/bash
11 |
12 | ./.deps/cargo-root/bin/complgen aot ./grammar/commands-bnf-grammar.txt \
13 | --zsh-script .shell-completion/zsh/_aerospace \
14 | --fish-script .shell-completion/fish/aerospace.fish \
15 | --bash-script .shell-completion/bash/aerospace
16 |
17 | if ! (not-outdated-bash --version | grep -q 'version 5'); then
18 | echo "bash version is too old. At least version 5 is required" > /dev/stderr
19 | exit 1
20 | fi
21 |
22 | # Check basic syntax
23 | zsh -c 'autoload -Uz compinit; compinit; source ./.shell-completion/zsh/_aerospace'
24 | fish -c 'source ./.shell-completion/fish/aerospace.fish'
25 | not-outdated-bash -c 'source ./.shell-completion/bash/aerospace'
26 |
--------------------------------------------------------------------------------
/docs/aerospace-balance-sizes.adoc:
--------------------------------------------------------------------------------
1 | = aerospace-balance-sizes(1)
2 | include::util/man-attributes.adoc[]
3 | :manname: aerospace-balance-sizes
4 | // tag::purpose[]
5 | :manpurpose: Balance sizes of all windows in the current workspace
6 | // end::purpose[]
7 |
8 | // =========================================================== Synopsis
9 | == Synopsis
10 | [verse]
11 | // tag::synopsis[]
12 | aerospace balance-sizes [-h|--help] [--workspace ]
13 |
14 | // end::synopsis[]
15 |
16 | // =========================================================== Description
17 | == Description
18 |
19 | // tag::body[]
20 | {manpurpose}
21 |
22 | // =========================================================== Options
23 | include::./util/conditional-options-header.adoc[]
24 |
25 | -h, --help:: Print help
26 |
27 | --workspace ::
28 | include::./util/workspace-flag-desc.adoc[]
29 |
30 | // end::body[]
31 |
32 | // =========================================================== Footer
33 | include::util/man-footer.adoc[]
34 |
--------------------------------------------------------------------------------
/docs/aerospace-close-all-windows-but-current.adoc:
--------------------------------------------------------------------------------
1 | = aerospace-close-all-windows-but-current(1)
2 | include::util/man-attributes.adoc[]
3 | :manname: aerospace-close-all-windows-but-current
4 | // tag::purpose[]
5 | :manpurpose: On the focused workspace, close all windows but current
6 | // end::purpose[]
7 |
8 | // =========================================================== Synopsis
9 | == Synopsis
10 | [verse]
11 | // tag::synopsis[]
12 | aerospace close-all-windows-but-current [-h|--help] [--quit-if-last-window]
13 |
14 | // end::synopsis[]
15 |
16 | // =========================================================== Description
17 | == Description
18 |
19 | // tag::body[]
20 | {manpurpose}
21 |
22 | // =========================================================== Options
23 | include::util/conditional-options-header.adoc[]
24 |
25 | -h, --help:: Print help
26 | --quit-if-last-window:: Quit the apps instead of closing them if it's their last window
27 |
28 | // end::body[]
29 |
30 | // =========================================================== Footer
31 | include::util/man-footer.adoc[]
32 |
--------------------------------------------------------------------------------
/docs/aerospace-close.adoc:
--------------------------------------------------------------------------------
1 | = aerospace-close(1)
2 | include::util/man-attributes.adoc[]
3 | :manname: aerospace-close
4 | // tag::purpose[]
5 | :manpurpose: Close the focused window
6 | // end::purpose[]
7 |
8 | // =========================================================== Synopsis
9 | == Synopsis
10 | [verse]
11 | // tag::synopsis[]
12 | aerospace close [-h|--help] [--quit-if-last-window] [--window-id ]
13 |
14 | // end::synopsis[]
15 |
16 | // =========================================================== Description
17 | == Description
18 |
19 | // tag::body[]
20 | {manpurpose}
21 |
22 | Normally, you don’t need to use this command, because macOS offers its own `cmd+w` binding.
23 | You might want to use the command from CLI for scripting purposes
24 |
25 | // =========================================================== Options
26 | include::./util/conditional-options-header.adoc[]
27 |
28 | -h, --help:: Print help
29 | --quit-if-last-window:: Quit the app instead of closing if it's the last window of the app
30 |
31 | --window-id ::
32 | include::./util/window-id-flag-desc.adoc[]
33 |
34 | // end::body[]
35 |
36 | // =========================================================== Footer
37 | include::util/man-footer.adoc[]
38 |
--------------------------------------------------------------------------------
/docs/aerospace-debug-windows.adoc:
--------------------------------------------------------------------------------
1 | = aerospace-debug-windows(1)
2 | include::util/man-attributes.adoc[]
3 | :manname: aerospace-debug-windows
4 | // tag::purpose[]
5 | :manpurpose: Interactive command to record Accessibility API debug information to create bug reports
6 | // end::purpose[]
7 |
8 | // =========================================================== Synopsis
9 | == Synopsis
10 | [verse]
11 | // tag::synopsis[]
12 | aerospace debug-windows [-h|--help] [--window-id ]
13 |
14 | // end::synopsis[]
15 |
16 | // =========================================================== Description
17 | == Description
18 |
19 | // tag::body[]
20 | {manpurpose}
21 |
22 | Use this command output to report bug reports about incorrect windows handling
23 | (e.g. some windows are floated when they shouldn't).
24 |
25 | The intended usage is the following:
26 |
27 | . Run the command to start the debug session recording
28 | . Focus problematic window or make the window appear.
29 | . Run the command one more time to stop the debug session recording and print the results
30 |
31 | `debug-windows` command is *not stable API*.
32 | Please *don't rely on* the command existence and output format.
33 | The only intended use case is to report bugs about incorrect windows handling.
34 |
35 | // =========================================================== Options
36 | include::util/conditional-options-header.adoc[]
37 |
38 | -h, --help:: Print help
39 |
40 | --window-id ::
41 | Print debug information of the specified window right away.
42 | Usage of this flag disables interactive mode.
43 |
44 | // end::body[]
45 |
46 | // =========================================================== Footer
47 | include::util/man-footer.adoc[]
48 |
--------------------------------------------------------------------------------
/docs/aerospace-enable.adoc:
--------------------------------------------------------------------------------
1 | = aerospace-enable(1)
2 | include::util/man-attributes.adoc[]
3 | :manname: aerospace-enable
4 | // tag::purpose[]
5 | :manpurpose: Temporarily disable window management
6 | // end::purpose[]
7 |
8 | // =========================================================== Synopsis
9 | == Synopsis
10 | [verse]
11 | // tag::synopsis[]
12 | aerospace enable [-h|--help] toggle
13 | aerospace enable [-h|--help] on [--fail-if-noop]
14 | aerospace enable [-h|--help] off [--fail-if-noop]
15 |
16 | // end::synopsis[]
17 |
18 | // =========================================================== Description
19 | == Description
20 |
21 | // tag::body[]
22 | {manpurpose}
23 |
24 | When you disable AeroSpace, windows from currently invisible workspaces will be placed to the visible area of the screen
25 |
26 | Key events are not intercepted when AeroSpace is disabled
27 |
28 | // =========================================================== Options
29 | include::util/conditional-options-header.adoc[]
30 |
31 | -h, --help:: Print help
32 | --fail-if-noop:: Exit with non-zero exit code if already in the requested mode
33 |
34 | // end::body[]
35 |
36 | // =========================================================== Footer
37 | include::util/man-footer.adoc[]
38 |
--------------------------------------------------------------------------------
/docs/aerospace-exec-and-forget.adoc:
--------------------------------------------------------------------------------
1 | = aerospace-exec-and-forget(1)
2 | include::util/man-attributes.adoc[]
3 | :manname: aerospace-exec-and-forget
4 | // tag::purpose[]
5 | :manpurpose: Run /bin/bash -c ''
6 | // end::purpose[]
7 |
8 | // =========================================================== Synopsis
9 | == Synopsis
10 | [verse]
11 | // tag::synopsis[]
12 | aerospace exec-and-forget
13 |
14 | // end::synopsis[]
15 |
16 | // =========================================================== Description
17 | == Description
18 |
19 | // tag::body[]
20 | Run `/bin/bash -c ''`, and don't wait for the command termination.
21 | Stdout, stderr and exit code are ignored.
22 |
23 | For example, you can use this command to launch applications:
24 |
25 | [source,toml]
26 | ----
27 | alt-enter = 'exec-and-forget open -n /System/Applications/Utilities/Terminal.app'
28 | ----
29 |
30 | `` is passed "as is" to bash without any transformations and escaping. `` is treated as suffix of the TOML string, it's not even an argument in classic CLI sense
31 |
32 | * The command is available in config
33 | * The command is *NOT* available in CLI
34 |
35 | // end::body[]
36 |
37 | // =========================================================== Footer
38 | include::util/man-footer.adoc[]
39 |
--------------------------------------------------------------------------------
/docs/aerospace-flatten-workspace-tree.adoc:
--------------------------------------------------------------------------------
1 | = aerospace-flatten-workspace-tree(1)
2 | include::util/man-attributes.adoc[]
3 | :manname: aerospace-flatten-workspace-tree
4 | // tag::purpose[]
5 | :manpurpose: Flatten the tree of the focused workspace
6 | // end::purpose[]
7 |
8 | // =========================================================== Synopsis
9 | == Synopsis
10 | [verse]
11 | // tag::synopsis[]
12 | aerospace flatten-workspace-tree [-h|--help] [--workspace ]
13 |
14 | // end::synopsis[]
15 |
16 | // =========================================================== Description
17 | == Description
18 |
19 | // tag::body[]
20 | {manpurpose}
21 |
22 | The command is useful when you messed up with your layout, and it's easier to "reset" it and start again.
23 |
24 | // =========================================================== Options
25 | include::./util/conditional-options-header.adoc[]
26 |
27 | -h, --help:: Print help
28 |
29 | --workspace ::
30 | include::./util/workspace-flag-desc.adoc[]
31 |
32 | // end::body[]
33 |
34 | // =========================================================== Footer
35 | include::util/man-footer.adoc[]
36 |
--------------------------------------------------------------------------------
/docs/aerospace-focus-back-and-forth.adoc:
--------------------------------------------------------------------------------
1 | = aerospace-focus-back-and-forth(1)
2 | include::util/man-attributes.adoc[]
3 | // tag::purpose[]
4 | :manpurpose: Switch between the current and previously focused elements back and forth
5 | // end::purpose[]
6 | :manname: aerospace-focus-back-and-forth
7 |
8 | // =========================================================== Synopsis
9 | == Synopsis
10 | [verse]
11 | // tag::synopsis[]
12 | aerospace focus-back-and-forth [-h|--help]
13 |
14 | // end::synopsis[]
15 |
16 | // =========================================================== Description
17 | == Description
18 |
19 | // tag::body[]
20 | {manpurpose}.
21 | The element is either a window or an empty workspace.
22 |
23 | AeroSpace stores only one previously focused window in history,
24 | which means that if you close the previous window,
25 | `focus-back-and-forth` has no window to switch focus to.
26 | In that case, the command will exit with non-zero exit code.
27 |
28 | That's why it may be preferred to combine `focus-back-and-forth` with `workspace-back-and-forth`: +
29 | ----
30 | aerospace focus-back-and-forth || aerospace workspace-back-and-forth
31 | ----
32 |
33 | Also see: <>
34 | // end::body[]
35 |
36 | // =========================================================== Options
37 | include::util/conditional-options-header.adoc[]
38 |
39 | -h, --help:: Print help
40 |
41 | // =========================================================== Footer
42 | include::util/man-footer.adoc[]
43 |
--------------------------------------------------------------------------------
/docs/aerospace-focus-monitor.adoc:
--------------------------------------------------------------------------------
1 | = aerospace-focus-monitor(1)
2 | include::util/man-attributes.adoc[]
3 | :manname: aerospace-focus-monitor
4 | // tag::purpose[]
5 | :manpurpose: Focus monitor by relative direction, by order, or by pattern
6 | // end::purpose[]
7 |
8 | // =========================================================== Synopsis
9 | == Synopsis
10 | [verse]
11 | // tag::synopsis[]
12 | aerospace focus-monitor [-h|--help] [--wrap-around] (left|down|up|right)
13 | aerospace focus-monitor [-h|--help] [--wrap-around] (next|prev)
14 | aerospace focus-monitor [-h|--help] ...
15 |
16 | // end::synopsis[]
17 |
18 | // =========================================================== Description
19 | == Description
20 |
21 | // tag::body[]
22 | {manpurpose}
23 |
24 | // =========================================================== Options
25 | include::./util/conditional-options-header.adoc[]
26 |
27 | -h, --help:: Print help
28 | --wrap-around:: Make it possible to wrap around focus
29 |
30 | // =========================================================== Arguments
31 | include::./util/conditional-arguments-header.adoc[]
32 |
33 | (left|down|up|right)::
34 | Focus monitor in direction relative to the focused monitor
35 |
36 | (next|prev)::
37 | Focus next|prev monitor in order they appear in tray icon
38 |
39 | ...::
40 | Find the first monitor pattern in the list that doesn't describe the current monitor and focus it.
41 | Monitor pattern is the same as in `workspace-to-monitor-force-assignment` config option
42 |
43 | // end::body[]
44 |
45 | // =========================================================== Footer
46 | include::util/man-footer.adoc[]
47 |
--------------------------------------------------------------------------------
/docs/aerospace-fullscreen.adoc:
--------------------------------------------------------------------------------
1 | = aerospace-fullscreen(1)
2 | include::util/man-attributes.adoc[]
3 | :manname: aerospace-fullscreen
4 | // tag::purpose[]
5 | :manpurpose: Toggle the fullscreen mode for the focused window
6 | // end::purpose[]
7 |
8 | // =========================================================== Synopsis
9 | == Synopsis
10 | [verse]
11 | // tag::synopsis[]
12 | aerospace fullscreen [-h|--help] [--window-id ] [--no-outer-gaps]
13 | aerospace fullscreen [-h|--help] on [--window-id ] [--no-outer-gaps] [--fail-if-noop]
14 | aerospace fullscreen [-h|--help] off [--window-id ] [--fail-if-noop]
15 |
16 | // end::synopsis[]
17 |
18 | // =========================================================== Description
19 | == Description
20 |
21 | // tag::body[]
22 | {manpurpose}
23 |
24 | Switching to a different tiling window within the same workspace while the current focused window is in fullscreen mode results in the fullscreen window exiting fullscreen mode.
25 |
26 | // =========================================================== Options
27 | include::./util/conditional-options-header.adoc[]
28 |
29 | -h, --help:: Print help
30 | --no-outer-gaps:: Remove the outer gaps when in fullscreen mode
31 | --fail-if-noop:: Exit with non-zero exit code if already fullscreen or already not fullscreen
32 |
33 | --window-id ::
34 | include::./util/window-id-flag-desc.adoc[]
35 |
36 | // =========================================================== Arguments
37 | include::./util/conditional-arguments-header.adoc[]
38 |
39 | on, off::
40 | `on` means enter fullscreen mode. `off` means exit fullscreen mode.
41 | Toggle between the two if not specified
42 |
43 | // end::body[]
44 |
45 | // =========================================================== Footer
46 | include::util/man-footer.adoc[]
47 |
--------------------------------------------------------------------------------
/docs/aerospace-join-with.adoc:
--------------------------------------------------------------------------------
1 | = aerospace-join-with(1)
2 | include::util/man-attributes.adoc[]
3 | // tag::purpose[]
4 | :manpurpose: Put the focused window and the nearest node in the specified direction under a common parent container
5 | // end::purpose[]
6 | :manname: aerospace-join-with
7 |
8 | // =========================================================== Synopsis
9 | == Synopsis
10 | [verse]
11 | // tag::synopsis[]
12 | aerospace join-with [-h|--help] [--window-id ] (left|down|up|right)
13 |
14 | // end::synopsis[]
15 |
16 | // =========================================================== Description
17 | == Description
18 |
19 | // tag::body[]
20 | {manpurpose}
21 |
22 | // =========================================================== Examples
23 | include::util/conditional-examples-header.adoc[]
24 |
25 | Given this layout
26 |
27 | ----
28 | h_tiles
29 | ├── window 1
30 | ├── window 2 (focused)
31 | └── window 3
32 | ----
33 |
34 | `join-with right` will result in the following layout
35 |
36 | ----
37 | h_tiles
38 | ├── window 1
39 | └── v_tiles
40 | ├── window 2 (focused)
41 | └── window 3
42 | ----
43 |
44 | NOTE: `join-with` is a high-level replacement for i3's https://i3wm.org/docs/userguide.html#_splitting_containers[split command].
45 | There is an observation that the only reason why you might want to split a node is to put several windows under a common "umbrella" parent. Unlike `split`, `join-with` can be used with xref:guide.adoc#normalization[`enable-normalization-flatten-containers`]
46 |
47 | // =========================================================== Options
48 | include::util/conditional-options-header.adoc[]
49 |
50 | -h, --help:: Print help
51 |
52 | --window-id ::
53 | include::./util/window-id-flag-desc.adoc[]
54 |
55 | // end::body[]
56 |
57 | // =========================================================== Footer
58 | include::util/man-footer.adoc[]
59 |
--------------------------------------------------------------------------------
/docs/aerospace-list-exec-env-vars.adoc:
--------------------------------------------------------------------------------
1 | = aerospace-list-exec-env-vars(1)
2 | include::util/man-attributes.adoc[]
3 | :manname: aerospace-list-exec-env-vars
4 | // tag::purpose[]
5 | :manpurpose: List environment variables that exec-* commands and callbacks are run with
6 | // end::purpose[]
7 |
8 | // =========================================================== Synopsis
9 | == Synopsis
10 | [verse]
11 | // tag::synopsis[]
12 | aerospace list-exec-env-vars [-h|--help]
13 |
14 | // end::synopsis[]
15 |
16 | // =========================================================== Description
17 | == Description
18 |
19 | // tag::body[]
20 | {manpurpose}
21 |
22 | Examples of commands and callbacks:
23 |
24 | * `aerospace exec-and-forget` command
25 | * `exec-on-workspace-change-callback`
26 |
27 | // end::body[]
28 |
29 | // =========================================================== Options
30 | include::util/conditional-options-header.adoc[]
31 |
32 | -h, --help:: Print help
33 |
34 | // =========================================================== Footer
35 | include::util/man-footer.adoc[]
36 |
--------------------------------------------------------------------------------
/docs/aerospace-list-modes.adoc:
--------------------------------------------------------------------------------
1 | = aerospace-list-modes(1)
2 | include::util/man-attributes.adoc[]
3 | // tag::purpose[]
4 | :manpurpose: Print a list of modes currently specified in the configuration
5 | // end::purpose[]
6 | :manname: aerospace-list-modes
7 |
8 | // =========================================================== Synopsis
9 | == Synopsis
10 | [verse]
11 | // tag::synopsis[]
12 | aerospace list-modes [-h|--help] [--current]
13 |
14 | // end::synopsis[]
15 |
16 | // =========================================================== Description
17 | == Description
18 |
19 | // tag::body[]
20 | {manpurpose}
21 |
22 | See xref:guide.adoc#binding-modes[the guide] for documentation about binding modes
23 |
24 | // =========================================================== Options
25 | include::util/conditional-options-header.adoc[]
26 |
27 | -h, --help:: Print help
28 |
29 | --current::
30 | Only print the currently active mode
31 |
32 | // end::body[]
33 |
34 | // =========================================================== Footer
35 | include::util/man-footer.adoc[]
36 |
--------------------------------------------------------------------------------
/docs/aerospace-macos-native-fullscreen.adoc:
--------------------------------------------------------------------------------
1 | = aerospace-macos-native-fullscreen(1)
2 | include::util/man-attributes.adoc[]
3 | :manname: aerospace-macos-native-fullscreen
4 | // tag::purpose[]
5 | :manpurpose: Toggle macOS fullscreen for the focused window
6 | // end::purpose[]
7 |
8 | // =========================================================== Synopsis
9 | == Synopsis
10 | [verse]
11 | // tag::synopsis[]
12 | aerospace macos-native-fullscreen [-h|--help] [--window-id ]
13 | aerospace macos-native-fullscreen [-h|--help] [--window-id ] [--fail-if-noop] on
14 | aerospace macos-native-fullscreen [-h|--help] [--window-id ] [--fail-if-noop] off
15 |
16 | // end::synopsis[]
17 |
18 | // =========================================================== Description
19 | == Description
20 |
21 | // tag::body[]
22 | {manpurpose}
23 |
24 | // =========================================================== Options
25 | include::./util/conditional-options-header.adoc[]
26 |
27 | -h, --help:: Print help
28 | --fail-if-noop:: Exit with non-zero exit code if already fullscreen or already not fullscreen
29 |
30 | --window-id ::
31 | include::./util/window-id-flag-desc.adoc[]
32 |
33 | // =========================================================== Arguments
34 | include::./util/conditional-arguments-header.adoc[]
35 |
36 | on, off::
37 | `on` means enter fullscreen mode.
38 | `off` means exit fullscreen mode.
39 | Toggle between the two if not specified
40 |
41 | // end::body[]
42 |
43 | // =========================================================== Footer
44 | include::util/man-footer.adoc[]
45 |
--------------------------------------------------------------------------------
/docs/aerospace-macos-native-minimize.adoc:
--------------------------------------------------------------------------------
1 | = aerospace-macos-native-minimize(1)
2 | include::util/man-attributes.adoc[]
3 | :manname: aerospace-macos-native-minimize
4 | // tag::purpose[]
5 | :manpurpose: Minimize focused window
6 | // end::purpose[]
7 |
8 | // =========================================================== Synopsis
9 | == Synopsis
10 | [verse]
11 | // tag::synopsis[]
12 | aerospace macos-native-minimize [-h|--help] [--window-id ]
13 |
14 | // end::synopsis[]
15 |
16 | // =========================================================== Description
17 | == Description
18 |
19 | // tag::body[]
20 | {manpurpose}
21 |
22 | // =========================================================== Options
23 | include::./util/conditional-options-header.adoc[]
24 |
25 | -h, --help:: Print help
26 |
27 | --window-id ::
28 | include::./util/window-id-flag-desc.adoc[]
29 |
30 | // end::body[]
31 |
32 | // =========================================================== Footer
33 | include::util/man-footer.adoc[]
34 |
--------------------------------------------------------------------------------
/docs/aerospace-mode.adoc:
--------------------------------------------------------------------------------
1 | = aerospace-mode(1)
2 | include::util/man-attributes.adoc[]
3 | // tag::purpose[]
4 | :manpurpose: Activate the specified binding mode
5 | // end::purpose[]
6 | :manname: aerospace-mode
7 |
8 | // =========================================================== Synopsis
9 | == Synopsis
10 | [verse]
11 | // tag::synopsis[]
12 | aerospace mode [-h|--help]
13 |
14 | // end::synopsis[]
15 |
16 | // =========================================================== Description
17 | == Description
18 |
19 | // tag::body[]
20 | {manpurpose}
21 |
22 | See xref:guide.adoc#binding-modes[the guide] for documentation about binding modes
23 | // end::body[]
24 |
25 | // =========================================================== Options
26 | include::util/conditional-options-header.adoc[]
27 |
28 | -h, --help:: Print help
29 |
30 | // =========================================================== Footer
31 | include::util/man-footer.adoc[]
32 |
--------------------------------------------------------------------------------
/docs/aerospace-move-node-to-workspace.adoc:
--------------------------------------------------------------------------------
1 | = aerospace-move-node-to-workspace(1)
2 | include::util/man-attributes.adoc[]
3 | :manname: aerospace-move-node-to-workspace
4 | // tag::purpose[]
5 | :manpurpose: Move the focused window to the specified workspace
6 | // end::purpose[]
7 |
8 | // =========================================================== Synopsis
9 | == Synopsis
10 | [verse]
11 | // tag::synopsis[]
12 | aerospace move-node-to-workspace [-h|--help] [--focus-follows-window] [--wrap-around]
13 | (next|prev)
14 | aerospace move-node-to-workspace [-h|--help] [--focus-follows-window] [--fail-if-noop]
15 | [--window-id ]
16 |
17 | // end::synopsis[]
18 |
19 | // =========================================================== Description
20 | == Description
21 |
22 | // tag::body[]
23 | {manpurpose}
24 |
25 | `(next|prev)` is identical to `workspace (next|prev)`
26 |
27 | // =========================================================== Options
28 | include::./util/conditional-options-header.adoc[]
29 |
30 | -h, --help:: Print help
31 | --wrap-around:: Make it possible to jump between first and last workspaces using (next|prev)
32 | --fail-if-noop:: Exit with non-zero code if move window to workspace it already belongs to
33 |
34 | --focus-follows-window::
35 | Make sure that the window in question receives focus after moving.
36 | This flag is a shortcut for manually running `aerospace-workspace`/`aerospace-focus` after `move-node-to-workspace` successful execution.
37 |
38 | --window-id ::
39 | include::./util/window-id-flag-desc.adoc[]
40 |
41 | // =========================================================== Arguments
42 | include::./util/conditional-arguments-header.adoc[]
43 |
44 | (next|prev):: Move window to next or prev workspace
45 | :: Specifies workspace name where to move window to
46 |
47 | // end::body[]
48 |
49 | // =========================================================== Footer
50 | include::util/man-footer.adoc[]
51 |
--------------------------------------------------------------------------------
/docs/aerospace-reload-config.adoc:
--------------------------------------------------------------------------------
1 | = aerospace-reload-config(1)
2 | include::util/man-attributes.adoc[]
3 | :manname: aerospace-reload-config
4 | // tag::purpose[]
5 | :manpurpose: Reload currently active config
6 | // end::purpose[]
7 |
8 | // =========================================================== Synopsis
9 | == Synopsis
10 | [verse]
11 | // tag::synopsis[]
12 | aerospace reload-config [-h|--help] [--no-gui] [--dry-run]
13 |
14 | // end::synopsis[]
15 |
16 | // =========================================================== Description
17 | == Description
18 |
19 | // tag::body[]
20 | {manpurpose}
21 |
22 | If the config contains errors they will be printed to stdout, and GUI will open to show the errors.
23 |
24 | // =========================================================== Options
25 | include::util/conditional-options-header.adoc[]
26 |
27 | -h, --help:: Print help
28 | --no-gui:: Don't open GUI to show error. Only use stdout to report errors
29 | --dry-run:: Validate the config and show errors (if any) but don't reload the config
30 |
31 | include::util/conditional-exit-code-header.adoc[]
32 |
33 | 0:: Success. The config is reloaded successfully.
34 | non-zero exit code:: Failure. The config contains errors.
35 |
36 | // end::body[]
37 |
38 | // =========================================================== Footer
39 | include::util/man-footer.adoc[]
40 |
--------------------------------------------------------------------------------
/docs/aerospace-resize.adoc:
--------------------------------------------------------------------------------
1 | = aerospace-resize(1)
2 | include::util/man-attributes.adoc[]
3 | :manname: aerospace-resize
4 | // tag::purpose[]
5 | :manpurpose: Resize the focused window
6 | // end::purpose[]
7 |
8 | // =========================================================== Synopsis
9 | == Synopsis
10 | [verse]
11 | // tag::synopsis[]
12 | aerospace resize [-h|--help] [--window-id ] (smart|smart-opposite|width|height) [+|-]
13 |
14 | // end::synopsis[]
15 |
16 | // =========================================================== Description
17 | == Description
18 |
19 | // tag::body[]
20 | {manpurpose}
21 |
22 | The dimension to resize is chosen by the first argument
23 |
24 | * `width` changes width
25 | * `height` changes height
26 | * `smart` changes width if the parent has horizontal orientation, and
27 | it changes height if the parent has vertical orientation
28 | * `smart-opposite` does resizes the opposite axis of smart
29 |
30 | Second argument controls how much the size changes
31 |
32 | * If the `` is prefixed with `+` then the dimension is increased
33 | * If the `` is prefixed with `-` then the dimension is decreased
34 | * If the `` is prefixed with neither `+` nor `-` then the command changes the absolute value of the dimension
35 |
36 | // =========================================================== Options
37 | include::./util/conditional-options-header.adoc[]
38 |
39 | -h, --help:: Print help
40 |
41 | --window-id ::
42 | include::./util/window-id-flag-desc.adoc[]
43 |
44 | // end::body[]
45 |
46 | // =========================================================== Footer
47 | include::util/man-footer.adoc[]
48 |
--------------------------------------------------------------------------------
/docs/aerospace-split.adoc:
--------------------------------------------------------------------------------
1 | = aerospace-split(1)
2 | include::util/man-attributes.adoc[]
3 | :manname: aerospace-split
4 | // tag::purpose[]
5 | :manpurpose: Split focused window
6 | // end::purpose[]
7 |
8 | // =========================================================== Synopsis
9 | == Synopsis
10 | [verse]
11 | // tag::synopsis[]
12 | aerospace split [-h|--help] [--window-id ] (horizontal|vertical|opposite)
13 |
14 | // end::synopsis[]
15 |
16 | // =========================================================== Description
17 | == Description
18 |
19 | // tag::body[]
20 | `split` command exist solely for compatibility with i3.
21 | Unless you're hardcore i3 user who knows what they are doing, it's recommended to use `join-with`
22 |
23 | *If the parent of focused window contains more than one child*, then the command
24 |
25 | . Creates a new tiling container
26 | . Replaces the focused window with the container
27 | . Puts the focused window into the container as its the only child
28 |
29 | The argument configures orientation of the newly created container.
30 | `opposite` means opposite orientation compared to the parent container.
31 |
32 | *If the parent of the focused window contains only a single child* (the window itself), then `split` command changes the orientation of the parent container
33 |
34 | IMPORTANT: `split` command has no effect if `enable-normalization-flatten-containers` is turned on.
35 | Consider using `join-with` if you want to keep `enable-normalization-flatten-containers` enabled
36 |
37 | // =========================================================== Options
38 | include::util/conditional-options-header.adoc[]
39 |
40 | -h, --help:: Print help
41 |
42 | --window-id ::
43 | include::./util/window-id-flag-desc.adoc[]
44 |
45 | // end::body[]
46 |
47 | // =========================================================== Footer
48 | include::util/man-footer.adoc[]
49 |
--------------------------------------------------------------------------------
/docs/aerospace-summon-workspace.adoc:
--------------------------------------------------------------------------------
1 | = aerospace-summon-workspace(1)
2 | include::util/man-attributes.adoc[]
3 | // tag::purpose[]
4 | :manpurpose: Move the requested workspace to the focused monitor.
5 | // end::purpose[]
6 | :manname: aerospace-summon-workspace
7 |
8 | // =========================================================== Synopsis
9 | == Synopsis
10 | [verse]
11 | // tag::synopsis[]
12 | aerospace summon-workspace [-h|--help] [--fail-if-noop]
13 |
14 | // end::synopsis[]
15 |
16 | // =========================================================== Description
17 | == Description
18 |
19 | // tag::body[]
20 | {manpurpose}
21 | The moved workspace becomes focused.
22 | The behavior is identical to Xmonad.
23 |
24 | The command makes sense only in multi-monitor setup.
25 | In single monitor setup the command is identical to `workspace` command.
26 |
27 | // =========================================================== Options
28 | include::./util/conditional-options-header.adoc[]
29 |
30 | -h, --help:: Print help
31 | --fail-if-noop:: Exit with non-zero exit code if the workspace already visible on the focused monitor.
32 |
33 | // =========================================================== Arguments
34 | include::./util/conditional-arguments-header.adoc[]
35 |
36 | :: The workspace to operate on.
37 |
38 | // end::body[]
39 |
40 | // =========================================================== Footer
41 | include::util/man-footer.adoc[]
42 |
--------------------------------------------------------------------------------
/docs/aerospace-trigger-binding.adoc:
--------------------------------------------------------------------------------
1 | = aerospace-trigger-binding(1)
2 | include::util/man-attributes.adoc[]
3 | :manname: aerospace-trigger-binding
4 | // tag::purpose[]
5 | :manpurpose: Trigger AeroSpace binding as if it was pressed by user
6 | // end::purpose[]
7 |
8 | // =========================================================== Synopsis
9 | == Synopsis
10 | [verse]
11 | // tag::synopsis[]
12 | aerospace trigger-binding [-h|--help] --mode
13 |
14 | // end::synopsis[]
15 |
16 | // =========================================================== Description
17 | == Description
18 |
19 | // tag::body[]
20 | {manpurpose}
21 |
22 | You can use aerospace-config command to inspect available bindings: +
23 | `aerospace config --get mode.main.binding --keys`
24 |
25 | // =========================================================== Options
26 | include::util/conditional-options-header.adoc[]
27 |
28 | -h, --help:: Print help
29 | --mode :: Mode to search `` in
30 |
31 | // =========================================================== Arguments
32 | include::util/conditional-arguments-header.adoc[]
33 |
34 | :: Binding to trigger
35 |
36 | // =========================================================== Examples
37 | include::util/conditional-examples-header.adoc[]
38 |
39 | * Run alphabetically first binding from config (useless and synthetic example): +
40 | `aerospace trigger-binding --mode main "$(aerospace config --get mode.main.binding --keys | head -1)"`
41 | * Trigger `alt-tab` binding: +
42 | `aerospace trigger-binding --mode main alt-tab`
43 |
44 | // end::body[]
45 |
46 | // =========================================================== Footer
47 | include::util/man-footer.adoc[]
48 |
--------------------------------------------------------------------------------
/docs/aerospace-volume.adoc:
--------------------------------------------------------------------------------
1 | = aerospace-volume(1)
2 | include::util/man-attributes.adoc[]
3 | // tag::purpose[]
4 | :manpurpose: Manipulate volume
5 | // end::purpose[]
6 | :manname: aerospace-volume
7 |
8 | // =========================================================== Synopsis
9 | == Synopsis
10 | [verse]
11 | // tag::synopsis[]
12 | aerospace volume [-h|--help] (up|down)
13 | aerospace volume [-h|--help] (mute-toggle|mute-off|mute-on)
14 | aerospace volume [-h|--help] set
15 |
16 | // end::synopsis[]
17 |
18 | // =========================================================== Description
19 | == Description
20 |
21 | // tag::body[]
22 | {manpurpose}
23 |
24 | // =========================================================== Options
25 | include::./util/conditional-options-header.adoc[]
26 |
27 | -h, --help:: Print help
28 |
29 | // =========================================================== Arguments
30 | include::./util/conditional-arguments-header.adoc[]
31 |
32 | (up|down):: Increase or decrease the volume
33 | (mute-toggle|mute-on|mute-off):: Toggle/On/Off mute
34 | set :: Set volume to the exact value on scale from 0 to 100
35 |
36 | // end::body[]
37 |
38 | // =========================================================== Footer
39 | include::util/man-footer.adoc[]
40 |
--------------------------------------------------------------------------------
/docs/aerospace-workspace-back-and-forth.adoc:
--------------------------------------------------------------------------------
1 | = aerospace-workspace-back-and-forth(1)
2 | include::util/man-attributes.adoc[]
3 | // tag::purpose[]
4 | :manpurpose: Switch between the focused workspace and previously focused workspace back and forth
5 | // end::purpose[]
6 | :manname: aerospace-workspace-back-and-forth
7 |
8 | // =========================================================== Synopsis
9 | == Synopsis
10 | [verse]
11 | // tag::synopsis[]
12 | aerospace workspace-back-and-forth [-h|--help]
13 |
14 | // end::synopsis[]
15 |
16 | // =========================================================== Description
17 | == Description
18 |
19 | // tag::body[]
20 | {manpurpose}
21 |
22 | Unlike `focus-back-and-forth`, `workspace-back-and-forth` always succeeds.
23 | Because unlike windows, workspaces can not be "closed".
24 | Workspaces are name-addressable objects.
25 | They are created and destroyed on the fly.
26 |
27 | Also see: <>
28 | // end::body[]
29 |
30 | // =========================================================== Options
31 | include::util/conditional-options-header.adoc[]
32 |
33 | -h, --help:: Print help
34 |
35 | // =========================================================== Footer
36 | include::util/man-footer.adoc[]
37 |
--------------------------------------------------------------------------------
/docs/aerospace-workspace.adoc:
--------------------------------------------------------------------------------
1 | = aerospace-workspace(1)
2 | include::util/man-attributes.adoc[]
3 | :manname: aerospace-workspace
4 | // tag::purpose[]
5 | :manpurpose: Focus the specified workspace
6 | // end::purpose[]
7 |
8 | // =========================================================== Synopsis
9 | == Synopsis
10 | [verse]
11 | // tag::synopsis[]
12 | aerospace workspace [-h|--help] [--auto-back-and-forth] [--fail-if-noop]
13 | aerospace workspace [-h|--help] [--wrap-around] (next|prev)
14 |
15 | // end::synopsis[]
16 |
17 | // =========================================================== Description
18 | == Description
19 |
20 | // tag::body[]
21 | *1. syntax*
22 |
23 | {manpurpose}
24 |
25 | *2. (next|prev) syntax*
26 |
27 | Focuses next or previous workspace in *the list*.
28 |
29 | * If stdin is not TTY and stdin contains non whitespace characters then *the list* is taken from stdin
30 | * Otherwise, *the list* is defined as all workspaces on focused monitor in alphabetical order
31 |
32 | // =========================================================== Options
33 | include::util/conditional-options-header.adoc[]
34 |
35 | -h, --help:: Print help
36 | --wrap-around:: Make it possible to jump between first and last workspaces using `(next|prev)`
37 |
38 | --auto-back-and-forth::
39 | Automatic `back-and-forth` when switching to already focused workspace.
40 | Incompatible with `--fail-if-noop`
41 |
42 | --fail-if-noop::
43 | Exit with non-zero exit code if switch to the already focused workspace
44 | Incompatible with `--auto-back-and-forth`
45 |
46 | // =========================================================== Examples
47 | include::util/conditional-examples-header.adoc[]
48 |
49 | * Go to the next non empty workspace on the focused monitor: +
50 | `aerospace list-workspaces --monitor focused --empty no | aerospace workspace next`
51 |
52 | // end::body[]
53 |
54 | // =========================================================== Footer
55 | include::util/man-footer.adoc[]
56 |
--------------------------------------------------------------------------------
/docs/aerospace.adoc:
--------------------------------------------------------------------------------
1 | = aerospace(1)
2 | include::util/man-attributes.adoc[]
3 | :manname: aerospace
4 | :manpurpose: i3-like tiling window manager for macOS
5 |
6 | == Synopsis
7 | [verse]
8 | aerospace [-h|--help] [-v|--version] [...] [...]
9 |
10 | == Description
11 |
12 | AeroSpace is an i3-like tiling window manager for macOS
13 |
14 | *aerospace* command line program is used to manipulate AeroSpace and query its state.
15 |
16 | See https://nikitabobko.github.io/AeroSpace/commands for available options
17 |
18 | See each individual man page for and
19 |
20 | == Options
21 |
22 | -h, --help:: Print help
23 |
24 | include::util/man-footer.adoc[]
25 |
--------------------------------------------------------------------------------
/docs/assets/h_accordion.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nikitabobko/AeroSpace/74bb715eb1a1818a98ecb3120e244b91d01f50b4/docs/assets/h_accordion.png
--------------------------------------------------------------------------------
/docs/assets/h_tiles.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nikitabobko/AeroSpace/74bb715eb1a1818a98ecb3120e244b91d01f50b4/docs/assets/h_tiles.png
--------------------------------------------------------------------------------
/docs/assets/icon.png:
--------------------------------------------------------------------------------
1 | ../../resources/Assets.xcassets/AppIcon.appiconset/icon.png
--------------------------------------------------------------------------------
/docs/assets/tree.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nikitabobko/AeroSpace/74bb715eb1a1818a98ecb3120e244b91d01f50b4/docs/assets/tree.png
--------------------------------------------------------------------------------
/docs/assets/v_accordion.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nikitabobko/AeroSpace/74bb715eb1a1818a98ecb3120e244b91d01f50b4/docs/assets/v_accordion.png
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Redirect
7 |
8 |
9 | Redirect
10 |
11 |
12 |
--------------------------------------------------------------------------------
/docs/util/all-monitors-option.adoc:
--------------------------------------------------------------------------------
1 | --all::
2 | Alias for `--monitor all`.
3 | Please use this option *with cautious*.
4 | Use it when you really need to get workspaces/windows from *all monitors*.
5 | +
6 | For multi-monitor setup `--monitor focused` is almost always a preferred option.
7 | If you're automating something then you don't want to mess up with workspaces/windows on a different monitor.
8 | +
9 | With great power comes great responsibility.
10 |
--------------------------------------------------------------------------------
/docs/util/conditional-arguments-header.adoc:
--------------------------------------------------------------------------------
1 | ifndef::env-site[]
2 | == Arguments
3 | endif::[]
4 |
5 | ifdef::env-site[]
6 | [.lead]
7 | **ARGUMENTS**
8 | endif::[]
9 |
--------------------------------------------------------------------------------
/docs/util/conditional-examples-header.adoc:
--------------------------------------------------------------------------------
1 | ifndef::env-site[]
2 | == Examples
3 | endif::[]
4 |
5 | ifdef::env-site[]
6 | [.lead]
7 | **EXAMPLES**
8 | endif::[]
9 |
--------------------------------------------------------------------------------
/docs/util/conditional-exit-code-header.adoc:
--------------------------------------------------------------------------------
1 | ifndef::env-site[]
2 | == Exit code
3 | endif::[]
4 |
5 | ifdef::env-site[]
6 | [.lead]
7 | **EXIT CODE**
8 | endif::[]
9 |
--------------------------------------------------------------------------------
/docs/util/conditional-options-header.adoc:
--------------------------------------------------------------------------------
1 | ifndef::env-site[]
2 | == Options
3 | endif::[]
4 |
5 | ifdef::env-site[]
6 | [.lead]
7 | **OPTIONS**
8 | endif::[]
9 |
--------------------------------------------------------------------------------
/docs/util/conditional-output-format-header.adoc:
--------------------------------------------------------------------------------
1 | ifndef::env-site[]
2 | == Output Format
3 | endif::[]
4 |
5 | ifdef::env-site[]
6 | [.lead]
7 | **OUTPUT FORMAT**
8 | endif::[]
9 |
--------------------------------------------------------------------------------
/docs/util/header.adoc:
--------------------------------------------------------------------------------
1 | ====
2 | AeroSpace is an i3-like tiling window manager for macOS
3 |
4 | *Project homepage*: https://github.com/nikitabobko/AeroSpace
5 |
6 | image:assets/icon.png[300,300,float="right"]
7 |
8 | * xref:guide.adoc[AeroSpace Guide]
9 | * xref:commands.adoc[AeroSpace Commands]
10 | * xref:goodies.adoc[AeroSpace Goodies]
11 | ====
12 |
--------------------------------------------------------------------------------
/docs/util/man-attributes.adoc:
--------------------------------------------------------------------------------
1 | ifndef::env-site[]
2 | :doctype: manpage
3 | :manmanual: AeroSpace Manual
4 | :mansource: AeroSpace
5 | endif::[]
6 |
--------------------------------------------------------------------------------
/docs/util/man-footer.adoc:
--------------------------------------------------------------------------------
1 | == Resources
2 |
3 | *Project homepage:* https://github.com/nikitabobko/AeroSpace +
4 | *Guide:* https://nikitabobko.github.io/AeroSpace/guide +
5 |
6 | == BUGS
7 |
8 | Bugs can be reported to https://github.com/nikitabobko/AeroSpace/discussions/categories/potential-bugs
9 |
10 | Maintainers will move verified bugs to https://github.com/nikitabobko/AeroSpace/issues
11 |
12 | == License
13 |
14 | Copyright (C) 2023 Nikita Bobko +
15 | Free use of this software is granted under the terms of the MIT License +
16 | You can find the full text of AeroSpace license and its dependencies in the 'legal' directory of the distributed zip archieve.
17 |
18 | == AUTHOR
19 |
20 | Nikita Bobko and contributors
21 |
--------------------------------------------------------------------------------
/docs/util/monitor-option.adoc:
--------------------------------------------------------------------------------
1 | --monitor ::
2 | Filter results to only print workspaces/windows that are attached to specified monitors.
3 | `` is a space separated list of monitor IDs. +
4 | +
5 | Possible monitors IDs: +
6 | +
7 | . 1-based index of a monitor as if monitors were ordered horizontally from left to right
8 | . `all` is a special monitor ID that represents all monitors
9 | . `mouse` is a special monitor ID that represents monitor with the mouse
10 | . `focused` is a special monitor ID that represents the focused monitor
11 |
--------------------------------------------------------------------------------
/docs/util/site-attributes.adoc:
--------------------------------------------------------------------------------
1 | :idprefix:
2 | :idseparator: -
3 | :prewrap!:
4 | :relfilesuffix:
5 | :sectanchors:
6 | :sectlinks:
7 | :sectnums:
8 | :source-highlighter: pygments
9 | :toc: left
10 | :env-site:
11 | :favicon: ./assets/icon.png
12 |
--------------------------------------------------------------------------------
/docs/util/window-id-flag-desc.adoc:
--------------------------------------------------------------------------------
1 | Act on the specified window instead of the focused window
2 |
--------------------------------------------------------------------------------
/docs/util/workspace-flag-desc.adoc:
--------------------------------------------------------------------------------
1 | Act on the specified workspace instead of the focused workspace
2 |
--------------------------------------------------------------------------------
/generate-shell-parser.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | cd "$(dirname "$0")"
3 | source ./script/setup.sh
4 |
5 | ./script/install-dep.sh --antlr
6 | ./.deps/python-venv/bin/antlr4 -v 4.13.1 -no-listener -Dlanguage=Swift \
7 | -o ./ShellParserGenerated/Sources/ShellParserGenerated \
8 | ./grammar/ShellLexer.g4 \
9 | ./grammar/ShellParser.g4
10 |
11 |
12 | mv ./ShellParserGenerated/Sources/ShellParserGenerated/grammar/*.swift ./ShellParserGenerated/Sources/ShellParserGenerated/
13 | rm -rf ./ShellParserGenerated/Sources/ShellParserGenerated/grammar # Antlr generates weird *.interp and *.tokens files
14 |
15 | # Sources/ShellParserGenerated/ShellParser.swift:557:7: warning: variable '_prevctx' was written to, but never read
16 | # var _prevctx: CmdContext = _localctx
17 | sed -i '' '/_prevctx/d' ./ShellParserGenerated/Sources/ShellParserGenerated/ShellParser.swift
18 |
--------------------------------------------------------------------------------
/grammar/ShellLexer.g4:
--------------------------------------------------------------------------------
1 | // shell lexer grammar. Powered by https://github.com/antlr/antlr4
2 | // Use ./generate-shell-parser.sh to regenerate grammar code
3 | lexer grammar ShellLexer;
4 |
5 | TRIPLE_QUOTE : '"""' | '\'\'\'' ; // Reserved
6 |
7 | SINGLE_QUOTED_STRING : '\'' .*? '\'' ;
8 |
9 | LDQUOTE : '"' -> pushMode(IN_DSTRING) ;
10 | LPAR : '(' -> pushMode(DEFAULT_MODE) ;
11 | INTERPOLATION_START : '$(' -> pushMode(DEFAULT_MODE) ;
12 | RPAR : ')' {
13 | _ = try? popMode()
14 | } ;
15 |
16 | // Keywords (some of them are unused, just reserved)
17 | ELIF : 'elif' NL* ;
18 | IF : 'if' NL* ;
19 | SWITCH : 'switch' NL* ;
20 | CASE : 'case' NL* ;
21 | DO : 'do' NL* ;
22 | THEN : 'then' NL* ;
23 | ELSE : 'else' NL* ;
24 | FOR : 'for' NL* ;
25 | WHILE : 'while' NL* ;
26 | CATCH : 'catch' NL* ;
27 | IN : 'in' NL* ;
28 | END : 'end' NL* ;
29 | DEFER : 'defer' NL* ;
30 |
31 | AND : '&&' ;
32 | PIPE : '|' ;
33 | OR : '||' ;
34 | SEMICOLON : ';' ;
35 | NL : SPACES? ('\r')? '\n' ;
36 | WORD : ([a-zA-Z] | '.' | '_' | '-' | '/')+ ;
37 | ARG : ([a-zA-Z0-9] | '.' | '_' | '-' | '/' | '+' | ',' | '=' | '!' | '%' | '{' | '}' | '^')+ ;
38 |
39 | ESCAPE_NEWLINE : '\\' SPACES? COMMENT? '\n' -> skip ;
40 | COMMENT : '#' ~('\n')* -> skip ;
41 | SPACES : [ \t]+ -> skip ;
42 |
43 | ANY : . ; // Catch all other
44 |
45 | mode IN_DSTRING;
46 | TEXT : ~('\\' | '"' | '$')+ ;
47 | INTERPOLATION_START_IN_DSTRING : '$(' -> pushMode(DEFAULT_MODE) ;
48 | ESCAPE_SEQUENCE : '\\' . ;
49 | RDQUOTE : '"' {
50 | _ = try? popMode()
51 | } ;
52 |
--------------------------------------------------------------------------------
/install-from-sources.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | cd "$(dirname "$0")"
3 | source ./script/setup.sh
4 |
5 | rebuild=1
6 | while test $# -gt 0; do
7 | case $1 in
8 | --dont-rebuild) rebuild=0; shift ;;
9 | *) echo "Unknown option $1"; exit 1 ;;
10 | esac
11 | done
12 |
13 | if test $rebuild == 1; then
14 | ./build-release.sh
15 | fi
16 |
17 | brew list aerospace-dev > /dev/null 2>&1 && brew uninstall aerospace-dev
18 | brew list aerospace > /dev/null 2>&1 && brew uninstall aerospace
19 |
20 | # Override HOMEBREW_CACHE. Otherwise, homebrew refuses to "redownload" the snapshot file
21 | # Maybe there is a better way, I don't know
22 | rm -rf /tmp/aerospace-from-sources-brew-cache
23 | env HOMEBREW_CACHE=/tmp/aerospace-from-sources-brew-cache brew install --cask ./.release/aerospace-dev.rb
24 |
--------------------------------------------------------------------------------
/legal/LICENSE.txt:
--------------------------------------------------------------------------------
1 | ../LICENSE.txt
--------------------------------------------------------------------------------
/legal/README.md:
--------------------------------------------------------------------------------
1 | # LICENSE
2 |
3 | The AeroSpace itself is licensed under MIT. See [LICENSE](./LICENSE.txt) for the full license text.
4 |
5 | ## Bundled dependencies and materials
6 |
7 | AeroSpace bundles the following dependencies and uses the following materials:
8 |
9 | **BlueSocket**.
10 | [BlueSocket GitHub link](https://github.com/Kitura/BlueSocket).
11 | [BlueSocket Apache 2.0 license](./third-party-license/LICENSE-BlueSocket.txt).
12 | BlueSocket is used as a more convenient Swift wrapper around UNIX C socket API.
13 |
14 | **HotKey**.
15 | [HotKey GitHub link](https://github.com/soffes/HotKey).
16 | [HotKey MIT license](./third-party-license/LICENSE-HotKey.txt).
17 | HotKey is used as a more convenient wrapper around macOS Carbon API to listen for global shortcuts.
18 |
19 | **TOMLKIT**.
20 | [TOMLKIT GitHub link](https://github.com/LebJe/TOMLKit).
21 | [TOMLKIT MIT license](./third-party-license/LICENSE-TOMLKIT.txt).
22 | TOMLKIT is used as a more convenient Swift wrapper around tomlplusplus C++ API.
23 |
24 | **tomlplusplus**.
25 | [tomlplusplus GitHub link](https://github.com/marzer/tomlplusplus).
26 | [tomlplusplus MIT license](./third-party-license/LICENSE-tomlplusplus.txt).
27 | tomlplusplus is used as TOML parser. tomlplusplus is used indirectly through TOMLKIT Swift API.
28 |
29 | **ANTLR v4**.
30 | [ANTLR v4 GitHub link](https://github.com/antlr/antlr4).
31 | [ANTLR BSD-3 license](./third-party-license/LICENSE-antlr.txt).
32 | ANTLR is used to parse AeroSpace built-in shell like language.
33 |
34 | **swift-collections**.
35 | [swift-collections GitHub link](https://github.com/apple/swift-collections).
36 | [swift-collections Apache 2.0 license](./third-party-license/LICENSE-swift-collections.txt).
37 | swift-collections is used for more advanced Swift collections.
38 |
39 | **ISSoundAdditions**
40 | [ISSoundAdditions GitHub link](https://github.com/InerziaSoft/ISSoundAdditions).
41 | [ISSoundAdditions MIT license](./third-party-license/LICENSE-ISSoundAdditions.txt).
42 | ISSoundAdditions is used as a convenient API to change system volume.
43 |
--------------------------------------------------------------------------------
/legal/third-party-license/LICENSE-HotKey.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2017–2019 Sam Soffes, http://soff.es
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/legal/third-party-license/LICENSE-ISSoundAdditions.txt:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 InerziaSoft - Massimo and Alessio Moiso.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/legal/third-party-license/LICENSE-TOMLKIT.txt:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Jeff Lebrun
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/legal/third-party-license/LICENSE-antlr.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2012-2022 The ANTLR Project. All rights reserved.
2 |
3 | Redistribution and use in source and binary forms, with or without
4 | modification, are permitted provided that the following conditions
5 | are met:
6 |
7 | 1. Redistributions of source code must retain the above copyright
8 | notice, this list of conditions and the following disclaimer.
9 |
10 | 2. Redistributions in binary form must reproduce the above copyright
11 | notice, this list of conditions and the following disclaimer in the
12 | documentation and/or other materials provided with the distribution.
13 |
14 | 3. Neither name of copyright holders nor the names of its contributors
15 | may be used to endorse or promote products derived from this software
16 | without specific prior written permission.
17 |
18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 | ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR
22 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
23 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
24 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
25 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
26 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
27 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
28 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 |
--------------------------------------------------------------------------------
/legal/third-party-license/LICENSE-tomlplusplus.txt:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) Mark Gillard
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
6 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
7 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
11 | Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
14 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
15 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
16 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
17 |
--------------------------------------------------------------------------------
/makefile:
--------------------------------------------------------------------------------
1 | # makefile is used to make :make command in vim work out of the box
2 | .PHONY: build test
3 |
4 | build:
5 | ./build-debug.sh
6 |
7 | test:
8 | ./run-tests.sh
9 |
--------------------------------------------------------------------------------
/project.yml:
--------------------------------------------------------------------------------
1 | # Xcode project configuration. Managed by https://github.com/yonaskolb/XcodeGen
2 | # Xcode is only used to build the release App Bundle. Debug builds only use Swift Package Manager
3 |
4 | name: AeroSpace
5 |
6 | packages:
7 | AeroSpacePackage:
8 | path: .
9 |
10 | configs:
11 | Debug: debug
12 | Release: release
13 |
14 | targets:
15 | AeroSpace:
16 | type: application
17 | platform: macOS
18 | deploymentTarget: "13.0"
19 | sources:
20 | - "Sources/AeroSpaceApp"
21 | - "resources"
22 | - "docs/config-examples/default-config.toml"
23 | dependencies:
24 | - package: AeroSpacePackage
25 | product: AppBundle
26 | # https://developer.apple.com/documentation/xcode/build-settings-reference
27 | settings:
28 | base:
29 | SWIFT_VERSION: 6.0
30 | GENERATE_INFOPLIST_FILE: YES
31 | MARKETING_VERSION: ${XCODEGEN_AEROSPACE_VERSION}
32 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/LaunchServicesKeys.html#//apple_ref/doc/uid/20001431-108256
33 | # Specifies whether the app runs as an agent app. If this key is set to YES, Launch Services runs the app as an agent app.
34 | # Agent apps do not appear in the Dock or in the Force Quit window
35 | INFOPLIST_KEY_LSUIElement: YES
36 | CODE_SIGN_IDENTITY: ${XCODEGEN_AEROSPACE_CODE_SIGN_IDENTITY}
37 | configs:
38 | Debug:
39 | PRODUCT_NAME: AeroSpace-Debug
40 | PRODUCT_BUNDLE_IDENTIFIER: bobko.aerospace.debug
41 | Release:
42 | PRODUCT_NAME: AeroSpace
43 | PRODUCT_BUNDLE_IDENTIFIER: bobko.aerospace
44 | entitlements:
45 | path: resources/AeroSpace.entitlements
46 | properties:
47 | # Accessibility API doesn't work in sandboxed app
48 | com.apple.security.app-sandbox: false
49 |
--------------------------------------------------------------------------------
/resources/AeroSpace.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/resources/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/resources/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "mac",
5 | "scale" : "1x",
6 | "size" : "16x16"
7 | },
8 | {
9 | "idiom" : "mac",
10 | "scale" : "2x",
11 | "size" : "16x16"
12 | },
13 | {
14 | "idiom" : "mac",
15 | "scale" : "1x",
16 | "size" : "32x32"
17 | },
18 | {
19 | "idiom" : "mac",
20 | "scale" : "2x",
21 | "size" : "32x32"
22 | },
23 | {
24 | "idiom" : "mac",
25 | "scale" : "1x",
26 | "size" : "128x128"
27 | },
28 | {
29 | "idiom" : "mac",
30 | "scale" : "2x",
31 | "size" : "128x128"
32 | },
33 | {
34 | "idiom" : "mac",
35 | "scale" : "1x",
36 | "size" : "256x256"
37 | },
38 | {
39 | "idiom" : "mac",
40 | "scale" : "2x",
41 | "size" : "256x256"
42 | },
43 | {
44 | "idiom" : "mac",
45 | "scale" : "1x",
46 | "size" : "512x512"
47 | },
48 | {
49 | "filename" : "icon.png",
50 | "idiom" : "mac",
51 | "scale" : "2x",
52 | "size" : "512x512"
53 | }
54 | ],
55 | "info" : {
56 | "author" : "xcode",
57 | "version" : 1
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/resources/Assets.xcassets/AppIcon.appiconset/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nikitabobko/AeroSpace/74bb715eb1a1818a98ecb3120e244b91d01f50b4/resources/Assets.xcassets/AppIcon.appiconset/icon.png
--------------------------------------------------------------------------------
/resources/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/run-cli.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | cd "$(dirname "$0")"
3 | source ./script/setup.sh
4 |
5 | ./build-debug.sh > /dev/null || ./build-debug.sh
6 | ./.debug/aerospace "$@"
7 |
--------------------------------------------------------------------------------
/run-debug.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | cd "$(dirname "$0")"
3 | source ./script/setup.sh
4 |
5 | ./build-debug.sh
6 | ./.debug/AeroSpaceApp "$@"
7 |
--------------------------------------------------------------------------------
/run-tests.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | cd "$(dirname "$0")"
3 | source ./script/setup.sh
4 |
5 | ./build-debug.sh
6 | swift test
7 |
8 | ./.debug/aerospace -h > /dev/null
9 | ./.debug/aerospace --help > /dev/null
10 | ./.debug/aerospace -v | grep -q "0.0.0-SNAPSHOT SNAPSHOT"
11 | ./.debug/aerospace --version | grep -q "0.0.0-SNAPSHOT SNAPSHOT"
12 |
13 | ./script/install-dep.sh --swiftformat
14 | ./.deps/swiftformat/swiftformat .
15 |
16 | ./script/install-dep.sh --swiftlint
17 | ./.deps/swiftlint/swiftlint lint --quiet
18 |
19 | ./generate.sh --all
20 | ./script/check-uncommitted-files.sh
21 |
--------------------------------------------------------------------------------
/script/check-uncommitted-files.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | cd "$(dirname "$0")/.."
3 | source ./script/setup.sh
4 |
5 | if ! test -z "$(git status --porcelain)"; then
6 | echo !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
7 | echo !!! Uncommitted files detected !!!
8 | echo !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
9 | git diff | sed 's/^/ /'
10 | echo !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
11 | echo !!! Uncommitted files detected !!!
12 | echo !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
13 | exit 1
14 | fi
15 |
--------------------------------------------------------------------------------
/script/clean-project.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | cd "$(dirname "$0")/.."
3 | source ./script/setup.sh
4 |
5 | ./script/clean-xcode.sh
6 | git clean -ffxd
7 |
--------------------------------------------------------------------------------
/script/clean-xcode.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | cd "$(dirname "$0")/.."
3 | source ./script/setup.sh
4 |
5 | ./script/check-uncommitted-files.sh
6 |
7 | git clean -ffxd
8 | rm -rf ~/Library/Developer/Xcode/DerivedData/AeroSpace-*
9 | rm -rf ./.xcode-build
10 |
11 | rm -rf AeroSpace.xcodeproj
12 | ./generate.sh
13 |
--------------------------------------------------------------------------------
/script/generate-cmd-help.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | cd "$(dirname "$0")/.."
3 | source ./script/setup.sh
4 |
5 | out_file='./Sources/Common/cmdHelpGenerated.swift'
6 |
7 | aerospace_prefix="aerospace"
8 | ____usage_prefix=" USAGE:"
9 | _______or_prefix=" OR:"
10 | ____strip_prefix=" "
11 |
12 | triple_quote='"""'
13 |
14 | cat << EOF > $out_file
15 | // FILE IS GENERATED FROM docs/aerospace-*.adoc files
16 | // TO REGENERATE THE FILE RUN generate.sh --all
17 |
18 | EOF
19 |
20 | for file in docs/aerospace-*.adoc; do
21 | subcommand=$(basename "$file" | sed 's/^aerospace-//' | sed 's/\.adoc$//' | sed 's/-/_/g')
22 | sed -n -E '/tag::synopsis/, /end::synopsis/ p' "$file" | \
23 | sed '1d' | \
24 | sed '$d' | \
25 | sed '/^$/ d' | \
26 | sed "1 s/^$aerospace_prefix/$____usage_prefix/" | \
27 | sed "2,$ s/^$aerospace_prefix/$_______or_prefix/" | \
28 | sed "s/^$____strip_prefix//" | \
29 | sed "1 s/^/let ${subcommand}_help_generated = $triple_quote\n/" | \
30 | sed "\$ s/$/\n${triple_quote}/" | \
31 | sed '2,$ s/^/ /' >> $out_file
32 | done
33 |
--------------------------------------------------------------------------------
/script/publish-release.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | cd "$(dirname "$0")/.."
3 | source ./script/setup.sh
4 |
5 | build_version=""
6 | cask_git_repo_path=""
7 | site_git_repo_path=""
8 | while test $# -gt 0; do
9 | case $1 in
10 | --build-version) build_version="$2"; shift 2;;
11 | --cask-git-repo-path) cask_git_repo_path="$2"; shift 2;;
12 | --site-git-repo-path) site_git_repo_path="$2"; shift 2;;
13 | *) echo "Unknown option $1"; exit 1;;
14 | esac
15 | done
16 |
17 | if test -z "$build_version"; then
18 | echo "--build-version flag is mandatory" > /dev/stderr
19 | exit 1
20 | fi
21 |
22 | if ! test -d "$cask_git_repo_path"; then
23 | echo "--cask-git-repo-path is a mandatory flag that must point to existing directory" > /dev/stderr
24 | exit 1
25 | fi
26 |
27 | if ! test -d "$site_git_repo_path"; then
28 | echo "--site-git-repo-path is a mandatory flag that must point to existing directory" > /dev/stderr
29 | exit 1
30 | fi
31 |
32 | ./run-tests.sh
33 | ./build-release.sh --build-version "$build_version"
34 |
35 | git tag -a "v$build_version" -m "v$build_version" && git push git@github.com:nikitabobko/AeroSpace.git "v$build_version"
36 | link="https://github.com/nikitabobko/AeroSpace/releases/new?tag=v$build_version"
37 | open "$link" || { echo "$link"; exit 1; }
38 | sleep 1
39 | open -R "./.release/AeroSpace-v$build_version.zip"
40 |
41 | echo "Please upload .zip to GitHub release and hit Enter"
42 | read -r
43 |
44 | ./script/build-brew-cask.sh \
45 | --cask-name aerospace \
46 | --zip-uri "https://github.com/nikitabobko/AeroSpace/releases/download/v$build_version/AeroSpace-v$build_version.zip" \
47 | --build-version "$build_version"
48 |
49 | eval "$cask_git_repo_path/pin.sh"
50 | cp -r .release/aerospace.rb "$cask_git_repo_path/Casks/aerospace.rb"
51 |
52 | rm -rf "${site_git_repo_path:?}/*" # https://www.shellcheck.net/wiki/SC2115
53 | cp -r .site/* "$site_git_repo_path"
54 |
--------------------------------------------------------------------------------
/script/reset-accessibility-permission-for-debug.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e # Exit if one of commands exit with non-zero exit code
3 | set -u # Treat unset variables and parameters other than the special parameters ‘@’ or ‘*’ as an error
4 | set -o pipefail # Any command failed in the pipe fails the whole pipe
5 | # set -x # Print shell commands as they are executed (or you can try -v which is less verbose)
6 |
7 | tccutil reset Accessibility bobko.AeroSpace.debug
8 |
--------------------------------------------------------------------------------