├── .gitignore ├── InputSourceSwitcher.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved ├── xcshareddata │ └── xcschemes │ │ └── InputSourceSwitcher.xcscheme └── xcuserdata │ └── yangxijie.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── InputSourceSwitcher ├── APP │ ├── AboutView.swift │ ├── App.swift │ └── ContentView.swift ├── Applescripts │ └── Applesripts.swift ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── icon1024.png │ │ ├── icon128.png │ │ ├── icon16.png │ │ ├── icon256-1.png │ │ ├── icon256.png │ │ ├── icon32-1.png │ │ ├── icon32.png │ │ ├── icon512-1.png │ │ ├── icon512.png │ │ └── icon64.png │ └── Contents.json ├── Auxiliary │ └── Pirnt+.swift ├── DataStructures │ ├── InputSource.swift │ └── InputSources.swift ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── Tools │ ├── Notificatioins.swift │ ├── Shortcuts.swift │ └── VersionAndBuildNumber.swift ├── ViewModel │ └── InputSourceModel.swift └── macOS │ ├── Info.plist │ └── InputSourceSwitcher.entitlements ├── LICENSE ├── README.md └── UpdateLog.md /.gitignore: -------------------------------------------------------------------------------- 1 | # My 2 | 3 | # Xcode 4 | # 5 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 6 | 7 | ## User settings 8 | xcuserdata/ 9 | 10 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 11 | *.xcscmblueprint 12 | *.xccheckout 13 | 14 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 15 | build/ 16 | DerivedData/ 17 | *.moved-aside 18 | *.pbxuser 19 | !default.pbxuser 20 | *.mode1v3 21 | !default.mode1v3 22 | *.mode2v3 23 | !default.mode2v3 24 | *.perspectivev3 25 | !default.perspectivev3 26 | 27 | ## Obj-C/Swift specific 28 | *.hmap 29 | 30 | ## App packaging 31 | *.ipa 32 | *.dSYM.zip 33 | *.dSYM 34 | 35 | ## Playgrounds 36 | timeline.xctimeline 37 | playground.xcworkspace 38 | 39 | # Swift Package Manager 40 | # 41 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 42 | # Packages/ 43 | # Package.pins 44 | # Package.resolved 45 | # *.xcodeproj 46 | # 47 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 48 | # hence it is not needed unless you have added a package configuration file to your project 49 | # .swiftpm 50 | 51 | .build/ 52 | 53 | # CocoaPods 54 | # 55 | # We recommend against adding the Pods directory to your .gitignore. However 56 | # you should judge for yourself, the pros and cons are mentioned at: 57 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 58 | # 59 | # Pods/ 60 | # 61 | # Add this line if you want to avoid checking in source code from the Xcode workspace 62 | # *.xcworkspace 63 | 64 | # Carthage 65 | # 66 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 67 | # Carthage/Checkouts 68 | 69 | Carthage/Build/ 70 | 71 | # Accio dependency management 72 | Dependencies/ 73 | .accio/ 74 | 75 | # fastlane 76 | # 77 | # It is recommended to not store the screenshots in the git repo. 78 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 79 | # For more information about the recommended setup visit: 80 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 81 | 82 | fastlane/report.xml 83 | fastlane/Preview.html 84 | fastlane/screenshots/**/*.png 85 | fastlane/test_output 86 | 87 | # Code Injection 88 | # 89 | # After new code Injection tools there's a generated folder /iOSInjectionProject 90 | # https://github.com/johnno1962/injectionforxcode 91 | 92 | iOSInjectionProject/ 93 | 94 | -------------------------------------------------------------------------------- /InputSourceSwitcher.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 52; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 4831FBE02683B46900D6C6F8 /* Pirnt+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4831FBDF2683B46900D6C6F8 /* Pirnt+.swift */; }; 11 | 48462E272681EC18006DB779 /* InputSourceModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48462E262681EC18006DB779 /* InputSourceModel.swift */; }; 12 | 48462E292681F296006DB779 /* InputSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48462E282681F296006DB779 /* InputSource.swift */; }; 13 | 48462E2E2681F871006DB779 /* KeyboardShortcuts in Frameworks */ = {isa = PBXBuildFile; productRef = 48462E2D2681F871006DB779 /* KeyboardShortcuts */; }; 14 | 4880B5372683819300D53186 /* Shortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4880B5362683819300D53186 /* Shortcuts.swift */; }; 15 | 4880B53A26838FC800D53186 /* Applesripts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4880B53926838FC800D53186 /* Applesripts.swift */; }; 16 | 48860FC826845485002A5487 /* VersionAndBuildNumber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48860FC726845485002A5487 /* VersionAndBuildNumber.swift */; }; 17 | 489B885426BD2EE20090DC8D /* Notificatioins.swift in Sources */ = {isa = PBXBuildFile; fileRef = 489B885326BD2EE20090DC8D /* Notificatioins.swift */; }; 18 | 48CE6B692682611D00B1A186 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48CE6B682682611D00B1A186 /* AboutView.swift */; }; 19 | 48DA10482681781500A3D537 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48DA10472681781500A3D537 /* App.swift */; }; 20 | 48DA104A2681781500A3D537 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48DA10492681781500A3D537 /* ContentView.swift */; }; 21 | 48DA104C2681781700A3D537 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 48DA104B2681781700A3D537 /* Assets.xcassets */; }; 22 | 48DA104F2681781700A3D537 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 48DA104E2681781700A3D537 /* Preview Assets.xcassets */; }; 23 | 48DA10602681B26E00A3D537 /* InputSources.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48DA105F2681B26E00A3D537 /* InputSources.swift */; }; 24 | /* End PBXBuildFile section */ 25 | 26 | /* Begin PBXFileReference section */ 27 | 4831FBDE2683AA0700D6C6F8 /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = SOURCE_ROOT; }; 28 | 4831FBDF2683B46900D6C6F8 /* Pirnt+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Pirnt+.swift"; sourceTree = ""; }; 29 | 48462E262681EC18006DB779 /* InputSourceModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputSourceModel.swift; sourceTree = ""; }; 30 | 48462E282681F296006DB779 /* InputSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputSource.swift; sourceTree = ""; }; 31 | 4880B5362683819300D53186 /* Shortcuts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shortcuts.swift; sourceTree = ""; }; 32 | 4880B53926838FC800D53186 /* Applesripts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Applesripts.swift; sourceTree = ""; }; 33 | 48860FC726845485002A5487 /* VersionAndBuildNumber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionAndBuildNumber.swift; sourceTree = ""; }; 34 | 4892A68F272D1F5600D6E017 /* UpdateLog.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = UpdateLog.md; sourceTree = ""; }; 35 | 489B885326BD2EE20090DC8D /* Notificatioins.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notificatioins.swift; sourceTree = ""; }; 36 | 48CE6B682682611D00B1A186 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = ""; }; 37 | 48DA10442681781500A3D537 /* Source Switcher.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Source Switcher.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 38 | 48DA10472681781500A3D537 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = ""; }; 39 | 48DA10492681781500A3D537 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 40 | 48DA104B2681781700A3D537 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 41 | 48DA104E2681781700A3D537 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 42 | 48DA10502681781700A3D537 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 43 | 48DA10512681781700A3D537 /* InputSourceSwitcher.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = InputSourceSwitcher.entitlements; sourceTree = ""; }; 44 | 48DA10592681925400A3D537 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 45 | 48DA105F2681B26E00A3D537 /* InputSources.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputSources.swift; sourceTree = ""; }; 46 | /* End PBXFileReference section */ 47 | 48 | /* Begin PBXFrameworksBuildPhase section */ 49 | 48DA10412681781500A3D537 /* Frameworks */ = { 50 | isa = PBXFrameworksBuildPhase; 51 | buildActionMask = 2147483647; 52 | files = ( 53 | 48462E2E2681F871006DB779 /* KeyboardShortcuts in Frameworks */, 54 | ); 55 | runOnlyForDeploymentPostprocessing = 0; 56 | }; 57 | /* End PBXFrameworksBuildPhase section */ 58 | 59 | /* Begin PBXGroup section */ 60 | 48462E352682193C006DB779 /* APP */ = { 61 | isa = PBXGroup; 62 | children = ( 63 | 48DA10472681781500A3D537 /* App.swift */, 64 | 48DA10492681781500A3D537 /* ContentView.swift */, 65 | 48CE6B682682611D00B1A186 /* AboutView.swift */, 66 | ); 67 | path = APP; 68 | sourceTree = ""; 69 | }; 70 | 4858851926839730000D36BF /* macOS */ = { 71 | isa = PBXGroup; 72 | children = ( 73 | 48DA10512681781700A3D537 /* InputSourceSwitcher.entitlements */, 74 | 48DA10502681781700A3D537 /* Info.plist */, 75 | ); 76 | path = macOS; 77 | sourceTree = ""; 78 | }; 79 | 4858851A268398F7000D36BF /* Auxiliary */ = { 80 | isa = PBXGroup; 81 | children = ( 82 | 4831FBDF2683B46900D6C6F8 /* Pirnt+.swift */, 83 | ); 84 | path = Auxiliary; 85 | sourceTree = ""; 86 | }; 87 | 4880B53B26838FD700D53186 /* Applescripts */ = { 88 | isa = PBXGroup; 89 | children = ( 90 | 4880B53926838FC800D53186 /* Applesripts.swift */, 91 | ); 92 | path = Applescripts; 93 | sourceTree = ""; 94 | }; 95 | 489B885226BD2D200090DC8D /* Tools */ = { 96 | isa = PBXGroup; 97 | children = ( 98 | 48860FC726845485002A5487 /* VersionAndBuildNumber.swift */, 99 | 4880B5362683819300D53186 /* Shortcuts.swift */, 100 | 489B885326BD2EE20090DC8D /* Notificatioins.swift */, 101 | ); 102 | path = Tools; 103 | sourceTree = ""; 104 | }; 105 | 48DA103B2681781500A3D537 = { 106 | isa = PBXGroup; 107 | children = ( 108 | 48DA10462681781500A3D537 /* InputSourceSwitcher */, 109 | 4831FBDE2683AA0700D6C6F8 /* LICENSE */, 110 | 48DA10592681925400A3D537 /* README.md */, 111 | 4892A68F272D1F5600D6E017 /* UpdateLog.md */, 112 | 48DA10452681781500A3D537 /* Products */, 113 | ); 114 | sourceTree = ""; 115 | }; 116 | 48DA10452681781500A3D537 /* Products */ = { 117 | isa = PBXGroup; 118 | children = ( 119 | 48DA10442681781500A3D537 /* Source Switcher.app */, 120 | ); 121 | name = Products; 122 | sourceTree = ""; 123 | }; 124 | 48DA10462681781500A3D537 /* InputSourceSwitcher */ = { 125 | isa = PBXGroup; 126 | children = ( 127 | 48DA10612681B27500A3D537 /* DataStructures */, 128 | 48DA10642681B66100A3D537 /* ViewModel */, 129 | 48462E352682193C006DB779 /* APP */, 130 | 4880B53B26838FD700D53186 /* Applescripts */, 131 | 489B885226BD2D200090DC8D /* Tools */, 132 | 4858851A268398F7000D36BF /* Auxiliary */, 133 | 4858851926839730000D36BF /* macOS */, 134 | 48DA104B2681781700A3D537 /* Assets.xcassets */, 135 | 48DA104D2681781700A3D537 /* Preview Content */, 136 | ); 137 | path = InputSourceSwitcher; 138 | sourceTree = ""; 139 | }; 140 | 48DA104D2681781700A3D537 /* Preview Content */ = { 141 | isa = PBXGroup; 142 | children = ( 143 | 48DA104E2681781700A3D537 /* Preview Assets.xcassets */, 144 | ); 145 | path = "Preview Content"; 146 | sourceTree = ""; 147 | }; 148 | 48DA10612681B27500A3D537 /* DataStructures */ = { 149 | isa = PBXGroup; 150 | children = ( 151 | 48462E282681F296006DB779 /* InputSource.swift */, 152 | 48DA105F2681B26E00A3D537 /* InputSources.swift */, 153 | ); 154 | path = DataStructures; 155 | sourceTree = ""; 156 | }; 157 | 48DA10642681B66100A3D537 /* ViewModel */ = { 158 | isa = PBXGroup; 159 | children = ( 160 | 48462E262681EC18006DB779 /* InputSourceModel.swift */, 161 | ); 162 | path = ViewModel; 163 | sourceTree = ""; 164 | }; 165 | /* End PBXGroup section */ 166 | 167 | /* Begin PBXNativeTarget section */ 168 | 48DA10432681781500A3D537 /* Source Switcher */ = { 169 | isa = PBXNativeTarget; 170 | buildConfigurationList = 48DA10542681781700A3D537 /* Build configuration list for PBXNativeTarget "Source Switcher" */; 171 | buildPhases = ( 172 | 48DA10402681781500A3D537 /* Sources */, 173 | 48DA10412681781500A3D537 /* Frameworks */, 174 | 48DA10422681781500A3D537 /* Resources */, 175 | ); 176 | buildRules = ( 177 | ); 178 | dependencies = ( 179 | ); 180 | name = "Source Switcher"; 181 | packageProductDependencies = ( 182 | 48462E2D2681F871006DB779 /* KeyboardShortcuts */, 183 | ); 184 | productName = InputSourceSwitcher; 185 | productReference = 48DA10442681781500A3D537 /* Source Switcher.app */; 186 | productType = "com.apple.product-type.application"; 187 | }; 188 | /* End PBXNativeTarget section */ 189 | 190 | /* Begin PBXProject section */ 191 | 48DA103C2681781500A3D537 /* Project object */ = { 192 | isa = PBXProject; 193 | attributes = { 194 | LastSwiftUpdateCheck = 1250; 195 | LastUpgradeCheck = 1250; 196 | TargetAttributes = { 197 | 48DA10432681781500A3D537 = { 198 | CreatedOnToolsVersion = 12.5; 199 | }; 200 | }; 201 | }; 202 | buildConfigurationList = 48DA103F2681781500A3D537 /* Build configuration list for PBXProject "InputSourceSwitcher" */; 203 | compatibilityVersion = "Xcode 9.3"; 204 | developmentRegion = en; 205 | hasScannedForEncodings = 0; 206 | knownRegions = ( 207 | en, 208 | Base, 209 | ); 210 | mainGroup = 48DA103B2681781500A3D537; 211 | packageReferences = ( 212 | 48462E2C2681F871006DB779 /* XCRemoteSwiftPackageReference "KeyboardShortcuts" */, 213 | ); 214 | productRefGroup = 48DA10452681781500A3D537 /* Products */; 215 | projectDirPath = ""; 216 | projectRoot = ""; 217 | targets = ( 218 | 48DA10432681781500A3D537 /* Source Switcher */, 219 | ); 220 | }; 221 | /* End PBXProject section */ 222 | 223 | /* Begin PBXResourcesBuildPhase section */ 224 | 48DA10422681781500A3D537 /* Resources */ = { 225 | isa = PBXResourcesBuildPhase; 226 | buildActionMask = 2147483647; 227 | files = ( 228 | 48DA104F2681781700A3D537 /* Preview Assets.xcassets in Resources */, 229 | 48DA104C2681781700A3D537 /* Assets.xcassets in Resources */, 230 | ); 231 | runOnlyForDeploymentPostprocessing = 0; 232 | }; 233 | /* End PBXResourcesBuildPhase section */ 234 | 235 | /* Begin PBXSourcesBuildPhase section */ 236 | 48DA10402681781500A3D537 /* Sources */ = { 237 | isa = PBXSourcesBuildPhase; 238 | buildActionMask = 2147483647; 239 | files = ( 240 | 48462E292681F296006DB779 /* InputSource.swift in Sources */, 241 | 48DA10602681B26E00A3D537 /* InputSources.swift in Sources */, 242 | 48860FC826845485002A5487 /* VersionAndBuildNumber.swift in Sources */, 243 | 4831FBE02683B46900D6C6F8 /* Pirnt+.swift in Sources */, 244 | 48DA104A2681781500A3D537 /* ContentView.swift in Sources */, 245 | 4880B53A26838FC800D53186 /* Applesripts.swift in Sources */, 246 | 4880B5372683819300D53186 /* Shortcuts.swift in Sources */, 247 | 48462E272681EC18006DB779 /* InputSourceModel.swift in Sources */, 248 | 48CE6B692682611D00B1A186 /* AboutView.swift in Sources */, 249 | 489B885426BD2EE20090DC8D /* Notificatioins.swift in Sources */, 250 | 48DA10482681781500A3D537 /* App.swift in Sources */, 251 | ); 252 | runOnlyForDeploymentPostprocessing = 0; 253 | }; 254 | /* End PBXSourcesBuildPhase section */ 255 | 256 | /* Begin XCBuildConfiguration section */ 257 | 48DA10522681781700A3D537 /* Debug */ = { 258 | isa = XCBuildConfiguration; 259 | buildSettings = { 260 | ALWAYS_SEARCH_USER_PATHS = NO; 261 | CLANG_ANALYZER_NONNULL = YES; 262 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 263 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 264 | CLANG_CXX_LIBRARY = "libc++"; 265 | CLANG_ENABLE_MODULES = YES; 266 | CLANG_ENABLE_OBJC_ARC = YES; 267 | CLANG_ENABLE_OBJC_WEAK = YES; 268 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 269 | CLANG_WARN_BOOL_CONVERSION = YES; 270 | CLANG_WARN_COMMA = YES; 271 | CLANG_WARN_CONSTANT_CONVERSION = YES; 272 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 273 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 274 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 275 | CLANG_WARN_EMPTY_BODY = YES; 276 | CLANG_WARN_ENUM_CONVERSION = YES; 277 | CLANG_WARN_INFINITE_RECURSION = YES; 278 | CLANG_WARN_INT_CONVERSION = YES; 279 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 280 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 281 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 282 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 283 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 284 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 285 | CLANG_WARN_STRICT_PROTOTYPES = YES; 286 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 287 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 288 | CLANG_WARN_UNREACHABLE_CODE = YES; 289 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 290 | COPY_PHASE_STRIP = NO; 291 | DEBUG_INFORMATION_FORMAT = dwarf; 292 | ENABLE_STRICT_OBJC_MSGSEND = YES; 293 | ENABLE_TESTABILITY = YES; 294 | GCC_C_LANGUAGE_STANDARD = gnu11; 295 | GCC_DYNAMIC_NO_PIC = NO; 296 | GCC_NO_COMMON_BLOCKS = YES; 297 | GCC_OPTIMIZATION_LEVEL = 0; 298 | GCC_PREPROCESSOR_DEFINITIONS = ( 299 | "DEBUG=1", 300 | "$(inherited)", 301 | ); 302 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 303 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 304 | GCC_WARN_UNDECLARED_SELECTOR = YES; 305 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 306 | GCC_WARN_UNUSED_FUNCTION = YES; 307 | GCC_WARN_UNUSED_VARIABLE = YES; 308 | MACOSX_DEPLOYMENT_TARGET = 11.3; 309 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 310 | MTL_FAST_MATH = YES; 311 | ONLY_ACTIVE_ARCH = YES; 312 | SDKROOT = macosx; 313 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 314 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 315 | }; 316 | name = Debug; 317 | }; 318 | 48DA10532681781700A3D537 /* Release */ = { 319 | isa = XCBuildConfiguration; 320 | buildSettings = { 321 | ALWAYS_SEARCH_USER_PATHS = NO; 322 | CLANG_ANALYZER_NONNULL = YES; 323 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 324 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 325 | CLANG_CXX_LIBRARY = "libc++"; 326 | CLANG_ENABLE_MODULES = YES; 327 | CLANG_ENABLE_OBJC_ARC = YES; 328 | CLANG_ENABLE_OBJC_WEAK = YES; 329 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 330 | CLANG_WARN_BOOL_CONVERSION = YES; 331 | CLANG_WARN_COMMA = YES; 332 | CLANG_WARN_CONSTANT_CONVERSION = YES; 333 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 334 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 335 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 336 | CLANG_WARN_EMPTY_BODY = YES; 337 | CLANG_WARN_ENUM_CONVERSION = YES; 338 | CLANG_WARN_INFINITE_RECURSION = YES; 339 | CLANG_WARN_INT_CONVERSION = YES; 340 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 341 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 342 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 343 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 344 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 345 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 346 | CLANG_WARN_STRICT_PROTOTYPES = YES; 347 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 348 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 349 | CLANG_WARN_UNREACHABLE_CODE = YES; 350 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 351 | COPY_PHASE_STRIP = NO; 352 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 353 | ENABLE_NS_ASSERTIONS = NO; 354 | ENABLE_STRICT_OBJC_MSGSEND = YES; 355 | GCC_C_LANGUAGE_STANDARD = gnu11; 356 | GCC_NO_COMMON_BLOCKS = YES; 357 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 358 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 359 | GCC_WARN_UNDECLARED_SELECTOR = YES; 360 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 361 | GCC_WARN_UNUSED_FUNCTION = YES; 362 | GCC_WARN_UNUSED_VARIABLE = YES; 363 | MACOSX_DEPLOYMENT_TARGET = 11.3; 364 | MTL_ENABLE_DEBUG_INFO = NO; 365 | MTL_FAST_MATH = YES; 366 | SDKROOT = macosx; 367 | SWIFT_COMPILATION_MODE = wholemodule; 368 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 369 | }; 370 | name = Release; 371 | }; 372 | 48DA10552681781700A3D537 /* Debug */ = { 373 | isa = XCBuildConfiguration; 374 | buildSettings = { 375 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 376 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 377 | CODE_SIGN_ENTITLEMENTS = InputSourceSwitcher/macOS/InputSourceSwitcher.entitlements; 378 | CODE_SIGN_IDENTITY = "-"; 379 | CODE_SIGN_STYLE = Automatic; 380 | COMBINE_HIDPI_IMAGES = YES; 381 | CURRENT_PROJECT_VERSION = 10; 382 | DEVELOPMENT_ASSET_PATHS = "\"InputSourceSwitcher/Preview Content\""; 383 | DEVELOPMENT_TEAM = C4W6UUH267; 384 | ENABLE_HARDENED_RUNTIME = YES; 385 | ENABLE_PREVIEWS = YES; 386 | INFOPLIST_FILE = InputSourceSwitcher/macOS/Info.plist; 387 | LD_RUNPATH_SEARCH_PATHS = ( 388 | "$(inherited)", 389 | "@executable_path/../Frameworks", 390 | ); 391 | MACOSX_DEPLOYMENT_TARGET = 11.0; 392 | MARKETING_VERSION = 1.3; 393 | PRODUCT_BUNDLE_IDENTIFIER = com.yangxijie.SourceSwitcher; 394 | PRODUCT_NAME = "$(TARGET_NAME)"; 395 | SWIFT_VERSION = 5.0; 396 | }; 397 | name = Debug; 398 | }; 399 | 48DA10562681781700A3D537 /* Release */ = { 400 | isa = XCBuildConfiguration; 401 | buildSettings = { 402 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 403 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 404 | CODE_SIGN_ENTITLEMENTS = InputSourceSwitcher/macOS/InputSourceSwitcher.entitlements; 405 | CODE_SIGN_IDENTITY = "-"; 406 | CODE_SIGN_STYLE = Automatic; 407 | COMBINE_HIDPI_IMAGES = YES; 408 | CURRENT_PROJECT_VERSION = 10; 409 | DEVELOPMENT_ASSET_PATHS = "\"InputSourceSwitcher/Preview Content\""; 410 | DEVELOPMENT_TEAM = C4W6UUH267; 411 | ENABLE_HARDENED_RUNTIME = YES; 412 | ENABLE_PREVIEWS = YES; 413 | INFOPLIST_FILE = InputSourceSwitcher/macOS/Info.plist; 414 | LD_RUNPATH_SEARCH_PATHS = ( 415 | "$(inherited)", 416 | "@executable_path/../Frameworks", 417 | ); 418 | MACOSX_DEPLOYMENT_TARGET = 11.0; 419 | MARKETING_VERSION = 1.3; 420 | PRODUCT_BUNDLE_IDENTIFIER = com.yangxijie.SourceSwitcher; 421 | PRODUCT_NAME = "$(TARGET_NAME)"; 422 | SWIFT_VERSION = 5.0; 423 | }; 424 | name = Release; 425 | }; 426 | /* End XCBuildConfiguration section */ 427 | 428 | /* Begin XCConfigurationList section */ 429 | 48DA103F2681781500A3D537 /* Build configuration list for PBXProject "InputSourceSwitcher" */ = { 430 | isa = XCConfigurationList; 431 | buildConfigurations = ( 432 | 48DA10522681781700A3D537 /* Debug */, 433 | 48DA10532681781700A3D537 /* Release */, 434 | ); 435 | defaultConfigurationIsVisible = 0; 436 | defaultConfigurationName = Release; 437 | }; 438 | 48DA10542681781700A3D537 /* Build configuration list for PBXNativeTarget "Source Switcher" */ = { 439 | isa = XCConfigurationList; 440 | buildConfigurations = ( 441 | 48DA10552681781700A3D537 /* Debug */, 442 | 48DA10562681781700A3D537 /* Release */, 443 | ); 444 | defaultConfigurationIsVisible = 0; 445 | defaultConfigurationName = Release; 446 | }; 447 | /* End XCConfigurationList section */ 448 | 449 | /* Begin XCRemoteSwiftPackageReference section */ 450 | 48462E2C2681F871006DB779 /* XCRemoteSwiftPackageReference "KeyboardShortcuts" */ = { 451 | isa = XCRemoteSwiftPackageReference; 452 | repositoryURL = "https://github.com/sindresorhus/KeyboardShortcuts"; 453 | requirement = { 454 | kind = upToNextMajorVersion; 455 | minimumVersion = 1.2.0; 456 | }; 457 | }; 458 | /* End XCRemoteSwiftPackageReference section */ 459 | 460 | /* Begin XCSwiftPackageProductDependency section */ 461 | 48462E2D2681F871006DB779 /* KeyboardShortcuts */ = { 462 | isa = XCSwiftPackageProductDependency; 463 | package = 48462E2C2681F871006DB779 /* XCRemoteSwiftPackageReference "KeyboardShortcuts" */; 464 | productName = KeyboardShortcuts; 465 | }; 466 | /* End XCSwiftPackageProductDependency section */ 467 | }; 468 | rootObject = 48DA103C2681781500A3D537 /* Project object */; 469 | } 470 | -------------------------------------------------------------------------------- /InputSourceSwitcher.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /InputSourceSwitcher.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /InputSourceSwitcher.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "KeyboardShortcuts", 6 | "repositoryURL": "https://github.com/sindresorhus/KeyboardShortcuts", 7 | "state": { 8 | "branch": null, 9 | "revision": "a7a3ff6248b7564ef4ef2e9003672bf1b9f77ad1", 10 | "version": "1.2.0" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /InputSourceSwitcher.xcodeproj/xcshareddata/xcschemes/InputSourceSwitcher.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /InputSourceSwitcher.xcodeproj/xcuserdata/yangxijie.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | InputSourceSwitcher.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | 48DA10432681781500A3D537 16 | 17 | primary 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /InputSourceSwitcher/APP/AboutView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct AboutView: View { 4 | static let height = CGFloat(390) 5 | 6 | var body: some View { 7 | VStack(alignment: .leading) { 8 | IntroductionView() 9 | 10 | Divider() 11 | 12 | InstructionsView() 13 | 14 | Divider() 15 | 16 | ExternalLinksView() 17 | 18 | Divider() 19 | 20 | ReferecedOpenSourceProjectsView() 21 | } 22 | .padding() 23 | .frame(minHeight: AboutView.height) 24 | } 25 | 26 | struct IntroductionView: View { 27 | var body: some View { 28 | VStack(alignment: .leading) { 29 | Text("A menu bar app to switch input sources") 30 | Text("swiftly using shortcuts on") 31 | Text("macOS 11 Big Sur or later.") 32 | // TODO: macOS 12 will support markdown format. 33 | } 34 | } 35 | } 36 | 37 | struct InstructionsView: View { 38 | var body: some View { 39 | VStack(alignment: .leading) { 40 | SingleLineInstructionView(name: "About", shortcut: "⌘I", instruction: "Open this window.") 41 | SingleLineInstructionView(name: "Update", shortcut: "⌘U", instruction: "Update input sources and preserve shortcuts.") 42 | SingleLineInstructionView(name: "Reset", shortcut: "⌘R", instruction: "Reset input sources and shortcuts.") 43 | SingleLineInstructionView(name: "Quit", shortcut: "⌘Q", instruction: "Quit the app.") 44 | } 45 | } 46 | 47 | struct SingleLineInstructionView: View { 48 | var name: String = "" 49 | var shortcut: String = "" 50 | var instruction: String = "" 51 | 52 | var body: some View { 53 | VStack(alignment: .leading) { 54 | Text("\(name) \(shortcut)") 55 | .fontWeight(.bold) 56 | 57 | Text(instruction) 58 | } 59 | } 60 | } 61 | } 62 | 63 | struct ExternalLinksView: View { 64 | @Environment(\.openURL) var openURL 65 | var body: some View { 66 | VStack(alignment: .leading) { 67 | Text("Version \(appVersionString) (\(buildNumber))") 68 | 69 | Button("View it on GitHub") { 70 | openURL(URL(string: "https://github.com/Yang-Xijie/InputSourceSwitcher")!) 71 | } 72 | 73 | Button("Report a bug") { 74 | openURL(URL(string: "https://github.com/Yang-Xijie/InputSourceSwitcher/issues")!) 75 | } 76 | 77 | Button("Support") { 78 | openURL(URL(string: "https://yang-xijie.github.io/SITE/postscript/support.html")!) 79 | } 80 | } 81 | } 82 | } 83 | 84 | struct ReferecedOpenSourceProjectsView: View { 85 | var body: some View { 86 | VStack(alignment: .leading) { 87 | Text("Referenced open source projects") 88 | 89 | OpenSourceProjectView( 90 | projectName: "KeyboardShortcuts", 91 | repositoryURL: "https://github.com/sindresorhus/KeyboardShortcuts", 92 | licenseURL: "https://github.com/sindresorhus/KeyboardShortcuts/blob/main/license") 93 | OpenSourceProjectView( 94 | projectName: "menubarpopoverswiftui2", 95 | repositoryURL: "https://github.com/zaferarican/menubarpopoverswiftui2", 96 | licenseURL: "https://github.com/zaferarican/menubarpopoverswiftui2/blob/master/LICENSE") 97 | } 98 | } 99 | 100 | struct OpenSourceProjectView: View { 101 | var projectName: String = "" 102 | var repositoryURL: String = "" 103 | var licenseURL: String = "" 104 | 105 | var body: some View { 106 | HStack { 107 | Link(projectName, 108 | destination: URL(string: repositoryURL)!) 109 | Link("LICENSE", 110 | destination: URL(string: licenseURL)!) 111 | } 112 | } 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /InputSourceSwitcher/APP/App.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import UserNotifications 3 | 4 | @main 5 | struct SourceSwitcherApp: App { 6 | // make this app a menu bar app 7 | @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate 8 | 9 | // add link to AboutView 10 | @Environment(\.openURL) var openURL 11 | 12 | var body: some Scene { 13 | // [another window] 14 | Settings { 15 | EmptyView() 16 | } 17 | } 18 | } 19 | 20 | // make this app a menu bar app 21 | // refer to: https://github.com/zaferarican/menubarpopoverswiftui2 22 | // LICENSE: https://github.com/zaferarican/menubarpopoverswiftui2/blob/master/LICENSE 23 | class AppDelegate: NSObject, NSApplicationDelegate { 24 | @ObservedObject var MyInputSources = InputSourcesModel() // use @StateObject will induce some problems... 25 | 26 | var popover = NSPopover() 27 | var statusBarItem: NSStatusItem? 28 | 29 | func applicationDidFinishLaunching(_ notification: Notification) { 30 | // Specifies the behavior of the popover. 31 | // transient - The system will close the popover when the user interacts with a user interface element outside the popover. 32 | popover.behavior = .transient 33 | popover.animates = false 34 | // The view controller that manages the content of the popover. 35 | popover.contentViewController = NSViewController() 36 | popover.contentViewController?.view = NSHostingView( 37 | rootView: ContentView(MyInputSources: MyInputSources)) 38 | popover.contentViewController?.view.window?.makeKey() 39 | 40 | statusBarItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) 41 | statusBarItem?.button?.image = NSImage( 42 | systemSymbolName: "keyboard", 43 | accessibilityDescription: "keyboard")? 44 | .withSymbolConfiguration(NSImage.SymbolConfiguration(textStyle: .body, scale: .large)) 45 | statusBarItem?.button?.action = #selector(AppDelegate.togglePopover(_:)) 46 | 47 | // When starting the app, show the popover. 48 | // If don't do like this, open the app and shortcuts will not work until click the icon on the menu bar. 49 | // FIXME: If the user hides the menu bar (System Preferences -> Dock & Menu Bar -> Dock & Menu Bar -> Automatically hide and show the menu bar -> checked), the popover will appear on the top left corner of the screen, instead of appearing below the menu bar icon. 50 | if 51 | // true // debug 52 | UserDefaults.isFirstLaunch() 53 | { 54 | // first launch - show the window 55 | DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { 56 | if let button = self.statusBarItem?.button { 57 | self.popover.show( 58 | relativeTo: button.bounds, 59 | of: button, preferredEdge: NSRectEdge.minY) 60 | } 61 | } 62 | } else { 63 | // second and after launch - activate the window to start `KeyboardShortcuts`, but not show the window 64 | if let button = statusBarItem?.button { 65 | popover.show( 66 | relativeTo: button.bounds, 67 | of: button, preferredEdge: NSRectEdge.minY) 68 | } 69 | } 70 | 71 | // send notifications on macOS 72 | RequestNotificationCenterAuthorization() 73 | } 74 | 75 | func RequestNotificationCenterAuthorization() { 76 | let center = UNUserNotificationCenter.current() 77 | center.requestAuthorization(options: [.alert, .sound, .badge]) { _, error in 78 | if let error = error { 79 | print("[NotificationCenter.requestAuthorization] error - \(error)") 80 | } 81 | } 82 | } 83 | 84 | @objc func showPopover(_ sender: AnyObject?) { 85 | if let button = statusBarItem?.button { 86 | popover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY) 87 | } 88 | } 89 | 90 | @objc func closePopover(_ sender: AnyObject?) { 91 | popover.performClose(sender) 92 | } 93 | 94 | @objc func togglePopover(_ sender: AnyObject?) { 95 | print("[Popover before toggling] popover.isShown: \(popover.isShown)") 96 | if popover.isShown { 97 | closePopover(sender) 98 | } else { 99 | showPopover(sender) 100 | } 101 | print("[Popover after toggling] popover.isShown: \(popover.isShown)") 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /InputSourceSwitcher/APP/ContentView.swift: -------------------------------------------------------------------------------- 1 | import KeyboardShortcuts 2 | import SwiftUI 3 | 4 | // [ContentView] 5 | struct ContentView: View { 6 | @ObservedObject var MyInputSources = InputSourcesModel() 7 | 8 | @State var isShowingAbout: Bool = false 9 | 10 | var body: some View { 11 | let frameHeight = CGFloat(100) + CGFloat(MyInputSources.inputSources.count * 30) // This formula is revived from experiments on MacBook Pro 13', whose display is scaled at 1680 * 1050. 12 | 13 | VStack(alignment: .center) { 14 | VStack { 15 | TopOptionView(MyInputSources: MyInputSources, isShowingAbout: $isShowingAbout) 16 | 17 | Divider() 18 | 19 | SwitcherView(MyInputSources: MyInputSources) 20 | } 21 | .frame(minHeight: frameHeight) 22 | 23 | if isShowingAbout { 24 | Divider() 25 | 26 | AboutView() 27 | } 28 | } 29 | .frame(minWidth: 400) 30 | .padding() 31 | } 32 | } 33 | 34 | struct TopOptionView: View { 35 | @ObservedObject var MyInputSources: InputSourcesModel 36 | @Binding var isShowingAbout: Bool 37 | 38 | @Environment(\.openURL) var openURL 39 | 40 | var body: some View { 41 | HStack { 42 | Button(isShowingAbout ? "Hide About" : "About") { 43 | print("[Button] About clicked") 44 | 45 | withAnimation { 46 | isShowingAbout.toggle() 47 | } 48 | } 49 | .padding(.trailing) 50 | .keyboardShortcut("i", modifiers: .command) 51 | 52 | Button("Update") { 53 | print("[Button] Update clicked") 54 | 55 | // get new InputSources from system and save KeyboardShortcuts at the same time 56 | withAnimation { 57 | MyInputSources.Update() 58 | } 59 | } 60 | 61 | .keyboardShortcut("u", modifiers: .command) 62 | 63 | Button("Reset") { 64 | print("[Button] Reset clicked") 65 | 66 | // reset KeyboardShortcuts and InputSources 67 | withAnimation { 68 | MyInputSources.Reset() 69 | } 70 | } 71 | 72 | .keyboardShortcut("r", modifiers: .command) 73 | 74 | Button("Quit") { 75 | print("[Button] Quit clicked") 76 | NSApplication.shared.terminate(self) // quit app == cmd Q 77 | } 78 | .keyboardShortcut("q", modifiers: .command) 79 | .padding(.leading) 80 | } 81 | .padding() 82 | } 83 | } 84 | 85 | struct SwitcherView: View { 86 | @ObservedObject var MyInputSources: InputSourcesModel 87 | 88 | var body: some View { 89 | // Use right alignment to put `record shortcut` who are all the same size to make SwitcherView orderly 90 | VStack(alignment: .trailing) { 91 | ForEach(MyInputSources.inputSources) { inputSource in 92 | HStack { 93 | Button("Switch to \(inputSource.name)") { 94 | print("[Button] Switch to \(inputSource.name) Clicked") 95 | 96 | MyInputSources.SwitchInputSource(to: inputSource.name) 97 | } 98 | 99 | KeyboardShortcuts.Recorder(for: KeyboardShortcuts.Name(inputSource.name)) 100 | .onAppear { 101 | KeyboardShortcuts.onKeyDown(for: KeyboardShortcuts.Name(inputSource.name)) { 102 | print("[KeyboardShortcuts] shortcut of \(inputSource.name) down") 103 | 104 | MyInputSources.SwitchInputSource(to: inputSource.name) 105 | } 106 | } 107 | } 108 | } 109 | } 110 | .padding() 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /InputSourceSwitcher/Applescripts/Applesripts.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | func UseApplescriptToGetSystemInputSourcesInMenubar() -> [InputSource] { 4 | let applesript = """ 5 | tell application "System Events" 6 | tell process "TextInputMenuAgent" 7 | get the name of menu item of menu 1 of menu bar item 1 of menu bar 2 8 | end tell 9 | end tell 10 | """ 11 | 12 | if let script = NSAppleScript(source: applesript) { 13 | var error: NSDictionary? 14 | let descriptor = script.executeAndReturnError(&error) 15 | /// descriptor: 16 | if let err = error { 17 | print("[Applescript] NSAppleScript.executeAndReturnError(): \(err)") 18 | } else { 19 | var currentInputSources: [InputSource] = [] 20 | for i in 1 ... descriptor.numberOfItems { 21 | if let inputSource = descriptor.atIndex(i)?.stringValue { 22 | currentInputSources.append(InputSource(name: inputSource, id: i)) 23 | } else { 24 | print("[Applescript] Got \(currentInputSources.count) input sources from menu bar.") 25 | return currentInputSources 26 | } 27 | } 28 | } 29 | } else { 30 | print("[Applescript] NSAppleScript.init()") 31 | } 32 | return [] 33 | } 34 | 35 | func UseApplescriptToSwitchInputSource(to inputSourceName: String) { 36 | let applesript = """ 37 | ignoring application responses 38 | tell application "System Events" 39 | click menu bar item 1 of menu bar 2 of application process "TextInputMenuAgent" of application "System Events" 40 | end tell 41 | end ignoring 42 | 43 | delay 0.1 44 | do shell script "killall 'System Events'" 45 | delay 0.1 46 | 47 | tell application "System Events" 48 | launch 49 | click menu item "\(inputSourceName)" of menu 1 of menu bar item 1 of menu bar 2 of application process "TextInputMenuAgent" of application "System Events" 50 | end tell 51 | """ 52 | 53 | if let script = NSAppleScript(source: applesript) { 54 | var error: NSDictionary? 55 | 56 | script.executeAndReturnError(&error) 57 | 58 | if let err = error { 59 | print("[Applescript] NSAppleScript.executeAndReturnError(): \(err)") 60 | } else { 61 | // Successfully switched. 62 | PushNotification_DidSwitchInputSource(to: inputSourceName) 63 | } 64 | } else { 65 | print("[Applescript] NSAppleScript.init()") 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /InputSourceSwitcher/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /InputSourceSwitcher/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon16.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "icon32-1.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "icon32.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "icon64.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "icon128.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "icon256-1.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "icon256.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "icon512-1.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "icon512.png", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "icon1024.png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /InputSourceSwitcher/Assets.xcassets/AppIcon.appiconset/icon1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yang-Xijie/InputSourceSwitcher/357d2803f412361bfb2f42032f8117c8b79873f2/InputSourceSwitcher/Assets.xcassets/AppIcon.appiconset/icon1024.png -------------------------------------------------------------------------------- /InputSourceSwitcher/Assets.xcassets/AppIcon.appiconset/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yang-Xijie/InputSourceSwitcher/357d2803f412361bfb2f42032f8117c8b79873f2/InputSourceSwitcher/Assets.xcassets/AppIcon.appiconset/icon128.png -------------------------------------------------------------------------------- /InputSourceSwitcher/Assets.xcassets/AppIcon.appiconset/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yang-Xijie/InputSourceSwitcher/357d2803f412361bfb2f42032f8117c8b79873f2/InputSourceSwitcher/Assets.xcassets/AppIcon.appiconset/icon16.png -------------------------------------------------------------------------------- /InputSourceSwitcher/Assets.xcassets/AppIcon.appiconset/icon256-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yang-Xijie/InputSourceSwitcher/357d2803f412361bfb2f42032f8117c8b79873f2/InputSourceSwitcher/Assets.xcassets/AppIcon.appiconset/icon256-1.png -------------------------------------------------------------------------------- /InputSourceSwitcher/Assets.xcassets/AppIcon.appiconset/icon256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yang-Xijie/InputSourceSwitcher/357d2803f412361bfb2f42032f8117c8b79873f2/InputSourceSwitcher/Assets.xcassets/AppIcon.appiconset/icon256.png -------------------------------------------------------------------------------- /InputSourceSwitcher/Assets.xcassets/AppIcon.appiconset/icon32-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yang-Xijie/InputSourceSwitcher/357d2803f412361bfb2f42032f8117c8b79873f2/InputSourceSwitcher/Assets.xcassets/AppIcon.appiconset/icon32-1.png -------------------------------------------------------------------------------- /InputSourceSwitcher/Assets.xcassets/AppIcon.appiconset/icon32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yang-Xijie/InputSourceSwitcher/357d2803f412361bfb2f42032f8117c8b79873f2/InputSourceSwitcher/Assets.xcassets/AppIcon.appiconset/icon32.png -------------------------------------------------------------------------------- /InputSourceSwitcher/Assets.xcassets/AppIcon.appiconset/icon512-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yang-Xijie/InputSourceSwitcher/357d2803f412361bfb2f42032f8117c8b79873f2/InputSourceSwitcher/Assets.xcassets/AppIcon.appiconset/icon512-1.png -------------------------------------------------------------------------------- /InputSourceSwitcher/Assets.xcassets/AppIcon.appiconset/icon512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yang-Xijie/InputSourceSwitcher/357d2803f412361bfb2f42032f8117c8b79873f2/InputSourceSwitcher/Assets.xcassets/AppIcon.appiconset/icon512.png -------------------------------------------------------------------------------- /InputSourceSwitcher/Assets.xcassets/AppIcon.appiconset/icon64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yang-Xijie/InputSourceSwitcher/357d2803f412361bfb2f42032f8117c8b79873f2/InputSourceSwitcher/Assets.xcassets/AppIcon.appiconset/icon64.png -------------------------------------------------------------------------------- /InputSourceSwitcher/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /InputSourceSwitcher/Auxiliary/Pirnt+.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public func print(_ items: Any..., separator: String = " ", terminator: String = "\n") { 4 | let time: String = "[\(DateFormatter.localizedString(from: Date(), dateStyle: .short, timeStyle: .medium))] " 5 | 6 | var output = items.map { "\($0)" }.joined(separator: separator) 7 | output = time + output 8 | 9 | Swift.print(output, terminator: terminator) 10 | } 11 | -------------------------------------------------------------------------------- /InputSourceSwitcher/DataStructures/InputSource.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct InputSource: Identifiable, Encodable, Decodable { 4 | var name: String = "" 5 | var id: Int = 0 6 | } 7 | 8 | extension InputSource: CustomStringConvertible { 9 | var description: String { 10 | return name 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /InputSourceSwitcher/DataStructures/InputSources.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import KeyboardShortcuts 3 | 4 | struct InputSources { 5 | var inputSources: [InputSource] = [] 6 | let inputSources_key: String = "InputSources" 7 | 8 | // MARK: - init 9 | 10 | init() { 11 | if UserDefaults.isFirstLaunch() { 12 | LoadNewInputSourcesFromSystem() 13 | // FIXME: If you don't give `Accessiblity` to this app in `System Preferences -> Security` before you open this app for the first time, then no input source will appear. User can `Reset` to solve that problem. 14 | } else { 15 | // when the app restart, use Update() to clear unwanted global shortcuts 16 | Update() 17 | } 18 | } 19 | 20 | // Once `inputSources` changes, call this function. 21 | func StoreInputSourcesToUserDefault() { 22 | do { 23 | let data = try JSONEncoder().encode(inputSources) 24 | UserDefaults.standard.set(data, forKey: inputSources_key) // save inputSources 25 | } catch { 26 | print("[StoreInputSourcesToUserDefault Error] \(error)") 27 | } 28 | } 29 | 30 | func GetInputSourcesFromUserDefaults() -> [InputSource] { 31 | do { 32 | let storedData = UserDefaults.standard.data(forKey: inputSources_key) // get inputSources 33 | let arr = try JSONDecoder().decode([InputSource].self, from: storedData!) 34 | return arr 35 | } catch { 36 | print("[GetInputSourcesFromUserDefaults Error] \(error)") 37 | return [] 38 | } 39 | } 40 | 41 | // MARK: - functions that change `inputSources` 42 | 43 | /// Update currentInputSources in model. 44 | mutating func LoadNewInputSourcesFromSystem() { 45 | inputSources = GetNewInputSourcesFromSystem() 46 | StoreInputSourcesToUserDefault() 47 | } 48 | 49 | /// `Update`: **re-get** Input Sources from system and **preserve** shortcut settings at the same time. 50 | mutating func Update() { 51 | // [Analysis] 52 | // [user did't change System Input Sources] 53 | // just refresh inputSources 54 | // [user added System Input Sources] 55 | // just refresh inputSources 56 | // [user removes System Input Sources] 57 | // Firstly, find the removed Input Sources 58 | // Then, use their names to reset their shortcuts (to avoid situation of unwanted global shortcuts) 59 | // Next, refresh inputSources 60 | 61 | // Firstly, find the removed Input Sources 62 | let originalInputSources: [InputSource] = GetInputSourcesFromUserDefaults() 63 | let newInputSources: [InputSource] = GetNewInputSourcesFromSystem() 64 | 65 | var original: [String] = [] 66 | for item in originalInputSources { 67 | original.append(item.name) 68 | } 69 | 70 | var new: [String] = [] 71 | for item in newInputSources { 72 | new.append(item.name) 73 | } 74 | 75 | let inputSourcesToResetShortcut: Set = Set(original).subtracting(Set(new)) 76 | 77 | // print("[Model Debug]\n\toriginalInputSources:\(originalInputSources)\n\tnewInputSources:\(newInputSources)\n\tinputSourcesToResetShortcut:\(inputSourcesToResetShortcut)") 78 | 79 | // Then, use their names to reset their shortcuts 80 | 81 | if inputSourcesToResetShortcut != Set() { 82 | for inputSource in inputSourcesToResetShortcut { 83 | KeyboardShortcuts.reset(KeyboardShortcuts.Name(inputSource)) 84 | print("[KeyboardShortcuts] reset \(inputSource)") 85 | } 86 | } 87 | 88 | // Next, refresh inputSources 89 | LoadNewInputSourcesFromSystem() 90 | } 91 | 92 | /// `Reset`: **reset** the shortcut settings and **re-get** Input Sources from system. 93 | mutating func Reset() { 94 | for inputSource in inputSources { 95 | KeyboardShortcuts.reset(KeyboardShortcuts.Name(inputSource.name)) 96 | print("[KeyboardShortcuts] reset \(inputSource.name)") 97 | } 98 | 99 | LoadNewInputSourcesFromSystem() 100 | } 101 | 102 | // MARK: - functions concerning Applescript 103 | 104 | func GetNewInputSourcesFromSystem() -> [InputSource] { 105 | return UseApplescriptToGetSystemInputSourcesInMenubar() 106 | } 107 | 108 | func SwitchInputSource(to inputSourceName: String) { 109 | UseApplescriptToSwitchInputSource(to: inputSourceName) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /InputSourceSwitcher/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /InputSourceSwitcher/Tools/Notificatioins.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | import UserNotifications 4 | 5 | func PushNotification_DidSwitchInputSource(to inputsourceName: String) { 6 | // Create the notification and setup information 7 | let content = UNMutableNotificationContent() 8 | content.title = "\(inputsourceName)" 9 | content.body = "Successfully switched." 10 | 11 | // Create the request 12 | let uuidString = UUID().uuidString // TODO: not clear 13 | let request = UNNotificationRequest( 14 | identifier: uuidString, 15 | content: content, 16 | trigger: .none) 17 | 18 | let notificationCenter = UNUserNotificationCenter.current() 19 | notificationCenter.add(request) { error in 20 | if error != nil { 21 | print("[PushNotification_DidSwitchInputSource] Error") 22 | } else { 23 | // Successfully 24 | print("[PushNotification_DidSwitchInputSource] Successfully pushed.") 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /InputSourceSwitcher/Tools/Shortcuts.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // Swift Package https://github.com/sindresorhus/KeyboardShortcuts 4 | // LICENSE https://github.com/sindresorhus/KeyboardShortcuts/blob/main/license 5 | -------------------------------------------------------------------------------- /InputSourceSwitcher/Tools/VersionAndBuildNumber.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | let appVersionString: String = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String 4 | let buildNumber: String = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as! String 5 | 6 | extension UserDefaults { 7 | // 应用第一次启动 app starts for the first time 8 | static func isFirstLaunch() -> Bool { 9 | let hasBeenLaunched = "hasBeenLaunched" 10 | let isFirstLaunch = !UserDefaults.standard.bool(forKey: hasBeenLaunched) 11 | if isFirstLaunch { 12 | UserDefaults.standard.set(true, forKey: hasBeenLaunched) 13 | UserDefaults.standard.synchronize() 14 | } 15 | return isFirstLaunch 16 | } 17 | 18 | // 当前版本第一次启动 the new version app starts 19 | static func isFirstLaunchOfNewVersion() -> Bool { 20 | // 主程序版本号 version of current app 21 | let infoDictionary = Bundle.main.infoDictionary! 22 | let majorVersion = infoDictionary["CFBundleShortVersionString"] as! String 23 | 24 | // 上次启动的版本号 last launch version 25 | let hasBeenLaunchedOfNewVersion = "hasBeenLaunchedOfNewVersion" 26 | let lastLaunchVersion = UserDefaults.standard.string(forKey: 27 | hasBeenLaunchedOfNewVersion) 28 | 29 | // 版本号比较 compare two versions 30 | let isFirstLaunchOfNewVersion = majorVersion != lastLaunchVersion 31 | if isFirstLaunchOfNewVersion { 32 | UserDefaults.standard.set(majorVersion, forKey: 33 | hasBeenLaunchedOfNewVersion) 34 | UserDefaults.standard.synchronize() 35 | } 36 | return isFirstLaunchOfNewVersion 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /InputSourceSwitcher/ViewModel/InputSourceModel.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import KeyboardShortcuts 3 | 4 | class InputSourcesModel: ObservableObject { 5 | // MARK: - Create Model 6 | 7 | @Published private var model: InputSources = InputSourcesModel.createMyInputSources() 8 | 9 | private static func createMyInputSources() -> InputSources { 10 | print("[Model] createMyInputSources()") 11 | 12 | return InputSources() 13 | } 14 | 15 | // MARK: - Access to Data in Model 16 | 17 | var inputSources: [InputSource] { 18 | model.inputSources 19 | } 20 | 21 | // MARK: - Deal with Intents from View 22 | 23 | /// `Reset`: **reset** the shortcut settings and **re-get** Input Sources from system. 24 | func Reset() { 25 | model.Reset() 26 | 27 | print("[Model] Reset()") 28 | } 29 | 30 | /// `Update`: **re-get** Input Sources from system and **preserve** shortcut settings at the same time. 31 | func Update() { 32 | model.Update() 33 | 34 | print("[Model] Update()") 35 | } 36 | 37 | func SwitchInputSource(to inputSourceName: String) { 38 | model.SwitchInputSource(to: inputSourceName) 39 | 40 | print("[Model] Switch to InputSource\(inputSourceName)") 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /InputSourceSwitcher/macOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleURLTypes 20 | 21 | 22 | CFBundleTypeRole 23 | Viewer 24 | CFBundleURLName 25 | com.yangxijie.SourceSwitcher 26 | CFBundleURLSchemes 27 | 28 | SourceSwitcherAbout 29 | 30 | 31 | 32 | CFBundleTypeRole 33 | Viewer 34 | CFBundleURLName 35 | com.yangxijie.SourceSwitcher 36 | CFBundleURLSchemes 37 | 38 | SourceSwitcherHelp 39 | 40 | 41 | 42 | CFBundleVersion 43 | $(CURRENT_PROJECT_VERSION) 44 | LSApplicationCategoryType 45 | public.app-category.productivity 46 | LSMinimumSystemVersion 47 | $(MACOSX_DEPLOYMENT_TARGET) 48 | LSUIElement 49 | 50 | NSAppleEventsUsageDescription 51 | Do you allow automation for that App? 52 | NSSupportsAutomaticTermination 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /InputSourceSwitcher/macOS/InputSourceSwitcher.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.temporary-exception.apple-events 6 | 7 | com.apple.security.automation.apple-events 8 | 9 | com.apple.security.app-sandbox 10 | 11 | com.apple.security.files.user-selected.read-only 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Yang-Xijie 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NOTICE 2 | 3 | I have **stopped maintaining** this project because Applescript not works as before in `macOS 12`. 4 | 5 | ## macOS 11, Big Sur 6 | 7 | On `macOS 11`, Applescript below **worked well** with little latency: 8 | 9 | ```applescript 10 | tell application "System Events" 11 | tell process "TextInputMenuAgent" 12 | click menu item "\(inputSourceName)" of menu 1 of menu bar item 1 of menu bar 2 13 | click menu bar item 1 of menu bar 2 14 | end tell 15 | end tell 16 | ``` 17 | 18 | The corresponding release and commit are: 19 | 20 | https://github.com/Yang-Xijie/InputSourceSwitcher/releases/tag/v1.2 21 | 22 | d5f309e678f8e97d83da95e6facc5b4eada6b501 23 | 24 | ## macOS 12, Monterey 25 | 26 | When macOS 12 came, I tried to update this project. However, it seems that `click` in Applescript not works as expected. I made several commits on Oct 30, 2021 using a new Applescript. However, I should say it just works sometimes, not always realiable. 27 | 28 | ```applescript 29 | ignoring application responses 30 | tell application "System Events" 31 | click menu bar item 1 of menu bar 2 of application process "TextInputMenuAgent" of application "System Events" 32 | end tell 33 | end ignoring 34 | 35 | delay 0.1 36 | do shell script "killall 'System Events'" 37 | delay 0.1 38 | 39 | tell application "System Events" 40 | launch 41 | click menu item "\(inputSourceName)" of menu 1 of menu bar item 1 of menu bar 2 of application process "TextInputMenuAgent" of application "System Events" 42 | end tell 43 | ``` 44 | 45 | Corresponding release and commit are: 46 | 47 | https://github.com/Yang-Xijie/InputSourceSwitcher/releases/tag/v1.3 48 | 49 | 5f2b73d7c315f5900da1b76d04b8b3e9808d6716 50 | 51 | # Source Switcher 52 | 53 | A **menu bar app** to **change input sources** swiftly using **shortcuts** on *macOS Big Sur and later*. 54 | 55 | View [screenshots](https://github.com/Yang-Xijie/InputSourceSwitcher/discussions/11) 56 | 57 | ## Introduction 58 | 59 | This app uses `Applescript` to manipulate `UI operation` which is better than using `Carbon` (a framework most of whose APIs are deprecated) in other tools to change input sources. 60 | 61 | ## Installation / Usage 62 | 63 | Download the release version and drop `Source Switcher.app` into your `Applications` folder. (may not be the latest version) 64 | 65 | **Or** clone this repo and build it on your Mac. 66 | 67 | **Notice**: You should go to `System Preferences -> Security & Privacy -> Privacy -> Accessiblity` to make `Source Switcher` able to execute Applescript on your Mac. 68 | 69 | 70 | Then go to `System Preferences -> Security & Privacy -> Privacy -> Automation` to select `Source Switcher.app` and *Tick the box* of `System Envent.app`. 71 | 72 | You'd better perform this operation before you open this app for the first time. If no input source appears, just lick `Reset` to get your current input sources. 73 | 74 | **Notice**: If you get `Source Switcher.app can't be opened because Apple cannot check it for malicious software`, please open your `Applications` folder, find `Source Switcher.app` and `right click -> Open` to open the app. 75 | 76 | ## Background 77 | 78 | Search "input source mac" on GitHub and there are plenty of repositories such as [hnakamur / inputsource](https://github.com/hnakamur/inputsource) and [minoki / InputSourceSelector](https://github.com/minoki/InputSourceSelector). They are command line softwares, and you can easily use `Alfred` to add shortcuts to switch input sources conveniently. However, they have a common problem: when it comes to CJK such as Japanese, they don't work. (check [InputSourceSelector issues #2](https://github.com/minoki/InputSourceSelector/issues/2) (Japanese) and [stackoverflow questions #22885767](https://stackoverflow.com/questions/22885767/how-to-programmatically-switch-an-input-method-on-os-x) (English) to get more details) 79 | 80 | After some analysis, I find that they all use `Carbon Core`, most of whose APIs are deprecated after `macOS 10.8`, and use the `input source id` (eg. `com.apple.inputmethod.Kotoeri.RomajiTyping.Japanese`) to identify different input sources. 81 | 82 | [laishulu / macism](https://github.com/laishulu/macism) says it solves the problem. I'm not sure whether it works: it uses Carbon APIs and just stops for some time to deal with that problem... 83 | 84 | Then how to solve it? The fact is, if we uses some mature exposed APIs provided by the system, issues not appear! To be concrete, we click the control center in the menu bar or switch input sources in System Preferences instead of calling macOS APIs. They are kind of `UI things`. 85 | 86 | So one way is what [hatashiro / kawa](https://github.com/hatashiro/kawa) uses, which really solves the problem by using a method in `System Preferences -> Keyboard -> Shortcuts -> Input Sources -> Select the previous input source`. But issue still exists: if you change input sources quickly, you will find [kawa](https://github.com/hatashiro/kawa) doesn't work. This is because `Select the previous input source` has different functions when triggering the shortcut (⌃␣ if you haven't changed it) at a different speed. [Kawa](https://github.com/hatashiro/kawa) will work (as far as I consider) if change `Select the previous input source` to `Select next source in input menu` (see [kawa pull #21](https://github.com/hatashiro/kawa/pull/21)). By the way, this project also uses Carbon APIs and this repo closes issues... 87 | 88 | **Innovatively, I use a completely _UI_ way to solve all these problems.** Referring to [Geoff Taylor | Scripting the menu bar in macOS Big Sur](https://www.geofftaylor.me/2020/scripting-the-menu-bar-in-macos-big-sur/), I use [UI Browser](https://pfiddlesoft.com/uibrowser/index.html) to find how to use Applesript to express the Input Source Panel in the menu bar (Control Center). Then refer to [Chris J. | How to launch an AppleScript from AppKit on Catalina with Swift](https://medium.com/macoclock/everything-you-need-to-do-to-launch-an-applescript-from-appkit-on-macos-catalina-with-swift-1ba82537f7c3) to use Applescript in macOS app (*which has a drawback that sandbox is disabled*). After getting the prototype, I design the UI of this app, make this app a menu bar app, add shortcuts to it and better its code logic to avoid some potential bugs. 89 | 90 | ## Referenced Open Source Projects 91 | 92 | [menubarpopoverswiftui2](https://github.com/zaferarican/menubarpopoverswiftui2) - [LICENSE](https://github.com/zaferarican/menubarpopoverswiftui2/blob/master/LICENSE) 93 | 94 | [KeyboardShortcuts](https://github.com/sindresorhus/KeyboardShortcuts) - [LICENSE](https://github.com/sindresorhus/KeyboardShortcuts/blob/main/license) 95 | 96 | ## About 97 | 98 | Discuss about this app in the `Discussion` part. Check current desired features in `Projects -> TODO`. 99 | 100 | Any `issue` or `pull request` are welcome! 101 | 102 | If you find this tiny app improving your efficency, feel free to [buy me a cup of coffee](https://yang-xijie.github.io/SITE/postscript/support.html) using `WeChat Pay` or `Alipay`. 103 | -------------------------------------------------------------------------------- /UpdateLog.md: -------------------------------------------------------------------------------- 1 | # Update Log 2 | 3 | ## v1.3 4 | 5 | * Update the applescript for `macOS 12 Monterey` to change input sources in `Applescripts.swift`. The new method is a little bit slower than before. Check more at . 6 | --------------------------------------------------------------------------------