├── assets
└── banner.png
├── .gitignore
├── SwipeAeroSpace
├── Assets.xcassets
│ ├── Contents.json
│ ├── AppIcon.appiconset
│ │ ├── icon_16x16.png
│ │ ├── icon_32x32.png
│ │ ├── icon_128x128.png
│ │ ├── icon_16x16@2x.png
│ │ ├── icon_256x256.png
│ │ ├── icon_32x32@2x.png
│ │ ├── icon_512x512.png
│ │ ├── icon_128x128@2x.png
│ │ ├── icon_256x256@2x.png
│ │ ├── icon_512x512@2x.png
│ │ └── Contents.json
│ └── MenubarIcon.imageset
│ │ ├── Frame 2.png
│ │ └── Contents.json
├── Preview Content
│ └── Preview Assets.xcassets
│ │ └── Contents.json
├── SwipeAeroSpace.entitlements
├── BundleInfo.swift
├── AboutView.swift
├── PrivacyHelper.swift
├── SwipeAeroSpaceApp.swift
├── SettingsView.swift
├── LaunchAtLogin.swift
└── SwipeManager.swift
├── SwipeAeroSpace.xcodeproj
├── project.xcworkspace
│ └── contents.xcworkspacedata
├── xcuserdata
│ └── tricster.xcuserdatad
│ │ └── xcschemes
│ │ └── xcschememanagement.plist
└── project.pbxproj
├── LICENSE
├── .github
└── workflows
│ └── distribute.yml
├── Readme.md
└── scripts
├── build-brew-cask.py
└── build-brew-cask.sh
/assets/banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MediosZ/SwipeAeroSpace/HEAD/assets/banner.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Mac OS X
2 | .DS_Store
3 |
4 | # Xcode
5 | *.xcuserstate
6 | project.xcworkspace/
7 | xcuserdata/
8 | build*/
9 |
--------------------------------------------------------------------------------
/SwipeAeroSpace/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/SwipeAeroSpace/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/SwipeAeroSpace/Assets.xcassets/AppIcon.appiconset/icon_16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MediosZ/SwipeAeroSpace/HEAD/SwipeAeroSpace/Assets.xcassets/AppIcon.appiconset/icon_16x16.png
--------------------------------------------------------------------------------
/SwipeAeroSpace/Assets.xcassets/AppIcon.appiconset/icon_32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MediosZ/SwipeAeroSpace/HEAD/SwipeAeroSpace/Assets.xcassets/AppIcon.appiconset/icon_32x32.png
--------------------------------------------------------------------------------
/SwipeAeroSpace/Assets.xcassets/MenubarIcon.imageset/Frame 2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MediosZ/SwipeAeroSpace/HEAD/SwipeAeroSpace/Assets.xcassets/MenubarIcon.imageset/Frame 2.png
--------------------------------------------------------------------------------
/SwipeAeroSpace/Assets.xcassets/AppIcon.appiconset/icon_128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MediosZ/SwipeAeroSpace/HEAD/SwipeAeroSpace/Assets.xcassets/AppIcon.appiconset/icon_128x128.png
--------------------------------------------------------------------------------
/SwipeAeroSpace/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MediosZ/SwipeAeroSpace/HEAD/SwipeAeroSpace/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png
--------------------------------------------------------------------------------
/SwipeAeroSpace/Assets.xcassets/AppIcon.appiconset/icon_256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MediosZ/SwipeAeroSpace/HEAD/SwipeAeroSpace/Assets.xcassets/AppIcon.appiconset/icon_256x256.png
--------------------------------------------------------------------------------
/SwipeAeroSpace/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MediosZ/SwipeAeroSpace/HEAD/SwipeAeroSpace/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png
--------------------------------------------------------------------------------
/SwipeAeroSpace/Assets.xcassets/AppIcon.appiconset/icon_512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MediosZ/SwipeAeroSpace/HEAD/SwipeAeroSpace/Assets.xcassets/AppIcon.appiconset/icon_512x512.png
--------------------------------------------------------------------------------
/SwipeAeroSpace/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MediosZ/SwipeAeroSpace/HEAD/SwipeAeroSpace/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png
--------------------------------------------------------------------------------
/SwipeAeroSpace/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MediosZ/SwipeAeroSpace/HEAD/SwipeAeroSpace/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png
--------------------------------------------------------------------------------
/SwipeAeroSpace/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MediosZ/SwipeAeroSpace/HEAD/SwipeAeroSpace/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png
--------------------------------------------------------------------------------
/SwipeAeroSpace.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/SwipeAeroSpace/Assets.xcassets/MenubarIcon.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "Frame 2.png",
5 | "idiom" : "mac"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | },
12 | "properties" : {
13 | "template-rendering-intent" : "template"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/SwipeAeroSpace/SwipeAeroSpace.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.files.user-selected.read-only
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/SwipeAeroSpace.xcodeproj/xcuserdata/tricster.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | SwipeAeroSpace.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 0
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/SwipeAeroSpace/BundleInfo.swift:
--------------------------------------------------------------------------------
1 | import Cocoa
2 |
3 | class BundleInfo {
4 | private static func bundleInfo(_ key: String) -> String {
5 | return Bundle.main.infoDictionary?[key] as? String ?? ""
6 | }
7 |
8 | static func iconName() -> String {
9 | return bundleInfo("CFBundleIconName")
10 | }
11 |
12 | static func displayName() -> String {
13 | return bundleInfo("CFBundleDisplayName")
14 | }
15 |
16 | static func version() -> String {
17 | return bundleInfo("CFBundleShortVersionString")
18 | }
19 |
20 | static func build() -> String {
21 | return bundleInfo("CFBundleVersion")
22 | }
23 |
24 | static func copyright() -> String {
25 | return bundleInfo("NSHumanReadableCopyright")
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/SwipeAeroSpace/AboutView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct AboutView: View {
4 | var body: some View {
5 | VStack(alignment: .center, spacing: 16) {
6 | Image(nsImage: NSImage(named: BundleInfo.iconName()) ?? NSImage())
7 | .resizable()
8 | .frame(width: 64, height: 64)
9 | Text(BundleInfo.displayName())
10 | .font(.system(size: 16, weight: .bold))
11 | Text("Version \(BundleInfo.version()) (\(BundleInfo.build()))")
12 | .font(.system(size: 12))
13 | Text("Copyright \(BundleInfo.copyright())")
14 | .font(.system(size: 12))
15 | }
16 | .padding(.horizontal, 32)
17 | .padding(.vertical, 16)
18 | }
19 | }
20 |
21 | struct AboutView_Previews: PreviewProvider {
22 | static var previews: some View {
23 | AboutView()
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Tricster
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 |
--------------------------------------------------------------------------------
/SwipeAeroSpace/PrivacyHelper.swift:
--------------------------------------------------------------------------------
1 | import Cocoa
2 |
3 | class PrivacyHelper {
4 | static func isProcessTrustedWithPrompt() -> Bool {
5 | //TODO: Investigation required. Calling AXIsProcessTrustedWithOptions in sandboxed app doesn't prompt user (at least in Ventura 13.4.1) but creating CGEventTap does it.
6 | let isAccessibilityPermissionGranted = AXIsProcessTrustedWithOptions(
7 | [kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: true]
8 | as CFDictionary
9 | )
10 | if isAccessibilityPermissionGranted {
11 | return true
12 | } else {
13 | PrivacyHelper.promptForAccessibilityPermissionFromSandbox()
14 | return false
15 | }
16 | }
17 |
18 | private static func promptForAccessibilityPermissionFromSandbox() {
19 | _ = CGEvent.tapCreate(
20 | tap: .cghidEventTap,
21 | place: .headInsertEventTap,
22 | options: .defaultTap,
23 | eventsOfInterest: NSEvent.EventTypeMask.gesture.rawValue,
24 | callback: dummyEventHandler,
25 | userInfo: nil
26 | )
27 | }
28 | }
29 |
30 | private func dummyEventHandler(
31 | proxy: CGEventTapProxy,
32 | eventType: CGEventType,
33 | cgEvent: CGEvent,
34 | userInfo: UnsafeMutableRawPointer?
35 | ) -> Unmanaged? {
36 | debugPrint("Should never happen!")
37 | return Unmanaged.passUnretained(cgEvent)
38 | }
39 |
--------------------------------------------------------------------------------
/.github/workflows/distribute.yml:
--------------------------------------------------------------------------------
1 | name: Distribute SwipeAeroSpace
2 |
3 | on:
4 | push:
5 | tags:
6 | - '*'
7 |
8 | jobs:
9 | build-and-distribute:
10 | runs-on: macos-15
11 | timeout-minutes: 30
12 | steps:
13 | - name: Check out code
14 | uses: actions/checkout@v4
15 |
16 | - name: "Select Xcode (Default)"
17 | run: sudo xcode-select -s /Applications/Xcode_16.2.app
18 |
19 | - name: Build and export app
20 | run: |
21 | xcodebuild -scheme SwipeAeroSpace -configuration Release -derivedDataPath build -destination "generic/platform=macOS"
22 |
23 | - name: Make Release Dmg
24 | run: |
25 | hdiutil create temp.dmg -ov -volname "SwipeAeroSpace" -fs HFS+ -srcfolder "build/Build/Products/Release/SwipeAeroSpace.app"
26 | hdiutil convert temp.dmg -format UDZO -o "SwipeAeroSpace.dmg"
27 |
28 | - name: Make Release Zip
29 | run: |
30 | cp -r "build/Build/Products/Release/SwipeAeroSpace.app" "SwipeAeroSpace.app"
31 | zip -r "SwipeAeroSpace.zip" "SwipeAeroSpace.app"
32 |
33 | - name: Upload a Build Artifact
34 | id: upload_artifact
35 | uses: actions/upload-artifact@v4
36 | with:
37 | name: SwipeAeroSpace
38 | path: "SwipeAeroSpace.dmg"
39 |
40 | - name: Release to GitHub
41 | uses: softprops/action-gh-release@v2
42 | with:
43 | token: ${{ secrets.CUSTOM_GITHUB_TOKEN }}
44 | files: |
45 | SwipeAeroSpace.dmg
46 | SwipeAeroSpace.zip
--------------------------------------------------------------------------------
/SwipeAeroSpace/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "icon_16x16.png",
5 | "idiom" : "mac",
6 | "scale" : "1x",
7 | "size" : "16x16"
8 | },
9 | {
10 | "filename" : "icon_16x16@2x.png",
11 | "idiom" : "mac",
12 | "scale" : "2x",
13 | "size" : "16x16"
14 | },
15 | {
16 | "filename" : "icon_32x32.png",
17 | "idiom" : "mac",
18 | "scale" : "1x",
19 | "size" : "32x32"
20 | },
21 | {
22 | "filename" : "icon_32x32@2x.png",
23 | "idiom" : "mac",
24 | "scale" : "2x",
25 | "size" : "32x32"
26 | },
27 | {
28 | "filename" : "icon_128x128.png",
29 | "idiom" : "mac",
30 | "scale" : "1x",
31 | "size" : "128x128"
32 | },
33 | {
34 | "filename" : "icon_128x128@2x.png",
35 | "idiom" : "mac",
36 | "scale" : "2x",
37 | "size" : "128x128"
38 | },
39 | {
40 | "filename" : "icon_256x256.png",
41 | "idiom" : "mac",
42 | "scale" : "1x",
43 | "size" : "256x256"
44 | },
45 | {
46 | "filename" : "icon_256x256@2x.png",
47 | "idiom" : "mac",
48 | "scale" : "2x",
49 | "size" : "256x256"
50 | },
51 | {
52 | "filename" : "icon_512x512.png",
53 | "idiom" : "mac",
54 | "scale" : "1x",
55 | "size" : "512x512"
56 | },
57 | {
58 | "filename" : "icon_512x512@2x.png",
59 | "idiom" : "mac",
60 | "scale" : "2x",
61 | "size" : "512x512"
62 | }
63 | ],
64 | "info" : {
65 | "author" : "xcode",
66 | "version" : 1
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/Readme.md:
--------------------------------------------------------------------------------
1 |
2 | 
3 |
4 | # About
5 |
6 | Swipe with three fingers to change AeroSpace workspaces. This can be a single purpose alternative to Better Touch Tool.
7 |
8 | # Installation
9 |
10 | You can either download the pre-built binary (built with github actions) or build it from source.
11 |
12 | ## Homebrew
13 |
14 | The easiest way to install is to use Homebrew:
15 |
16 | ```bash
17 | brew install --cask mediosz/tap/swipeaerospace
18 | ```
19 |
20 | ## Download pre-built binary
21 |
22 | First, Download the latest `SwipeAeroSpace.dmg` from [Releases](https://github.com/MediosZ/SwipeAeroSpace/releases) page.
23 |
24 | But it can’t be opened because Apple cannot check it for malicious software.
25 |
26 | There are two options:
27 |
28 | - You may right-click the app and click Open and click Open again, or you could goto `System Settings > Privacy & Security > Security` and select `Open Anyway`.
29 | - You could use `xattr -d com.apple.quarantine /path/to/SwipeAeroSpace.app` to remove the constraint.
30 |
31 | The app needs access to global trackpad events. Allow `SwipeAeroSpace` to control your computer in `System Settings > Privacy & Security > Accessibility`.
32 |
33 | ## Build from source
34 |
35 | First install Xcode, then there are two options:
36 |
37 | - Open `SwipeAeroSpace.xcodeproj` to build the project and export the app.
38 | - Or you can use `xcodebuild` directly to build and export the app.
39 |
40 |
41 | # Usage
42 |
43 | After properly installation, you can use the 3-finger swipe to switch between AeroSpace workspaces.
44 |
45 | # License
46 |
47 | This project is licensed under the MIT License - see the LICENSE file for details.
48 |
49 | # Acknowledgement
50 |
51 | Big thanks to [Touch-Tab](https://github.com/ris58h/Touch-Tab).
52 |
53 |
54 |
--------------------------------------------------------------------------------
/scripts/build-brew-cask.py:
--------------------------------------------------------------------------------
1 |
2 | import os
3 | import sys
4 | import subprocess
5 | import argparse
6 |
7 | def main():
8 | # Parse command line arguments
9 | parser = argparse.ArgumentParser(description='Build brew cask for SwipeAeroSpace')
10 | parser.add_argument('--build-version', type=str, required=True, help='Build version')
11 | args = parser.parse_args()
12 |
13 |
14 | # Default values
15 | cask_name = 'swipeaerospace'
16 | cask_version = args.build_version
17 | app_bundle_dir_name = 'SwipeAeroSpace.app'
18 | zip_uri = f"https://github.com/MediosZ/SwipeAeroSpace/releases/download/{cask_version}/SwipeAeroSpace.zip"
19 |
20 | # Process ZIP file
21 | zip_file = ''
22 | if os.path.isfile(zip_uri):
23 | zip_file = zip_uri
24 | zip_uri = f"file://{os.path.realpath(zip_file)}"
25 | elif zip_uri.startswith('http'):
26 | zip_file = '/tmp/AeroSpace-tmp.zip'
27 | if os.path.exists(zip_file):
28 | os.remove(zip_file)
29 | subprocess.run(['curl', '-L', zip_uri, '-o', zip_file], check=True)
30 | else:
31 | sys.stderr.write(f"{zip_uri} doesn't exist\n")
32 | sys.exit(1)
33 |
34 | # Calculate SHA256
35 | sha = subprocess.check_output(['shasum', '-a', '256', zip_file]).decode('utf-8').split()[0]
36 |
37 | # Generate the Ruby file
38 | cask_template = f"""cask "{cask_name}" do # THE FILE IS GENERATED BY build-brew-cask.py
39 | version "{cask_version}"
40 | sha256 "{sha}"
41 |
42 | url "https://github.com/MediosZ/SwipeAeroSpace/releases/download/#{{version}}/SwipeAeroSpace.zip"
43 | name "SwipeAeroSpace"
44 | desc "SwipeAeroSpace is a tool to switch AeroSpace worksapces by swiping."
45 | homepage "https://github.com/MediosZ/SwipeAeroSpace"
46 |
47 | depends_on macos: ">= :ventura" # macOS 13
48 |
49 | postflight do
50 | system "xattr -d com.apple.quarantine #{{appdir}}/{app_bundle_dir_name}"
51 | end
52 |
53 | app "{app_bundle_dir_name}"
54 | end
55 | """
56 |
57 | with open(f"{cask_name}.rb", "w") as f:
58 | f.write(cask_template)
59 |
60 |
61 | if __name__ == "__main__":
62 | main()
--------------------------------------------------------------------------------
/scripts/build-brew-cask.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # cd "$(dirname "$0")/.."
3 |
4 | zip_uri='' # mandatory
5 | cask_name='swipeaerospace'
6 | build_version="0.0.0-SNAPSHOT"
7 | app_bundle_dir_name='SwipeAeroSpace.app'
8 | while test $# -gt 0; do
9 | case $1 in
10 | --build-version) build_version="$2"; shift 2;;
11 | --zip-uri) zip_uri="$2"; shift 2;;
12 | --app-bundle-dir-name) app_bundle_dir_name="$2"; shift 2;;
13 | --cask-name) cask_name="$2"; shift 2;;
14 | *) echo "Unknown arg $1"; exit 1;;
15 | esac
16 | done
17 |
18 | if test -z "$zip_uri"; then echo "--zip-uri is mandatory" > /dev/stderr; exit 1; fi
19 | if test -z "$cask_name"; then echo "--cask-name is mandatory" > /dev/stderr; exit 1; fi
20 |
21 | case "$cask_name" in
22 | swipeaerospace) ;;
23 | *) echo "Unknown cask name: $cask_name. Allowed cask names: swipeaerospace" > /dev/stderr; exit 1;;
24 | esac
25 |
26 | zip_file=''
27 | if test -f "$zip_uri"; then
28 | zip_file=$zip_uri
29 | zip_uri="file://$(realpath "$zip_file")"
30 | elif grep -q '^http' <<< "$zip_uri"; then
31 | zip_file=/tmp/AeroSpace-tmp.zip
32 | rm -rf $zip_file
33 | curl -L "$zip_uri" -o $zip_file
34 | else
35 | echo "$zip_uri doesn't exist" > /dev/stderr; exit 1
36 | fi
37 | sha=$(shasum -a 256 "$zip_file" | awk '{print $1}')
38 |
39 | cask_version=':latest' # Prevent 'Not upgrading aerospace, the latest version is already installed'
40 | zip_root_dir="SwipeAeroSpace-v$build_version"
41 | if ! grep -q SNAPSHOT <<< "$build_version"; then
42 | cask_version="'$build_version'"
43 | zip_root_dir=$(sed "s/$build_version/#{version}/g" <<< "$zip_root_dir")
44 | zip_uri=$(sed "s/$build_version/#{version}/g" <<< "$zip_uri")
45 | fi
46 |
47 | echo "cask_name=$cask_name"
48 | echo "cask_version=$cask_version"
49 | echo "zip_uri=$zip_uri"
50 | echo "zip_root_dir=$zip_root_dir"
51 | echo "app_bundle_dir_name=$app_bundle_dir_name"
52 |
53 |
54 | cat > "$cask_name.rb" <= :ventura" # macOS 13
65 |
66 | postflight do
67 | system "xattr -d com.apple.quarantine #{appdir}/$app_bundle_dir_name"
68 | end
69 |
70 | app "$app_bundle_dir_name"
71 | end
72 | EOF
73 |
--------------------------------------------------------------------------------
/SwipeAeroSpace/SwipeAeroSpaceApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SwipeAeroSpaceApp.swift
3 | // SwipeAeroSpace
4 | //
5 | // Created by Tricster on 1/25/25.
6 | //
7 |
8 | import SwiftUI
9 |
10 | @available(macOS 14.0, *)
11 | struct SettingsButton: View {
12 | @Environment(\.openSettings) private var openSettings
13 |
14 | var body: some View {
15 | Button("Settings") {
16 | openSettings()
17 | }
18 | }
19 | }
20 |
21 | func checkAccessibilityPermissions() {
22 | let options = [
23 | kAXTrustedCheckOptionPrompt.takeRetainedValue() as String: true
24 | ]
25 | if !AXIsProcessTrustedWithOptions(options as CFDictionary) {
26 | _ = try? Process.run(
27 | URL(filePath: "/usr/bin/tccutil"),
28 | arguments: [
29 | "reset", "Accessibility", "club.mediosz.SwipeAeroSpace",
30 | ]
31 | )
32 | NSApplication.shared.terminate(nil)
33 | }
34 | }
35 |
36 | @main
37 | struct SwipeAeroSpaceApp: App {
38 | @AppStorage("menuBarExtraIsInserted") var menuBarExtraIsInserted = true
39 | @Environment(\.openWindow) private var openWindow
40 | @State var swipeManager = SwipeManager()
41 |
42 | init() {
43 | checkAccessibilityPermissions()
44 | swipeManager.start()
45 | }
46 |
47 | var body: some Scene {
48 | MenuBarExtra(
49 | "Screenshots",
50 | image: "MenubarIcon",
51 | isInserted: $menuBarExtraIsInserted
52 | ) {
53 | Button("Next Workspace") {
54 | swipeManager.nextWorkspace()
55 | }
56 | Button("Prev Workspace") {
57 | swipeManager.prevWorkspace()
58 | }
59 |
60 | if #available(macOS 14.0, *) {
61 | SettingsButton()
62 | } else {
63 | Button(
64 | action: {
65 | NSApp.sendAction(
66 | Selector(("showSettingsWindow:")),
67 | to: nil,
68 | from: nil
69 | )
70 | },
71 | label: {
72 | Text("Settings")
73 | }
74 | )
75 | }
76 |
77 | Button("About") {
78 | openWindow(id: "about")
79 | }
80 | Divider()
81 |
82 | Button("Quit") {
83 | swipeManager.stop()
84 | NSApplication.shared.terminate(nil)
85 | }.keyboardShortcut("q")
86 | }
87 |
88 | Settings {
89 | SettingsView(
90 | swipeManager: swipeManager,
91 | socketInfo: swipeManager.socketInfo
92 | )
93 | }.windowResizability(.contentSize)
94 |
95 | WindowGroup(id: "about") {
96 | AboutView()
97 | }.windowResizability(.contentSize)
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/SwipeAeroSpace/SettingsView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct SettingsView: View {
4 | @AppStorage("threshold") private static var swipeThreshold: Double = 0.3
5 | @AppStorage("wrap") private var wrapWorkspace: Bool = false
6 | @AppStorage("natrual") private var naturalSwipe: Bool = true
7 | @AppStorage("skip-empty") private var skipEmpty: Bool = false
8 | @AppStorage("fingers") private var fingers: String = "Three"
9 |
10 | @State private var numberFormatter: NumberFormatter = {
11 | var nf = NumberFormatter()
12 | nf.numberStyle = .decimal
13 | return nf
14 | }()
15 |
16 | let numbers = ["Three", "Four"]
17 |
18 | var swipeManager: SwipeManager
19 | @ObservedObject var socketInfo: SocketInfo
20 |
21 | var body: some View {
22 | VStack(alignment: .leading, spacing: 16) {
23 | VStack(alignment: .leading) {
24 | HStack {
25 | Text("Socket Status: ")
26 | Image(systemName: "circle.fill").foregroundStyle(
27 | socketInfo.socketConnected ? .green : .red
28 | )
29 | }
30 | if !socketInfo.socketConnected {
31 | Button("Try to connect socket") {
32 | swipeManager.connectSocket(reconnect: true)
33 | }
34 | }
35 | }
36 | Form {
37 | TextField(
38 | "Swipe Threshold",
39 | value: SettingsView.$swipeThreshold,
40 | formatter: numberFormatter,
41 | prompt: Text("0.3")
42 | ).textFieldStyle(RoundedBorderTextFieldStyle()).frame(
43 | maxWidth: 200
44 | )
45 | }
46 |
47 | Picker("Number of Fingers:", selection: $fingers) {
48 | ForEach(numbers, id: \.self) {
49 | Text($0)
50 | }
51 | }
52 | .pickerStyle(.segmented)
53 | .frame(maxWidth: 400)
54 | .padding(.vertical, 4)
55 |
56 | VStack(alignment: .leading) {
57 | Toggle("Wrap Workspace", isOn: $wrapWorkspace)
58 | Text("Enable to jump between first and last workspaces")
59 | .foregroundStyle(.secondary)
60 | }.padding(.vertical, 4)
61 |
62 | VStack(alignment: .leading) {
63 | Toggle("Natural Swipe", isOn: $naturalSwipe)
64 | Text("Disable to use reversed swipe ").foregroundStyle(
65 | .secondary
66 | )
67 | }.padding(.vertical, 4)
68 |
69 | VStack(alignment: .leading) {
70 | Toggle("Skip Empty Workspace", isOn: $skipEmpty)
71 | Text("Enable to skip empty workspaces").foregroundStyle(
72 | .secondary
73 | )
74 | }.padding(.vertical, 4)
75 |
76 | LaunchAtLogin.Toggle {
77 | Text("Launch At Login")
78 | }
79 | }
80 | .padding(.horizontal, 32)
81 | .padding(.vertical, 24)
82 |
83 | }
84 | }
85 |
86 | struct SettingsView_Previews: PreviewProvider {
87 | static var swipeManager = SwipeManager()
88 | static var previews: some View {
89 | SettingsView(
90 | swipeManager: swipeManager,
91 | socketInfo: swipeManager.socketInfo
92 | )
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/SwipeAeroSpace/LaunchAtLogin.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LaunchAtLogin.swift
3 | // SwipeAeroSpace
4 | //
5 | // Created by Tricster on 1/25/25.
6 | //
7 |
8 | import ServiceManagement
9 | import SwiftUI
10 | import os.log
11 |
12 | public enum LaunchAtLogin {
13 | private static let logger = Logger(
14 | subsystem: "com.sindresorhus.LaunchAtLogin",
15 | category: "main"
16 | )
17 | fileprivate static let observable = Observable()
18 |
19 | /**
20 | Toggle “launch at login” for your app or check whether it's enabled.
21 | */
22 | public static var isEnabled: Bool {
23 | get { SMAppService.mainApp.status == .enabled }
24 | set {
25 | observable.objectWillChange.send()
26 |
27 | do {
28 | if newValue {
29 | if SMAppService.mainApp.status == .enabled {
30 | try? SMAppService.mainApp.unregister()
31 | }
32 |
33 | try SMAppService.mainApp.register()
34 | } else {
35 | try SMAppService.mainApp.unregister()
36 | }
37 | } catch {
38 | logger.error(
39 | "Failed to \(newValue ? "enable" : "disable") launch at login: \(error.localizedDescription)"
40 | )
41 | }
42 | }
43 | }
44 |
45 | /**
46 | Whether the app was launched at login.
47 |
48 | - Important: This property must only be checked in `NSApplicationDelegate#applicationDidFinishLaunching`.
49 | */
50 | public static var wasLaunchedAtLogin: Bool {
51 | let event = NSAppleEventManager.shared().currentAppleEvent
52 | return event?.eventID == kAEOpenApplication
53 | && event?.paramDescriptor(forKeyword: keyAEPropData)?.enumCodeValue
54 | == keyAELaunchedAsLogInItem
55 | }
56 | }
57 |
58 | extension LaunchAtLogin {
59 | final class Observable: ObservableObject {
60 | var isEnabled: Bool {
61 | get { LaunchAtLogin.isEnabled }
62 | set {
63 | LaunchAtLogin.isEnabled = newValue
64 | }
65 | }
66 | }
67 | }
68 |
69 | extension LaunchAtLogin {
70 | /**
71 | This package comes with a `LaunchAtLogin.Toggle` view which is like the built-in `Toggle` but with a predefined binding and label. Clicking the view toggles “launch at login” for your app.
72 |
73 | ```
74 | struct ContentView: View {
75 | var body: some View {
76 | LaunchAtLogin.Toggle()
77 | }
78 | }
79 | ```
80 |
81 | The default label is `"Launch at login"`, but it can be overridden for localization and other needs:
82 |
83 | ```
84 | struct ContentView: View {
85 | var body: some View {
86 | LaunchAtLogin.Toggle {
87 | Text("Launch at login")
88 | }
89 | }
90 | }
91 | ```
92 | */
93 | public struct Toggle: View {
94 | @ObservedObject private var launchAtLogin = LaunchAtLogin.observable
95 | private let label: Label
96 |
97 | /**
98 | Creates a toggle that displays a custom label.
99 |
100 | - Parameters:
101 | - label: A view that describes the purpose of the toggle.
102 | */
103 | public init(@ViewBuilder label: () -> Label) {
104 | self.label = label()
105 | }
106 |
107 | public var body: some View {
108 | SwiftUI.Toggle(isOn: $launchAtLogin.isEnabled) { label }
109 | }
110 | }
111 | }
112 |
113 | extension LaunchAtLogin.Toggle {
114 | /**
115 | Creates a toggle that generates its label from a localized string key.
116 |
117 | This initializer creates a ``Text`` view on your behalf with the provided `titleKey`.
118 |
119 | - Parameters:
120 | - titleKey: The key for the toggle's localized title, that describes the purpose of the toggle.
121 | */
122 | public init(_ titleKey: LocalizedStringKey) {
123 | label = Text(titleKey)
124 | }
125 |
126 | /**
127 | Creates a toggle that generates its label from a string.
128 |
129 | This initializer creates a `Text` view on your behalf with the provided `title`.
130 |
131 | - Parameters:
132 | - title: A string that describes the purpose of the toggle.
133 | */
134 | public init(_ title: some StringProtocol) {
135 | label = Text(title)
136 | }
137 |
138 | /**
139 | Creates a toggle with the default title of `Launch at login`.
140 | */
141 | public init() {
142 | self.init("Launch at login")
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/SwipeAeroSpace/SwipeManager.swift:
--------------------------------------------------------------------------------
1 | import Cocoa
2 | import Foundation
3 | import Socket
4 | import SwiftUI
5 | import os
6 |
7 | enum Direction {
8 | case next
9 | case prev
10 |
11 | var value: String {
12 | switch self {
13 | case .next:
14 | "next"
15 | case .prev:
16 | "prev"
17 | }
18 | }
19 | }
20 |
21 | enum GestureState {
22 | case began
23 | case changed
24 | case ended
25 | case cancelled
26 | }
27 |
28 | enum SwipeError: Error {
29 | case SocketError(String)
30 | case CommandFail(String)
31 | case Unknown(String)
32 | }
33 |
34 | public struct ClientRequest: Codable, Sendable {
35 | public let command: String
36 | public let args: [String]
37 | public let stdin: String
38 | public let windowId: UInt32?
39 | public let workspace: String?
40 |
41 | public init(
42 | args: [String],
43 | stdin: String,
44 | windowId: UInt32?,
45 | workspace: String?
46 | ) {
47 | self.command = ""
48 | self.args = args
49 | self.stdin = stdin
50 | self.windowId = windowId
51 | self.workspace = workspace
52 | }
53 | }
54 |
55 | public struct ServerAnswer: Codable, Sendable {
56 | public let exitCode: Int32
57 | public let stdout: String
58 | public let stderr: String
59 | public let serverVersionAndHash: String
60 |
61 | public init(
62 | exitCode: Int32,
63 | stdout: String = "",
64 | stderr: String = "",
65 | serverVersionAndHash: String
66 | ) {
67 | self.exitCode = exitCode
68 | self.stdout = stdout
69 | self.stderr = stderr
70 | self.serverVersionAndHash = serverVersionAndHash
71 | }
72 | }
73 |
74 | class SocketInfo: ObservableObject {
75 | @Published var socketConnected: Bool = false
76 | }
77 |
78 | extension Result {
79 | public var isSuccess: Bool {
80 | switch self {
81 | case .success: true
82 | case .failure: false
83 | }
84 | }
85 | }
86 |
87 | class SwipeManager {
88 | // user settings
89 | @AppStorage("threshold") private var swipeThreshold: Double = 0.3
90 | @AppStorage("wrap") private var wrapWorkspace: Bool = false
91 | @AppStorage("natrual") private var naturalSwipe: Bool = true
92 | @AppStorage("skip-empty") private var skipEmpty: Bool = false
93 | @AppStorage("fingers") private var fingers: String = "Three"
94 |
95 | var socketInfo = SocketInfo()
96 |
97 | private var eventTap: CFMachPort? = nil
98 | private var accDisX: Float = 0
99 | private var prevTouchPositions: [String: NSPoint] = [:]
100 | private var state: GestureState = .ended
101 | private var socket: Socket? = nil
102 |
103 | private var logger: Logger = Logger(
104 | subsystem: Bundle.main.bundleIdentifier!,
105 | category: "Info"
106 | )
107 |
108 | private func runCommand(args: [String], stdin: String, retry: Bool = false)
109 | -> Result
110 | {
111 | guard let socket = socket else {
112 | return .failure(.SocketError("No socket created"))
113 | }
114 | do {
115 | let request = try JSONEncoder().encode(
116 | ClientRequest(args: args, stdin: stdin, windowId: nil, workspace: nil)
117 | )
118 | try socket.write(from: request)
119 | let _ = try Socket.wait(
120 | for: [socket],
121 | timeout: 0,
122 | waitForever: true
123 | )
124 | var answer = Data()
125 | try socket.read(into: &answer)
126 | let result = try JSONDecoder().decode(
127 | ServerAnswer.self,
128 | from: answer
129 | )
130 | if result.exitCode != 0 {
131 | return .failure(.CommandFail(result.stderr))
132 | }
133 | return .success(result.stdout)
134 |
135 | } catch let error {
136 | guard let socketError = error as? Socket.Error else {
137 | return .failure(.Unknown(error.localizedDescription))
138 | }
139 | // if we encouter the socket error
140 | // try reconnect the socket and rerun the command only once.
141 | if retry {
142 | return .failure(.SocketError(socketError.localizedDescription))
143 | }
144 | logger.info("Trying reconnect socket...")
145 | connectSocket(reconnect: true)
146 | return runCommand(args: args, stdin: stdin, retry: true)
147 | }
148 | }
149 |
150 | private func getNonEmptyWorkspaces() -> Result {
151 | let args = [
152 | "list-workspaces", "--monitor", "focused", "--empty", "no",
153 | ]
154 | return runCommand(args: args, stdin: "")
155 | }
156 |
157 | @discardableResult
158 | private func switchWorkspace(direction: Direction) -> Result<
159 | String, SwipeError
160 | > {
161 |
162 | var res = runCommand(
163 | args: ["list-workspaces", "--monitor", "mouse", "--visible"],
164 | stdin: ""
165 | )
166 | guard let mouse_on = try? res.get() else {
167 | return res
168 | }
169 | res = runCommand(args: ["workspace", mouse_on], stdin: "")
170 | guard (try? res.get()) != nil else {
171 | return res
172 | }
173 |
174 | var args = ["workspace", direction.value]
175 | if wrapWorkspace {
176 | args.append("--wrap-around")
177 | }
178 | var stdin = ""
179 | if skipEmpty {
180 | res = getNonEmptyWorkspaces()
181 | guard let ws = try? res.get() else {
182 | return res
183 | }
184 | stdin = ws
185 | if stdin != "" {
186 | // explicitly insert '--stdin'
187 | args.append("--stdin")
188 | }
189 | }
190 | return runCommand(args: args, stdin: stdin)
191 | }
192 |
193 | func nextWorkspace() {
194 | switch switchWorkspace(direction: .next) {
195 | case .success: return
196 | case .failure(let err): logger.error("\(err.localizedDescription)")
197 | }
198 | }
199 |
200 | func prevWorkspace() {
201 | switch switchWorkspace(direction: .prev) {
202 | case .success: return
203 | case .failure(let err): logger.error("\(err.localizedDescription)")
204 | }
205 |
206 | }
207 |
208 | func connectSocket(reconnect: Bool = false) {
209 | if socket != nil && !reconnect {
210 | logger.warning("socket is connected")
211 | return
212 | }
213 |
214 | let socket_path = "/tmp/bobko.aerospace-\(NSUserName()).sock"
215 | do {
216 | socket = try Socket.create(
217 | family: .unix,
218 | type: .stream,
219 | proto: .unix
220 | )
221 | try socket?.connect(to: socket_path)
222 | socketInfo.socketConnected = true
223 | logger.info("connect to socket \(socket_path)")
224 | } catch let error {
225 | logger.error("Unexpected error: \(error.localizedDescription)")
226 | }
227 | }
228 |
229 | func start() {
230 | if eventTap != nil {
231 | logger.warning("SwipeManager is already started")
232 | return
233 | }
234 | logger.info("SwipeManager start")
235 | eventTap = CGEvent.tapCreate(
236 | tap: .cghidEventTap,
237 | place: .headInsertEventTap,
238 | options: .defaultTap,
239 | eventsOfInterest: NSEvent.EventTypeMask.gesture.rawValue,
240 | callback: { proxy, type, cgEvent, me in
241 | let wrapper = Unmanaged.fromOpaque(me!)
242 | .takeUnretainedValue()
243 | return wrapper.eventHandler(
244 | proxy: proxy,
245 | eventType: type,
246 | cgEvent: cgEvent
247 | )
248 | },
249 | userInfo: Unmanaged.passUnretained(self).toOpaque()
250 | )
251 | if eventTap == nil {
252 | logger.error("SwipeManager couldn't create event tap")
253 | return
254 | }
255 |
256 | let runLoopSource = CFMachPortCreateRunLoopSource(nil, eventTap, 0)
257 | CFRunLoopAddSource(
258 | CFRunLoopGetCurrent(),
259 | runLoopSource,
260 | CFRunLoopMode.commonModes
261 | )
262 | CGEvent.tapEnable(tap: eventTap!, enable: true)
263 |
264 | connectSocket()
265 | }
266 |
267 | func stop() {
268 | logger.info("stop the app")
269 | socket?.close()
270 | }
271 |
272 | private func eventHandler(
273 | proxy: CGEventTapProxy,
274 | eventType: CGEventType,
275 | cgEvent: CGEvent
276 | ) -> Unmanaged? {
277 | if eventType.rawValue == NSEvent.EventType.gesture.rawValue,
278 | let nsEvent = NSEvent(cgEvent: cgEvent)
279 | {
280 | touchEventHandler(nsEvent)
281 | } else if eventType == .tapDisabledByUserInput
282 | || eventType == .tapDisabledByTimeout
283 | {
284 | logger.info("SwipeManager tap disabled \(eventType.rawValue)")
285 | CGEvent.tapEnable(tap: eventTap!, enable: true)
286 | }
287 | return Unmanaged.passUnretained(cgEvent)
288 | }
289 |
290 | private func touchEventHandler(_ nsEvent: NSEvent) {
291 | let touches = nsEvent.allTouches()
292 |
293 | // Sometimes there are empty touch events that we have to skip. There are no empty touch events if Mission Control or App Expose use 3-finger swipes though.
294 | if touches.isEmpty {
295 | return
296 | }
297 | let touchesCount =
298 | touches.allSatisfy({ $0.phase == .ended }) ? 0 : touches.count
299 | if touchesCount == 0 {
300 | stopGesture()
301 | } else {
302 | processTouches(touches: touches, count: touchesCount)
303 | }
304 | }
305 |
306 | private func stopGesture() {
307 | if state == .began {
308 | state = .ended
309 | handleGesture()
310 | clearEventState()
311 | }
312 | }
313 |
314 | private func processTouches(touches: Set, count: Int) {
315 | let finger_count = fingers == "Three" ? 3 : 4
316 | if state != .began && count == finger_count {
317 | state = .began
318 | }
319 | if state == .began {
320 | accDisX += horizontalSwipeDistance(touches: touches)
321 | }
322 | }
323 |
324 | private func clearEventState() {
325 | accDisX = 0
326 | prevTouchPositions.removeAll()
327 | }
328 |
329 | private func handleGesture() {
330 | // filter
331 | if abs(accDisX) < Float(swipeThreshold) {
332 | return
333 | }
334 | let direction: Direction =
335 | if naturalSwipe {
336 | accDisX < 0 ? .next : .prev
337 | } else {
338 | accDisX < 0 ? .prev : .next
339 | }
340 | switch switchWorkspace(direction: direction) {
341 | case .success: return
342 | case .failure(let err): logger.error("\(err.localizedDescription)")
343 | }
344 | }
345 |
346 | private func horizontalSwipeDistance(touches: Set) -> Float {
347 | var allRight = true
348 | var allLeft = true
349 | var sumDisX = Float(0)
350 | var sumDisY = Float(0)
351 | for touch in touches {
352 | let (disX, disY) = touchDistance(touch)
353 | allRight = allRight && disX >= 0
354 | allLeft = allLeft && disX <= 0
355 | sumDisX += disX
356 | sumDisY += disY
357 |
358 | if touch.phase == .ended {
359 | prevTouchPositions.removeValue(forKey: "\(touch.identity)")
360 | } else {
361 | prevTouchPositions["\(touch.identity)"] =
362 | touch.normalizedPosition
363 | }
364 | }
365 | // All fingers should move in the same direction.
366 | if !allRight && !allLeft {
367 | return 0
368 | }
369 |
370 | // Only horizontal swipes are interesting.
371 | if abs(sumDisX) <= abs(sumDisY) {
372 | return 0
373 | }
374 |
375 | return sumDisX
376 | }
377 |
378 | private func touchDistance(_ touch: NSTouch) -> (Float, Float) {
379 | guard let prevPosition = prevTouchPositions["\(touch.identity)"] else {
380 | return (0, 0)
381 | }
382 | let position = touch.normalizedPosition
383 | return (
384 | Float(position.x - prevPosition.x),
385 | Float(position.y - prevPosition.y)
386 | )
387 | }
388 | }
389 |
--------------------------------------------------------------------------------
/SwipeAeroSpace.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 77;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 9E4137D22D97E63100634ACB /* BlueSocketTestCommonLibrary in Frameworks */ = {isa = PBXBuildFile; productRef = 9E4137D12D97E63100634ACB /* BlueSocketTestCommonLibrary */; };
11 | 9E4137D42D97E63100634ACB /* Socket in Frameworks */ = {isa = PBXBuildFile; productRef = 9E4137D32D97E63100634ACB /* Socket */; };
12 | /* End PBXBuildFile section */
13 |
14 | /* Begin PBXFileReference section */
15 | 9E04C59A2D44E5FB00320146 /* SwipeAeroSpace.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwipeAeroSpace.app; sourceTree = BUILT_PRODUCTS_DIR; };
16 | /* End PBXFileReference section */
17 |
18 | /* Begin PBXFileSystemSynchronizedRootGroup section */
19 | 9E04C59C2D44E5FB00320146 /* SwipeAeroSpace */ = {
20 | isa = PBXFileSystemSynchronizedRootGroup;
21 | path = SwipeAeroSpace;
22 | sourceTree = "";
23 | };
24 | /* End PBXFileSystemSynchronizedRootGroup section */
25 |
26 | /* Begin PBXFrameworksBuildPhase section */
27 | 9E04C5972D44E5FB00320146 /* Frameworks */ = {
28 | isa = PBXFrameworksBuildPhase;
29 | buildActionMask = 2147483647;
30 | files = (
31 | 9E4137D22D97E63100634ACB /* BlueSocketTestCommonLibrary in Frameworks */,
32 | 9E4137D42D97E63100634ACB /* Socket in Frameworks */,
33 | );
34 | runOnlyForDeploymentPostprocessing = 0;
35 | };
36 | /* End PBXFrameworksBuildPhase section */
37 |
38 | /* Begin PBXGroup section */
39 | 9E04C5912D44E5FB00320146 = {
40 | isa = PBXGroup;
41 | children = (
42 | 9E04C59C2D44E5FB00320146 /* SwipeAeroSpace */,
43 | 9E04C59B2D44E5FB00320146 /* Products */,
44 | );
45 | sourceTree = "";
46 | };
47 | 9E04C59B2D44E5FB00320146 /* Products */ = {
48 | isa = PBXGroup;
49 | children = (
50 | 9E04C59A2D44E5FB00320146 /* SwipeAeroSpace.app */,
51 | );
52 | name = Products;
53 | sourceTree = "";
54 | };
55 | /* End PBXGroup section */
56 |
57 | /* Begin PBXNativeTarget section */
58 | 9E04C5992D44E5FB00320146 /* SwipeAeroSpace */ = {
59 | isa = PBXNativeTarget;
60 | buildConfigurationList = 9E04C5A92D44E5FC00320146 /* Build configuration list for PBXNativeTarget "SwipeAeroSpace" */;
61 | buildPhases = (
62 | 9E04C5962D44E5FB00320146 /* Sources */,
63 | 9E04C5972D44E5FB00320146 /* Frameworks */,
64 | 9E04C5982D44E5FB00320146 /* Resources */,
65 | );
66 | buildRules = (
67 | );
68 | dependencies = (
69 | );
70 | fileSystemSynchronizedGroups = (
71 | 9E04C59C2D44E5FB00320146 /* SwipeAeroSpace */,
72 | );
73 | name = SwipeAeroSpace;
74 | packageProductDependencies = (
75 | 9E4137D12D97E63100634ACB /* BlueSocketTestCommonLibrary */,
76 | 9E4137D32D97E63100634ACB /* Socket */,
77 | );
78 | productName = SwipeAeroSpace;
79 | productReference = 9E04C59A2D44E5FB00320146 /* SwipeAeroSpace.app */;
80 | productType = "com.apple.product-type.application";
81 | };
82 | /* End PBXNativeTarget section */
83 |
84 | /* Begin PBXProject section */
85 | 9E04C5922D44E5FB00320146 /* Project object */ = {
86 | isa = PBXProject;
87 | attributes = {
88 | BuildIndependentTargetsInParallel = 1;
89 | LastSwiftUpdateCheck = 1620;
90 | LastUpgradeCheck = 1620;
91 | TargetAttributes = {
92 | 9E04C5992D44E5FB00320146 = {
93 | CreatedOnToolsVersion = 16.2;
94 | };
95 | };
96 | };
97 | buildConfigurationList = 9E04C5952D44E5FB00320146 /* Build configuration list for PBXProject "SwipeAeroSpace" */;
98 | developmentRegion = en;
99 | hasScannedForEncodings = 0;
100 | knownRegions = (
101 | en,
102 | Base,
103 | );
104 | mainGroup = 9E04C5912D44E5FB00320146;
105 | minimizedProjectReferenceProxies = 1;
106 | packageReferences = (
107 | 9E4137D02D97E63100634ACB /* XCRemoteSwiftPackageReference "BlueSocket" */,
108 | );
109 | preferredProjectObjectVersion = 77;
110 | productRefGroup = 9E04C59B2D44E5FB00320146 /* Products */;
111 | projectDirPath = "";
112 | projectRoot = "";
113 | targets = (
114 | 9E04C5992D44E5FB00320146 /* SwipeAeroSpace */,
115 | );
116 | };
117 | /* End PBXProject section */
118 |
119 | /* Begin PBXResourcesBuildPhase section */
120 | 9E04C5982D44E5FB00320146 /* Resources */ = {
121 | isa = PBXResourcesBuildPhase;
122 | buildActionMask = 2147483647;
123 | files = (
124 | );
125 | runOnlyForDeploymentPostprocessing = 0;
126 | };
127 | /* End PBXResourcesBuildPhase section */
128 |
129 | /* Begin PBXSourcesBuildPhase section */
130 | 9E04C5962D44E5FB00320146 /* Sources */ = {
131 | isa = PBXSourcesBuildPhase;
132 | buildActionMask = 2147483647;
133 | files = (
134 | );
135 | runOnlyForDeploymentPostprocessing = 0;
136 | };
137 | /* End PBXSourcesBuildPhase section */
138 |
139 | /* Begin XCBuildConfiguration section */
140 | 9E04C5A72D44E5FC00320146 /* Debug */ = {
141 | isa = XCBuildConfiguration;
142 | buildSettings = {
143 | ALWAYS_SEARCH_USER_PATHS = NO;
144 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
145 | CLANG_ANALYZER_NONNULL = YES;
146 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
147 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
148 | CLANG_ENABLE_MODULES = YES;
149 | CLANG_ENABLE_OBJC_ARC = YES;
150 | CLANG_ENABLE_OBJC_WEAK = YES;
151 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
152 | CLANG_WARN_BOOL_CONVERSION = YES;
153 | CLANG_WARN_COMMA = YES;
154 | CLANG_WARN_CONSTANT_CONVERSION = YES;
155 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
156 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
157 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
158 | CLANG_WARN_EMPTY_BODY = YES;
159 | CLANG_WARN_ENUM_CONVERSION = YES;
160 | CLANG_WARN_INFINITE_RECURSION = YES;
161 | CLANG_WARN_INT_CONVERSION = YES;
162 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
163 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
164 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
165 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
166 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
167 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
168 | CLANG_WARN_STRICT_PROTOTYPES = YES;
169 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
170 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
171 | CLANG_WARN_UNREACHABLE_CODE = YES;
172 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
173 | COPY_PHASE_STRIP = NO;
174 | DEBUG_INFORMATION_FORMAT = dwarf;
175 | ENABLE_STRICT_OBJC_MSGSEND = YES;
176 | ENABLE_TESTABILITY = YES;
177 | ENABLE_USER_SCRIPT_SANDBOXING = YES;
178 | GCC_C_LANGUAGE_STANDARD = gnu17;
179 | GCC_DYNAMIC_NO_PIC = NO;
180 | GCC_NO_COMMON_BLOCKS = YES;
181 | GCC_OPTIMIZATION_LEVEL = 0;
182 | GCC_PREPROCESSOR_DEFINITIONS = (
183 | "DEBUG=1",
184 | "$(inherited)",
185 | );
186 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
187 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
188 | GCC_WARN_UNDECLARED_SELECTOR = YES;
189 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
190 | GCC_WARN_UNUSED_FUNCTION = YES;
191 | GCC_WARN_UNUSED_VARIABLE = YES;
192 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
193 | MACOSX_DEPLOYMENT_TARGET = 15.2;
194 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
195 | MTL_FAST_MATH = YES;
196 | ONLY_ACTIVE_ARCH = YES;
197 | SDKROOT = macosx;
198 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
199 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
200 | };
201 | name = Debug;
202 | };
203 | 9E04C5A82D44E5FC00320146 /* Release */ = {
204 | isa = XCBuildConfiguration;
205 | buildSettings = {
206 | ALWAYS_SEARCH_USER_PATHS = NO;
207 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
208 | CLANG_ANALYZER_NONNULL = YES;
209 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
210 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
211 | CLANG_ENABLE_MODULES = YES;
212 | CLANG_ENABLE_OBJC_ARC = YES;
213 | CLANG_ENABLE_OBJC_WEAK = YES;
214 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
215 | CLANG_WARN_BOOL_CONVERSION = YES;
216 | CLANG_WARN_COMMA = YES;
217 | CLANG_WARN_CONSTANT_CONVERSION = YES;
218 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
219 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
220 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
221 | CLANG_WARN_EMPTY_BODY = YES;
222 | CLANG_WARN_ENUM_CONVERSION = YES;
223 | CLANG_WARN_INFINITE_RECURSION = YES;
224 | CLANG_WARN_INT_CONVERSION = YES;
225 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
226 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
227 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
228 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
229 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
230 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
231 | CLANG_WARN_STRICT_PROTOTYPES = YES;
232 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
233 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
234 | CLANG_WARN_UNREACHABLE_CODE = YES;
235 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
236 | COPY_PHASE_STRIP = NO;
237 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
238 | ENABLE_NS_ASSERTIONS = NO;
239 | ENABLE_STRICT_OBJC_MSGSEND = YES;
240 | ENABLE_USER_SCRIPT_SANDBOXING = YES;
241 | GCC_C_LANGUAGE_STANDARD = gnu17;
242 | GCC_NO_COMMON_BLOCKS = YES;
243 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
244 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
245 | GCC_WARN_UNDECLARED_SELECTOR = YES;
246 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
247 | GCC_WARN_UNUSED_FUNCTION = YES;
248 | GCC_WARN_UNUSED_VARIABLE = YES;
249 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
250 | MACOSX_DEPLOYMENT_TARGET = 15.2;
251 | MTL_ENABLE_DEBUG_INFO = NO;
252 | MTL_FAST_MATH = YES;
253 | SDKROOT = macosx;
254 | SWIFT_COMPILATION_MODE = wholemodule;
255 | };
256 | name = Release;
257 | };
258 | 9E04C5AA2D44E5FC00320146 /* Debug */ = {
259 | isa = XCBuildConfiguration;
260 | buildSettings = {
261 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
262 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
263 | CODE_SIGN_ENTITLEMENTS = SwipeAeroSpace/SwipeAeroSpace.entitlements;
264 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
265 | CODE_SIGN_STYLE = Automatic;
266 | COMBINE_HIDPI_IMAGES = YES;
267 | CURRENT_PROJECT_VERSION = 17;
268 | DEVELOPMENT_ASSET_PATHS = "\"SwipeAeroSpace/Preview Content\"";
269 | DEVELOPMENT_TEAM = "";
270 | ENABLE_HARDENED_RUNTIME = YES;
271 | ENABLE_PREVIEWS = YES;
272 | GENERATE_INFOPLIST_FILE = YES;
273 | INFOPLIST_KEY_CFBundleDisplayName = SwipeAeroSpace;
274 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools";
275 | INFOPLIST_KEY_LSUIElement = YES;
276 | INFOPLIST_KEY_NSHumanReadableCopyright = "©Tricster";
277 | LD_RUNPATH_SEARCH_PATHS = (
278 | "$(inherited)",
279 | "@executable_path/../Frameworks",
280 | );
281 | MACOSX_DEPLOYMENT_TARGET = 13.5;
282 | MARKETING_VERSION = 0.2.5;
283 | PRODUCT_BUNDLE_IDENTIFIER = club.mediosz.SwipeAeroSpace;
284 | PRODUCT_NAME = "$(TARGET_NAME)";
285 | SWIFT_EMIT_LOC_STRINGS = YES;
286 | SWIFT_VERSION = 5.0;
287 | };
288 | name = Debug;
289 | };
290 | 9E04C5AB2D44E5FC00320146 /* Release */ = {
291 | isa = XCBuildConfiguration;
292 | buildSettings = {
293 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
294 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
295 | CODE_SIGN_ENTITLEMENTS = SwipeAeroSpace/SwipeAeroSpace.entitlements;
296 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
297 | CODE_SIGN_STYLE = Automatic;
298 | COMBINE_HIDPI_IMAGES = YES;
299 | CURRENT_PROJECT_VERSION = 17;
300 | DEVELOPMENT_ASSET_PATHS = "\"SwipeAeroSpace/Preview Content\"";
301 | DEVELOPMENT_TEAM = "";
302 | ENABLE_HARDENED_RUNTIME = YES;
303 | ENABLE_PREVIEWS = YES;
304 | GENERATE_INFOPLIST_FILE = YES;
305 | INFOPLIST_KEY_CFBundleDisplayName = SwipeAeroSpace;
306 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools";
307 | INFOPLIST_KEY_LSUIElement = YES;
308 | INFOPLIST_KEY_NSHumanReadableCopyright = "©Tricster";
309 | LD_RUNPATH_SEARCH_PATHS = (
310 | "$(inherited)",
311 | "@executable_path/../Frameworks",
312 | );
313 | MACOSX_DEPLOYMENT_TARGET = 13.5;
314 | MARKETING_VERSION = 0.2.5;
315 | PRODUCT_BUNDLE_IDENTIFIER = club.mediosz.SwipeAeroSpace;
316 | PRODUCT_NAME = "$(TARGET_NAME)";
317 | SWIFT_EMIT_LOC_STRINGS = YES;
318 | SWIFT_VERSION = 5.0;
319 | };
320 | name = Release;
321 | };
322 | /* End XCBuildConfiguration section */
323 |
324 | /* Begin XCConfigurationList section */
325 | 9E04C5952D44E5FB00320146 /* Build configuration list for PBXProject "SwipeAeroSpace" */ = {
326 | isa = XCConfigurationList;
327 | buildConfigurations = (
328 | 9E04C5A72D44E5FC00320146 /* Debug */,
329 | 9E04C5A82D44E5FC00320146 /* Release */,
330 | );
331 | defaultConfigurationIsVisible = 0;
332 | defaultConfigurationName = Release;
333 | };
334 | 9E04C5A92D44E5FC00320146 /* Build configuration list for PBXNativeTarget "SwipeAeroSpace" */ = {
335 | isa = XCConfigurationList;
336 | buildConfigurations = (
337 | 9E04C5AA2D44E5FC00320146 /* Debug */,
338 | 9E04C5AB2D44E5FC00320146 /* Release */,
339 | );
340 | defaultConfigurationIsVisible = 0;
341 | defaultConfigurationName = Release;
342 | };
343 | /* End XCConfigurationList section */
344 |
345 | /* Begin XCRemoteSwiftPackageReference section */
346 | 9E4137D02D97E63100634ACB /* XCRemoteSwiftPackageReference "BlueSocket" */ = {
347 | isa = XCRemoteSwiftPackageReference;
348 | repositoryURL = "https://github.com/Kitura/BlueSocket";
349 | requirement = {
350 | kind = upToNextMajorVersion;
351 | minimumVersion = 2.0.4;
352 | };
353 | };
354 | /* End XCRemoteSwiftPackageReference section */
355 |
356 | /* Begin XCSwiftPackageProductDependency section */
357 | 9E4137D12D97E63100634ACB /* BlueSocketTestCommonLibrary */ = {
358 | isa = XCSwiftPackageProductDependency;
359 | package = 9E4137D02D97E63100634ACB /* XCRemoteSwiftPackageReference "BlueSocket" */;
360 | productName = BlueSocketTestCommonLibrary;
361 | };
362 | 9E4137D32D97E63100634ACB /* Socket */ = {
363 | isa = XCSwiftPackageProductDependency;
364 | package = 9E4137D02D97E63100634ACB /* XCRemoteSwiftPackageReference "BlueSocket" */;
365 | productName = Socket;
366 | };
367 | /* End XCSwiftPackageProductDependency section */
368 | };
369 | rootObject = 9E04C5922D44E5FB00320146 /* Project object */;
370 | }
371 |
--------------------------------------------------------------------------------