├── 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 | ![banner](./assets/banner.png) 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 | --------------------------------------------------------------------------------