├── .gitignore ├── PearHID-Helper └── com.alienator88.PearHID.Helper.plist ├── PearHID.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcshareddata │ │ └── swiftpm │ │ │ └── Package.resolved │ └── xcuserdata │ │ └── alin.xcuserdatad │ │ └── IDEFindNavigatorScopes.plist ├── xcshareddata │ └── xcschemes │ │ └── PearHID - Debug.xcscheme └── xcuserdata │ └── alin.xcuserdatad │ └── xcschemes │ ├── PearHID - Release.xcscheme │ └── xcschememanagement.plist ├── PearHID ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── icon_128x128.png │ │ ├── icon_16x16.png │ │ ├── icon_256x256.png │ │ ├── icon_32x32.png │ │ ├── icon_32x32@2x.png │ │ └── icon_512x512.png │ └── Contents.json ├── ContentView.swift ├── KeyManager.swift ├── Keys.swift ├── Metal │ ├── LavaLampShader.metal │ └── LavaLampView.swift ├── PearHIDApp.swift ├── Settings │ ├── SettingsView.swift │ └── UpdateView.swift ├── Styles.swift └── Utilities.swift ├── PearHIDHelper ├── com.alienator88.PearHID.Helper.plist └── main.swift ├── README.md ├── Resources └── Icons │ ├── icon_128x128.png │ ├── icon_128x128@2x.png │ ├── icon_16x16.png │ ├── icon_16x16@2x.png │ ├── icon_256x256.png │ ├── icon_256x256@2x.png │ ├── icon_32x32.png │ ├── icon_32x32@2x.png │ ├── icon_512x512.png │ └── icon_512x512@2x.png └── announcements.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ 91 | .DS_Store 92 | Builds/ 93 | -------------------------------------------------------------------------------- /PearHID-Helper/com.alienator88.PearHID.Helper.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Label 6 | com.alienator88.PearHID.Helper 7 | BundleProgram 8 | Contents/MacOS/PearHIDHelper 9 | MachServices 10 | 11 | com.alienator88.PearHID.Helper 12 | 13 | 14 | AssociatedBundleIdentifiers 15 | 16 | com.alienator88.PearHID 17 | 18 | 19 | -------------------------------------------------------------------------------- /PearHID.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 77; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | C701EEE32DA0A35100907AC6 /* PearHIDHelper in CopyFiles */ = {isa = PBXBuildFile; fileRef = C701EECF2DA0A15C00907AC6 /* PearHIDHelper */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 11 | C784F8052D15FA450005ABA6 /* AlinFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = C784F8042D15FA450005ABA6 /* AlinFoundation */; }; 12 | /* End PBXBuildFile section */ 13 | 14 | /* Begin PBXCopyFilesBuildPhase section */ 15 | C701EECD2DA0A15C00907AC6 /* CopyFiles */ = { 16 | isa = PBXCopyFilesBuildPhase; 17 | buildActionMask = 2147483647; 18 | dstPath = /usr/share/man/man1/; 19 | dstSubfolderSpec = 0; 20 | files = ( 21 | ); 22 | runOnlyForDeploymentPostprocessing = 1; 23 | }; 24 | C701EEDE2DA0A32100907AC6 /* CopyFiles */ = { 25 | isa = PBXCopyFilesBuildPhase; 26 | buildActionMask = 2147483647; 27 | dstPath = Contents/Library/LaunchDaemons; 28 | dstSubfolderSpec = 1; 29 | files = ( 30 | ); 31 | runOnlyForDeploymentPostprocessing = 0; 32 | }; 33 | C701EEE22DA0A34A00907AC6 /* CopyFiles */ = { 34 | isa = PBXCopyFilesBuildPhase; 35 | buildActionMask = 2147483647; 36 | dstPath = ""; 37 | dstSubfolderSpec = 6; 38 | files = ( 39 | C701EEE32DA0A35100907AC6 /* PearHIDHelper in CopyFiles */, 40 | ); 41 | runOnlyForDeploymentPostprocessing = 0; 42 | }; 43 | /* End PBXCopyFilesBuildPhase section */ 44 | 45 | /* Begin PBXFileReference section */ 46 | C701EECF2DA0A15C00907AC6 /* PearHIDHelper */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = PearHIDHelper; sourceTree = BUILT_PRODUCTS_DIR; }; 47 | C702B0F92DA0B707007DCDF0 /* changes.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = changes.md; sourceTree = ""; }; 48 | C702B0FA2DA0B707007DCDF0 /* ExportOptions.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = ExportOptions.plist; sourceTree = ""; }; 49 | C784F7EB2D15D4AE0005ABA6 /* PearHID.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PearHID.app; sourceTree = BUILT_PRODUCTS_DIR; }; 50 | /* End PBXFileReference section */ 51 | 52 | /* Begin PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet section */ 53 | C71968B92DA488AE00FA2FA5 /* Exceptions for "PearHIDHelper" folder in "Copy Files" phase from "PearHID" target */ = { 54 | isa = PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet; 55 | buildPhase = C701EEDE2DA0A32100907AC6 /* CopyFiles */; 56 | membershipExceptions = ( 57 | com.alienator88.PearHID.Helper.plist, 58 | ); 59 | }; 60 | /* End PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet section */ 61 | 62 | /* Begin PBXFileSystemSynchronizedRootGroup section */ 63 | C701EED02DA0A15C00907AC6 /* PearHIDHelper */ = { 64 | isa = PBXFileSystemSynchronizedRootGroup; 65 | exceptions = ( 66 | C71968B92DA488AE00FA2FA5 /* Exceptions for "PearHIDHelper" folder in "Copy Files" phase from "PearHID" target */, 67 | ); 68 | path = PearHIDHelper; 69 | sourceTree = ""; 70 | }; 71 | C784F7ED2D15D4AE0005ABA6 /* PearHID */ = { 72 | isa = PBXFileSystemSynchronizedRootGroup; 73 | path = PearHID; 74 | sourceTree = ""; 75 | }; 76 | /* End PBXFileSystemSynchronizedRootGroup section */ 77 | 78 | /* Begin PBXFrameworksBuildPhase section */ 79 | C701EECC2DA0A15C00907AC6 /* Frameworks */ = { 80 | isa = PBXFrameworksBuildPhase; 81 | buildActionMask = 2147483647; 82 | files = ( 83 | ); 84 | runOnlyForDeploymentPostprocessing = 0; 85 | }; 86 | C784F7E82D15D4AE0005ABA6 /* Frameworks */ = { 87 | isa = PBXFrameworksBuildPhase; 88 | buildActionMask = 2147483647; 89 | files = ( 90 | C784F8052D15FA450005ABA6 /* AlinFoundation in Frameworks */, 91 | ); 92 | runOnlyForDeploymentPostprocessing = 0; 93 | }; 94 | /* End PBXFrameworksBuildPhase section */ 95 | 96 | /* Begin PBXGroup section */ 97 | C702B0F82DA0B707007DCDF0 /* Export */ = { 98 | isa = PBXGroup; 99 | children = ( 100 | ); 101 | path = Export; 102 | sourceTree = ""; 103 | }; 104 | C702B0FB2DA0B707007DCDF0 /* Builds */ = { 105 | isa = PBXGroup; 106 | children = ( 107 | C702B0F82DA0B707007DCDF0 /* Export */, 108 | C702B0F92DA0B707007DCDF0 /* changes.md */, 109 | C702B0FA2DA0B707007DCDF0 /* ExportOptions.plist */, 110 | ); 111 | path = Builds; 112 | sourceTree = ""; 113 | }; 114 | C784F7E22D15D4AE0005ABA6 = { 115 | isa = PBXGroup; 116 | children = ( 117 | C702B0FB2DA0B707007DCDF0 /* Builds */, 118 | C784F7ED2D15D4AE0005ABA6 /* PearHID */, 119 | C701EED02DA0A15C00907AC6 /* PearHIDHelper */, 120 | C784F7EC2D15D4AE0005ABA6 /* Products */, 121 | ); 122 | sourceTree = ""; 123 | }; 124 | C784F7EC2D15D4AE0005ABA6 /* Products */ = { 125 | isa = PBXGroup; 126 | children = ( 127 | C784F7EB2D15D4AE0005ABA6 /* PearHID.app */, 128 | C701EECF2DA0A15C00907AC6 /* PearHIDHelper */, 129 | ); 130 | name = Products; 131 | sourceTree = ""; 132 | }; 133 | /* End PBXGroup section */ 134 | 135 | /* Begin PBXNativeTarget section */ 136 | C701EECE2DA0A15C00907AC6 /* PearHIDHelper */ = { 137 | isa = PBXNativeTarget; 138 | buildConfigurationList = C701EED32DA0A15C00907AC6 /* Build configuration list for PBXNativeTarget "PearHIDHelper" */; 139 | buildPhases = ( 140 | C701EECB2DA0A15C00907AC6 /* Sources */, 141 | C701EECC2DA0A15C00907AC6 /* Frameworks */, 142 | C701EECD2DA0A15C00907AC6 /* CopyFiles */, 143 | ); 144 | buildRules = ( 145 | ); 146 | dependencies = ( 147 | ); 148 | fileSystemSynchronizedGroups = ( 149 | C701EED02DA0A15C00907AC6 /* PearHIDHelper */, 150 | ); 151 | name = PearHIDHelper; 152 | packageProductDependencies = ( 153 | ); 154 | productName = "PearHID-Helper"; 155 | productReference = C701EECF2DA0A15C00907AC6 /* PearHIDHelper */; 156 | productType = "com.apple.product-type.tool"; 157 | }; 158 | C784F7EA2D15D4AE0005ABA6 /* PearHID */ = { 159 | isa = PBXNativeTarget; 160 | buildConfigurationList = C784F7FA2D15D4AF0005ABA6 /* Build configuration list for PBXNativeTarget "PearHID" */; 161 | buildPhases = ( 162 | C784F7E72D15D4AE0005ABA6 /* Sources */, 163 | C784F7E82D15D4AE0005ABA6 /* Frameworks */, 164 | C784F7E92D15D4AE0005ABA6 /* Resources */, 165 | C701EEDE2DA0A32100907AC6 /* CopyFiles */, 166 | C701EEE22DA0A34A00907AC6 /* CopyFiles */, 167 | ); 168 | buildRules = ( 169 | ); 170 | dependencies = ( 171 | ); 172 | fileSystemSynchronizedGroups = ( 173 | C784F7ED2D15D4AE0005ABA6 /* PearHID */, 174 | ); 175 | name = PearHID; 176 | packageProductDependencies = ( 177 | C784F8042D15FA450005ABA6 /* AlinFoundation */, 178 | ); 179 | productName = PearHID; 180 | productReference = C784F7EB2D15D4AE0005ABA6 /* PearHID.app */; 181 | productType = "com.apple.product-type.application"; 182 | }; 183 | /* End PBXNativeTarget section */ 184 | 185 | /* Begin PBXProject section */ 186 | C784F7E32D15D4AE0005ABA6 /* Project object */ = { 187 | isa = PBXProject; 188 | attributes = { 189 | BuildIndependentTargetsInParallel = 1; 190 | LastSwiftUpdateCheck = 1630; 191 | LastUpgradeCheck = 1630; 192 | TargetAttributes = { 193 | C701EECE2DA0A15C00907AC6 = { 194 | CreatedOnToolsVersion = 16.3; 195 | }; 196 | C784F7EA2D15D4AE0005ABA6 = { 197 | CreatedOnToolsVersion = 16.2; 198 | }; 199 | }; 200 | }; 201 | buildConfigurationList = C784F7E62D15D4AE0005ABA6 /* Build configuration list for PBXProject "PearHID" */; 202 | developmentRegion = en; 203 | hasScannedForEncodings = 0; 204 | knownRegions = ( 205 | en, 206 | Base, 207 | ); 208 | mainGroup = C784F7E22D15D4AE0005ABA6; 209 | minimizedProjectReferenceProxies = 1; 210 | packageReferences = ( 211 | C784F8032D15FA450005ABA6 /* XCRemoteSwiftPackageReference "AlinFoundation" */, 212 | ); 213 | preferredProjectObjectVersion = 77; 214 | productRefGroup = C784F7EC2D15D4AE0005ABA6 /* Products */; 215 | projectDirPath = ""; 216 | projectRoot = ""; 217 | targets = ( 218 | C784F7EA2D15D4AE0005ABA6 /* PearHID */, 219 | C701EECE2DA0A15C00907AC6 /* PearHIDHelper */, 220 | ); 221 | }; 222 | /* End PBXProject section */ 223 | 224 | /* Begin PBXResourcesBuildPhase section */ 225 | C784F7E92D15D4AE0005ABA6 /* Resources */ = { 226 | isa = PBXResourcesBuildPhase; 227 | buildActionMask = 2147483647; 228 | files = ( 229 | ); 230 | runOnlyForDeploymentPostprocessing = 0; 231 | }; 232 | /* End PBXResourcesBuildPhase section */ 233 | 234 | /* Begin PBXSourcesBuildPhase section */ 235 | C701EECB2DA0A15C00907AC6 /* Sources */ = { 236 | isa = PBXSourcesBuildPhase; 237 | buildActionMask = 2147483647; 238 | files = ( 239 | ); 240 | runOnlyForDeploymentPostprocessing = 0; 241 | }; 242 | C784F7E72D15D4AE0005ABA6 /* Sources */ = { 243 | isa = PBXSourcesBuildPhase; 244 | buildActionMask = 2147483647; 245 | files = ( 246 | ); 247 | runOnlyForDeploymentPostprocessing = 0; 248 | }; 249 | /* End PBXSourcesBuildPhase section */ 250 | 251 | /* Begin XCBuildConfiguration section */ 252 | C701EED42DA0A15C00907AC6 /* Debug */ = { 253 | isa = XCBuildConfiguration; 254 | buildSettings = { 255 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application"; 256 | CODE_SIGN_STYLE = Manual; 257 | DEVELOPMENT_TEAM = ""; 258 | "DEVELOPMENT_TEAM[sdk=macosx*]" = BK8443AXLU; 259 | ENABLE_HARDENED_RUNTIME = YES; 260 | MACOSX_DEPLOYMENT_TARGET = 13.0; 261 | PRODUCT_BUNDLE_IDENTIFIER = com.alienator88.PearHID.Helper; 262 | PRODUCT_NAME = "$(TARGET_NAME)"; 263 | PROVISIONING_PROFILE_SPECIFIER = ""; 264 | SKIP_INSTALL = YES; 265 | SWIFT_VERSION = 5.0; 266 | }; 267 | name = Debug; 268 | }; 269 | C701EED52DA0A15C00907AC6 /* Release */ = { 270 | isa = XCBuildConfiguration; 271 | buildSettings = { 272 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application"; 273 | CODE_SIGN_STYLE = Manual; 274 | DEVELOPMENT_TEAM = ""; 275 | "DEVELOPMENT_TEAM[sdk=macosx*]" = BK8443AXLU; 276 | ENABLE_HARDENED_RUNTIME = YES; 277 | MACOSX_DEPLOYMENT_TARGET = 13.0; 278 | PRODUCT_BUNDLE_IDENTIFIER = com.alienator88.PearHID.Helper; 279 | PRODUCT_NAME = "$(TARGET_NAME)"; 280 | PROVISIONING_PROFILE_SPECIFIER = ""; 281 | SKIP_INSTALL = YES; 282 | SWIFT_VERSION = 5.0; 283 | }; 284 | name = Release; 285 | }; 286 | C784F7F82D15D4AF0005ABA6 /* Debug */ = { 287 | isa = XCBuildConfiguration; 288 | buildSettings = { 289 | ALWAYS_SEARCH_USER_PATHS = NO; 290 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 291 | CLANG_ANALYZER_NONNULL = YES; 292 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 293 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 294 | CLANG_ENABLE_MODULES = YES; 295 | CLANG_ENABLE_OBJC_ARC = YES; 296 | CLANG_ENABLE_OBJC_WEAK = YES; 297 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 298 | CLANG_WARN_BOOL_CONVERSION = YES; 299 | CLANG_WARN_COMMA = YES; 300 | CLANG_WARN_CONSTANT_CONVERSION = YES; 301 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 302 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 303 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 304 | CLANG_WARN_EMPTY_BODY = YES; 305 | CLANG_WARN_ENUM_CONVERSION = YES; 306 | CLANG_WARN_INFINITE_RECURSION = YES; 307 | CLANG_WARN_INT_CONVERSION = YES; 308 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 309 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 310 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 311 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 312 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 313 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 314 | CLANG_WARN_STRICT_PROTOTYPES = YES; 315 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 316 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 317 | CLANG_WARN_UNREACHABLE_CODE = YES; 318 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 319 | COPY_PHASE_STRIP = NO; 320 | DEAD_CODE_STRIPPING = YES; 321 | DEBUG_INFORMATION_FORMAT = dwarf; 322 | DEVELOPMENT_TEAM = BK8443AXLU; 323 | ENABLE_STRICT_OBJC_MSGSEND = YES; 324 | ENABLE_TESTABILITY = YES; 325 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 326 | GCC_C_LANGUAGE_STANDARD = gnu17; 327 | GCC_DYNAMIC_NO_PIC = NO; 328 | GCC_NO_COMMON_BLOCKS = YES; 329 | GCC_OPTIMIZATION_LEVEL = 0; 330 | GCC_PREPROCESSOR_DEFINITIONS = ( 331 | "DEBUG=1", 332 | "$(inherited)", 333 | ); 334 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 335 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 336 | GCC_WARN_UNDECLARED_SELECTOR = YES; 337 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 338 | GCC_WARN_UNUSED_FUNCTION = YES; 339 | GCC_WARN_UNUSED_VARIABLE = YES; 340 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 341 | MACOSX_DEPLOYMENT_TARGET = 15.1; 342 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 343 | MTL_FAST_MATH = YES; 344 | ONLY_ACTIVE_ARCH = YES; 345 | SDKROOT = macosx; 346 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 347 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 348 | }; 349 | name = Debug; 350 | }; 351 | C784F7F92D15D4AF0005ABA6 /* Release */ = { 352 | isa = XCBuildConfiguration; 353 | buildSettings = { 354 | ALWAYS_SEARCH_USER_PATHS = NO; 355 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 356 | CLANG_ANALYZER_NONNULL = YES; 357 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 358 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 359 | CLANG_ENABLE_MODULES = YES; 360 | CLANG_ENABLE_OBJC_ARC = YES; 361 | CLANG_ENABLE_OBJC_WEAK = YES; 362 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 363 | CLANG_WARN_BOOL_CONVERSION = YES; 364 | CLANG_WARN_COMMA = YES; 365 | CLANG_WARN_CONSTANT_CONVERSION = YES; 366 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 367 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 368 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 369 | CLANG_WARN_EMPTY_BODY = YES; 370 | CLANG_WARN_ENUM_CONVERSION = YES; 371 | CLANG_WARN_INFINITE_RECURSION = YES; 372 | CLANG_WARN_INT_CONVERSION = YES; 373 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 374 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 375 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 376 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 377 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 378 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 379 | CLANG_WARN_STRICT_PROTOTYPES = YES; 380 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 381 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 382 | CLANG_WARN_UNREACHABLE_CODE = YES; 383 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 384 | COPY_PHASE_STRIP = NO; 385 | DEAD_CODE_STRIPPING = YES; 386 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 387 | DEVELOPMENT_TEAM = BK8443AXLU; 388 | ENABLE_NS_ASSERTIONS = NO; 389 | ENABLE_STRICT_OBJC_MSGSEND = YES; 390 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 391 | GCC_C_LANGUAGE_STANDARD = gnu17; 392 | GCC_NO_COMMON_BLOCKS = YES; 393 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 394 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 395 | GCC_WARN_UNDECLARED_SELECTOR = YES; 396 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 397 | GCC_WARN_UNUSED_FUNCTION = YES; 398 | GCC_WARN_UNUSED_VARIABLE = YES; 399 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 400 | MACOSX_DEPLOYMENT_TARGET = 15.1; 401 | MTL_ENABLE_DEBUG_INFO = NO; 402 | MTL_FAST_MATH = YES; 403 | SDKROOT = macosx; 404 | SWIFT_COMPILATION_MODE = wholemodule; 405 | }; 406 | name = Release; 407 | }; 408 | C784F7FB2D15D4AF0005ABA6 /* Debug */ = { 409 | isa = XCBuildConfiguration; 410 | buildSettings = { 411 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 412 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 413 | CODE_SIGN_ENTITLEMENTS = ""; 414 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application"; 415 | CODE_SIGN_STYLE = Manual; 416 | COMBINE_HIDPI_IMAGES = YES; 417 | CURRENT_PROJECT_VERSION = 1; 418 | DEAD_CODE_STRIPPING = YES; 419 | DEVELOPMENT_ASSET_PATHS = ""; 420 | DEVELOPMENT_TEAM = ""; 421 | "DEVELOPMENT_TEAM[sdk=macosx*]" = BK8443AXLU; 422 | ENABLE_HARDENED_RUNTIME = YES; 423 | ENABLE_PREVIEWS = YES; 424 | GENERATE_INFOPLIST_FILE = YES; 425 | INFOPLIST_KEY_CFBundleDisplayName = PearHID; 426 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; 427 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 428 | LD_RUNPATH_SEARCH_PATHS = ( 429 | "$(inherited)", 430 | "@executable_path/../Frameworks", 431 | ); 432 | MACOSX_DEPLOYMENT_TARGET = 13.0; 433 | MARKETING_VERSION = 1.0.0; 434 | PRODUCT_BUNDLE_IDENTIFIER = com.alienator88.PearHID; 435 | PRODUCT_NAME = "$(TARGET_NAME)"; 436 | PROVISIONING_PROFILE_SPECIFIER = ""; 437 | "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = PearHID; 438 | SWIFT_EMIT_LOC_STRINGS = YES; 439 | SWIFT_VERSION = 5.0; 440 | }; 441 | name = Debug; 442 | }; 443 | C784F7FC2D15D4AF0005ABA6 /* Release */ = { 444 | isa = XCBuildConfiguration; 445 | buildSettings = { 446 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 447 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 448 | CODE_SIGN_ENTITLEMENTS = ""; 449 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application"; 450 | CODE_SIGN_STYLE = Manual; 451 | COMBINE_HIDPI_IMAGES = YES; 452 | CURRENT_PROJECT_VERSION = 1; 453 | DEAD_CODE_STRIPPING = YES; 454 | DEVELOPMENT_ASSET_PATHS = ""; 455 | DEVELOPMENT_TEAM = ""; 456 | "DEVELOPMENT_TEAM[sdk=macosx*]" = BK8443AXLU; 457 | ENABLE_HARDENED_RUNTIME = YES; 458 | ENABLE_PREVIEWS = YES; 459 | GENERATE_INFOPLIST_FILE = YES; 460 | INFOPLIST_KEY_CFBundleDisplayName = PearHID; 461 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; 462 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 463 | LD_RUNPATH_SEARCH_PATHS = ( 464 | "$(inherited)", 465 | "@executable_path/../Frameworks", 466 | ); 467 | MACOSX_DEPLOYMENT_TARGET = 13.0; 468 | MARKETING_VERSION = 1.0.0; 469 | PRODUCT_BUNDLE_IDENTIFIER = com.alienator88.PearHID; 470 | PRODUCT_NAME = "$(TARGET_NAME)"; 471 | PROVISIONING_PROFILE_SPECIFIER = ""; 472 | "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = PearHID; 473 | SWIFT_EMIT_LOC_STRINGS = YES; 474 | SWIFT_VERSION = 5.0; 475 | }; 476 | name = Release; 477 | }; 478 | /* End XCBuildConfiguration section */ 479 | 480 | /* Begin XCConfigurationList section */ 481 | C701EED32DA0A15C00907AC6 /* Build configuration list for PBXNativeTarget "PearHIDHelper" */ = { 482 | isa = XCConfigurationList; 483 | buildConfigurations = ( 484 | C701EED42DA0A15C00907AC6 /* Debug */, 485 | C701EED52DA0A15C00907AC6 /* Release */, 486 | ); 487 | defaultConfigurationIsVisible = 0; 488 | defaultConfigurationName = Release; 489 | }; 490 | C784F7E62D15D4AE0005ABA6 /* Build configuration list for PBXProject "PearHID" */ = { 491 | isa = XCConfigurationList; 492 | buildConfigurations = ( 493 | C784F7F82D15D4AF0005ABA6 /* Debug */, 494 | C784F7F92D15D4AF0005ABA6 /* Release */, 495 | ); 496 | defaultConfigurationIsVisible = 0; 497 | defaultConfigurationName = Release; 498 | }; 499 | C784F7FA2D15D4AF0005ABA6 /* Build configuration list for PBXNativeTarget "PearHID" */ = { 500 | isa = XCConfigurationList; 501 | buildConfigurations = ( 502 | C784F7FB2D15D4AF0005ABA6 /* Debug */, 503 | C784F7FC2D15D4AF0005ABA6 /* Release */, 504 | ); 505 | defaultConfigurationIsVisible = 0; 506 | defaultConfigurationName = Release; 507 | }; 508 | /* End XCConfigurationList section */ 509 | 510 | /* Begin XCRemoteSwiftPackageReference section */ 511 | C784F8032D15FA450005ABA6 /* XCRemoteSwiftPackageReference "AlinFoundation" */ = { 512 | isa = XCRemoteSwiftPackageReference; 513 | repositoryURL = "https://github.com/alienator88/AlinFoundation"; 514 | requirement = { 515 | branch = main; 516 | kind = branch; 517 | }; 518 | }; 519 | /* End XCRemoteSwiftPackageReference section */ 520 | 521 | /* Begin XCSwiftPackageProductDependency section */ 522 | C784F8042D15FA450005ABA6 /* AlinFoundation */ = { 523 | isa = XCSwiftPackageProductDependency; 524 | package = C784F8032D15FA450005ABA6 /* XCRemoteSwiftPackageReference "AlinFoundation" */; 525 | productName = AlinFoundation; 526 | }; 527 | /* End XCSwiftPackageProductDependency section */ 528 | }; 529 | rootObject = C784F7E32D15D4AE0005ABA6 /* Project object */; 530 | } 531 | -------------------------------------------------------------------------------- /PearHID.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /PearHID.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "50739d16fb3b1c36e28568d832b61475022b29d5c6dad4328005a7afcef102b7", 3 | "pins" : [ 4 | { 5 | "identity" : "alinfoundation", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/alienator88/AlinFoundation", 8 | "state" : { 9 | "branch" : "main", 10 | "revision" : "b58a237ed09ee5e5de831fbe053b0d67c98afdd5" 11 | } 12 | } 13 | ], 14 | "version" : 3 15 | } 16 | -------------------------------------------------------------------------------- /PearHID.xcodeproj/project.xcworkspace/xcuserdata/alin.xcuserdatad/IDEFindNavigatorScopes.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /PearHID.xcodeproj/xcshareddata/xcschemes/PearHID - Debug.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 10 | 16 | 22 | 23 | 24 | 25 | 26 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 58 | 59 | 60 | 61 | 67 | 69 | 75 | 76 | 77 | 78 | 80 | 81 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /PearHID.xcodeproj/xcuserdata/alin.xcuserdatad/xcschemes/PearHID - Release.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 10 | 16 | 22 | 23 | 24 | 25 | 26 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /PearHID.xcodeproj/xcuserdata/alin.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | PearHID - Debug.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | PearHID - Release.xcscheme 13 | 14 | orderHint 15 | 1 16 | 17 | PearHID-Helper copy.xcscheme_^#shared#^_ 18 | 19 | orderHint 20 | 3 21 | 22 | PearHID-Helper.xcscheme_^#shared#^_ 23 | 24 | orderHint 25 | 2 26 | 27 | PearHIDHelper.xcscheme_^#shared#^_ 28 | 29 | orderHint 30 | 2 31 | 32 | 33 | SuppressBuildableAutocreation 34 | 35 | C784F7EA2D15D4AE0005ABA6 36 | 37 | primary 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /PearHID/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 | -------------------------------------------------------------------------------- /PearHID/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 | "idiom" : "mac", 11 | "scale" : "2x", 12 | "size" : "16x16" 13 | }, 14 | { 15 | "filename" : "icon_32x32.png", 16 | "idiom" : "mac", 17 | "scale" : "1x", 18 | "size" : "32x32" 19 | }, 20 | { 21 | "filename" : "icon_32x32@2x.png", 22 | "idiom" : "mac", 23 | "scale" : "2x", 24 | "size" : "32x32" 25 | }, 26 | { 27 | "filename" : "icon_128x128.png", 28 | "idiom" : "mac", 29 | "scale" : "1x", 30 | "size" : "128x128" 31 | }, 32 | { 33 | "idiom" : "mac", 34 | "scale" : "2x", 35 | "size" : "128x128" 36 | }, 37 | { 38 | "filename" : "icon_256x256.png", 39 | "idiom" : "mac", 40 | "scale" : "1x", 41 | "size" : "256x256" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "scale" : "2x", 46 | "size" : "256x256" 47 | }, 48 | { 49 | "filename" : "icon_512x512.png", 50 | "idiom" : "mac", 51 | "scale" : "1x", 52 | "size" : "512x512" 53 | }, 54 | { 55 | "idiom" : "mac", 56 | "scale" : "2x", 57 | "size" : "512x512" 58 | } 59 | ], 60 | "info" : { 61 | "author" : "xcode", 62 | "version" : 1 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /PearHID/Assets.xcassets/AppIcon.appiconset/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alienator88/PearHID/a34e891ae2a96da3d7702a16a15f73157d5fe8c0/PearHID/Assets.xcassets/AppIcon.appiconset/icon_128x128.png -------------------------------------------------------------------------------- /PearHID/Assets.xcassets/AppIcon.appiconset/icon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alienator88/PearHID/a34e891ae2a96da3d7702a16a15f73157d5fe8c0/PearHID/Assets.xcassets/AppIcon.appiconset/icon_16x16.png -------------------------------------------------------------------------------- /PearHID/Assets.xcassets/AppIcon.appiconset/icon_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alienator88/PearHID/a34e891ae2a96da3d7702a16a15f73157d5fe8c0/PearHID/Assets.xcassets/AppIcon.appiconset/icon_256x256.png -------------------------------------------------------------------------------- /PearHID/Assets.xcassets/AppIcon.appiconset/icon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alienator88/PearHID/a34e891ae2a96da3d7702a16a15f73157d5fe8c0/PearHID/Assets.xcassets/AppIcon.appiconset/icon_32x32.png -------------------------------------------------------------------------------- /PearHID/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alienator88/PearHID/a34e891ae2a96da3d7702a16a15f73157d5fe8c0/PearHID/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png -------------------------------------------------------------------------------- /PearHID/Assets.xcassets/AppIcon.appiconset/icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alienator88/PearHID/a34e891ae2a96da3d7702a16a15f73157d5fe8c0/PearHID/Assets.xcassets/AppIcon.appiconset/icon_512x512.png -------------------------------------------------------------------------------- /PearHID/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /PearHID/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // PearHID 4 | // 5 | // Created by Alin Lupascu on 12/20/24. 6 | // 7 | 8 | import SwiftUI 9 | import AlinFoundation 10 | 11 | struct ContentView: View { 12 | @EnvironmentObject var viewModel: MappingsViewModel 13 | @EnvironmentObject var updater: Updater 14 | @ObservedObject private var helperToolManager = HelperToolManager.shared 15 | @AppStorage("settings.animatedBackground") private var animatedBackground: Bool = true 16 | @State private var showPlist = false 17 | @State private var text = "" 18 | @State private var showCheck = false 19 | 20 | var body: some View { 21 | 22 | ZStack { 23 | 24 | VStack(spacing: 10) { 25 | 26 | Divider() 27 | .padding(.bottom) 28 | 29 | if viewModel.mappings.isEmpty { 30 | Spacer() 31 | Text("No custom mappings added") 32 | .font(.callout) 33 | .foregroundStyle(.secondary) 34 | 35 | Spacer() 36 | } else { 37 | ScrollView { 38 | LazyVStack(alignment: .leading, spacing: 10) { 39 | ForEach(viewModel.mappings.indices, id: \.self) { key in 40 | MappingRowListItem(mapping: viewModel.mappings[key]) 41 | } 42 | Spacer() 43 | } 44 | } 45 | } 46 | 47 | Divider() 48 | .padding(.vertical) 49 | 50 | HStack { 51 | 52 | if !helperToolManager.isHelperToolInstalled { 53 | HelperBadge() 54 | .controlSize(.small) 55 | Spacer() 56 | } else if updater.updateAvailable { 57 | UpdateBadge(updater: updater, hideLabel: true) 58 | .controlSize(.small) 59 | Spacer() 60 | } 61 | 62 | else { 63 | TextField("Test key mappings here", text: $text) 64 | .textFieldStyle(.plain) 65 | .foregroundStyle(.secondary) 66 | } 67 | 68 | Button { 69 | viewModel.clearHIDKeyMappings(show: $showCheck) 70 | } label: { 71 | Text("Reset").padding(5) 72 | } 73 | .opacity(viewModel.mappings.isEmpty ? 0.5 : 1) 74 | .help("Remove all custom HID key mappings") 75 | 76 | Button { 77 | viewModel.setHIDKeyMappings(show: $showCheck) 78 | } label: { 79 | Text("Save").padding(5) 80 | } 81 | .help("Set HID key mappings to the configured list above") 82 | .opacity(viewModel.mappings.isEmpty ? 0.5 : 1) 83 | 84 | } 85 | 86 | 87 | 88 | } 89 | .frame(maxWidth: .infinity, maxHeight: .infinity) 90 | .padding(20) 91 | 92 | 93 | // Preview Plist Content 94 | if showPlist { 95 | VStack(spacing: 5) { 96 | HStack { 97 | Text("PearHID.KeyMapping.plist") 98 | .font(.headline) 99 | .foregroundStyle(.secondary) 100 | .padding(.leading) 101 | 102 | Spacer() 103 | 104 | Button(action: { 105 | NSWorkspace.shared.activateFileViewerSelecting([URL(fileURLWithPath: "/Library/LaunchDaemons/PearHID.KeyMapping.plist")]) 106 | }) { 107 | Image(systemName: "folder") 108 | .padding(5) 109 | .padding(.horizontal, 2) 110 | .padding(.bottom, 1) 111 | .clipShape(RoundedRectangle(cornerRadius: 4)) 112 | .shadow(radius: 2) 113 | } 114 | .buttonStyle(.plain) 115 | .help("Copy to clipboard") 116 | 117 | Button(action: { 118 | copyToClipboard(viewModel.generatePlist()) 119 | }) { 120 | Image(systemName: "list.clipboard") 121 | .padding(5) 122 | .padding(.horizontal, 2) 123 | .padding(.bottom, 1) 124 | .clipShape(RoundedRectangle(cornerRadius: 4)) 125 | .shadow(radius: 2) 126 | } 127 | .buttonStyle(.plain) 128 | .help("Copy to clipboard") 129 | } 130 | .padding([.top, .trailing], 5) 131 | 132 | Divider() 133 | 134 | ScrollView { 135 | LazyVStack(alignment: .leading, spacing: 0) { 136 | Text(viewModel.generatePlist()) 137 | .textSelection(.enabled) 138 | .foregroundStyle(.secondary) 139 | Spacer() 140 | } 141 | .padding(.horizontal, 10) 142 | .padding(.vertical, 5) 143 | } 144 | Spacer() 145 | } 146 | .frame(width: 400) 147 | .background(.thickMaterial) 148 | .clipShape(RoundedRectangle(cornerRadius: 8)) 149 | .overlay { 150 | RoundedRectangle(cornerRadius: 8) 151 | .strokeBorder(.primary.opacity(0.1), lineWidth: 1) 152 | } 153 | .frame(maxWidth: .infinity, alignment: .trailing) 154 | .padding([.trailing, .bottom]) 155 | .transition(.move(edge: .trailing)) 156 | 157 | } 158 | 159 | if showCheck { 160 | CheckmarkOverlay(show: $showCheck) 161 | } 162 | } 163 | .toolbar { 164 | ToolbarItem(placement: .principal) { 165 | MainMappingRowView() 166 | } 167 | 168 | ToolbarItem(placement: .automatic) { 169 | Button { 170 | withAnimation { 171 | viewModel.loadExistingMappingsFromAPI() 172 | showCheck = true 173 | } 174 | } label: { 175 | Image(systemName: "arrow.counterclockwise") 176 | .font(.title3) 177 | } 178 | .buttonStyle(.bordered) 179 | .help("Reload existing mappings") 180 | } 181 | ToolbarItem(placement: .automatic) { 182 | Button { 183 | withAnimation { 184 | showPlist.toggle() 185 | } 186 | } label: { 187 | Image(systemName: "sidebar.right") 188 | .font(.title3) 189 | } 190 | .buttonStyle(.bordered) 191 | .help("Plist Preview") 192 | 193 | } 194 | 195 | } 196 | .frame(minWidth: 650, minHeight: 500) 197 | .background { 198 | if !animatedBackground { 199 | Color.clear 200 | } else { 201 | ZStack { 202 | MetalView() // metal view 203 | Rectangle().fill(.ultraThickMaterial) // material background 204 | } 205 | .edgesIgnoringSafeArea(.all) 206 | } 207 | } 208 | .onTapGesture { 209 | withAnimation { 210 | showPlist = false 211 | } 212 | } 213 | 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /PearHID/KeyManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyManager.swift 3 | // PearHID 4 | // 5 | // Created by Alin Lupascu on 12/20/24. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | import AlinFoundation 11 | import IOKit.hid 12 | 13 | struct MainMappingRowView: View { 14 | @EnvironmentObject var viewModel: MappingsViewModel 15 | @State private var newMapping = KeyMapping() 16 | @State private var showingFromPopover = false 17 | @State private var showingToPopover = false 18 | 19 | var body: some View { 20 | // let usedKeys = viewModel.usedKeyStrings 21 | 22 | HStack { 23 | Button { 24 | showingFromPopover.toggle() 25 | } label: { 26 | Text(newMapping.from?.key ?? "From Key") 27 | .frame(width: 150) 28 | } 29 | .popover(isPresented: $showingFromPopover) { 30 | KeySelectionPopover(selectedKey: $newMapping.from, 31 | oppositeKey: newMapping.to, 32 | usedKeys: viewModel.usedKeyStrings, 33 | onDismiss: { showingFromPopover = false }) 34 | } 35 | 36 | Button { 37 | showingToPopover.toggle() 38 | } label: { 39 | Text(newMapping.to?.key ?? "To Key") 40 | .frame(width: 150) 41 | } 42 | .popover(isPresented: $showingToPopover) { 43 | KeySelectionPopover(selectedKey: $newMapping.to, 44 | oppositeKey: newMapping.from, 45 | usedKeys: viewModel.usedKeyStrings, 46 | onDismiss: { showingToPopover = false }) 47 | } 48 | 49 | Button { 50 | if let _ = newMapping.from, let _ = newMapping.to { 51 | viewModel.mappings.append(newMapping) 52 | newMapping = KeyMapping() 53 | } 54 | } label: { 55 | Image(systemName: "plus") 56 | } 57 | .opacity((newMapping.from == nil || newMapping.to == nil) ? 0 : 1) 58 | .disabled(newMapping.from == nil || newMapping.to == nil) 59 | 60 | } 61 | } 62 | } 63 | 64 | struct KeySelectionPopover: View { 65 | @Binding var selectedKey: KeyItem? 66 | let oppositeKey: KeyItem? 67 | let usedKeys: Set 68 | let onDismiss: () -> Void 69 | @State private var selectedGroup: KeyGroup? 70 | 71 | var body: some View { 72 | VStack(alignment: .leading, spacing: 0) { 73 | if let group = selectedGroup { 74 | Text("← Back") 75 | .foregroundStyle(.secondary) 76 | .clipShape(Rectangle()) 77 | .padding() 78 | .frame(width: .infinity) 79 | .onTapGesture { 80 | selectedGroup = nil 81 | } 82 | Divider() 83 | 84 | ScrollView { 85 | LazyVStack(alignment: .leading, spacing: 0) { 86 | ForEach(group.keys) { key in 87 | Button(action: { 88 | selectedKey = key 89 | onDismiss() 90 | selectedGroup = nil 91 | }) { 92 | Text(key.key) 93 | .frame(maxWidth: .infinity, alignment: .leading) 94 | .padding(.vertical, 6) 95 | .padding(.horizontal) 96 | } 97 | .disabled(oppositeKey?.key == key.key || usedKeys.contains(key.key)) 98 | .buttonStyle(.plain) 99 | Divider() 100 | } 101 | } 102 | } 103 | } else { 104 | Text("Select a key").foregroundStyle(.secondary) 105 | .padding() 106 | Divider() 107 | ScrollView { 108 | LazyVStack(alignment: .leading, spacing: 0) { 109 | ForEach(allKeys) { group in 110 | Button(action: { 111 | selectedGroup = group 112 | }) { 113 | Text(group.group) 114 | .frame(maxWidth: .infinity, alignment: .leading) 115 | .padding(.vertical, 6) 116 | .padding(.horizontal) 117 | } 118 | .buttonStyle(.plain) 119 | Divider() 120 | } 121 | } 122 | } 123 | } 124 | } 125 | .frame(width: 200) 126 | } 127 | } 128 | 129 | struct MappingRowListItem: View { 130 | @EnvironmentObject var viewModel: MappingsViewModel 131 | let mapping: KeyMapping 132 | @State private var isHovered: Bool = false 133 | 134 | var body: some View { 135 | 136 | HStack { 137 | HStack { 138 | 139 | Text("\(mapping.from?.key ?? "Unknown")") 140 | .frame(maxWidth: .infinity, alignment: .center) 141 | 142 | Image(systemName: "arrow.right").foregroundStyle(.secondary).font(.title2).padding(.horizontal, 5) 143 | 144 | Text("\(mapping.to?.key ?? "Unknown")") 145 | .frame(maxWidth: .infinity, alignment: .center) 146 | 147 | Button(action: { 148 | viewModel.removeMapping(mapping) 149 | }) { 150 | Image(systemName: "x.circle.fill") 151 | .opacity(isHovered ? 1 : 0.5) 152 | 153 | } 154 | .buttonStyle(.plain) 155 | .help("Remove this mapping") 156 | .onHover { hovering in 157 | isHovered = hovering 158 | } 159 | 160 | } 161 | .padding(8) 162 | .background { 163 | RoundedRectangle(cornerRadius: 8) 164 | .fill(.secondary.opacity(0.1)) 165 | } 166 | 167 | } 168 | 169 | } 170 | } 171 | 172 | // View model holding the array of mappings 173 | class MappingsViewModel: ObservableObject { 174 | @Published var mappings: [KeyMapping] = [] 175 | @Published var plistLoaded: Bool = false 176 | var usedKeyStrings: Set { 177 | Set(mappings.compactMap { $0.from?.key } + mappings.compactMap { $0.to?.key }) 178 | } 179 | 180 | init() { 181 | loadExistingMappingsFromAPI() 182 | } 183 | 184 | func addMapping() { 185 | mappings.append(KeyMapping()) 186 | } 187 | 188 | func removeMapping(_ mapping: KeyMapping) { 189 | mappings.removeAll { $0.id == mapping.id } 190 | } 191 | 192 | func removeAllMappings() { 193 | mappings.removeAll() 194 | } 195 | 196 | // Generate plist string from current mappings 197 | func generatePlist() -> String { 198 | let items = mappings.compactMap { mappingToHexString(mapping: $0) }.joined(separator: ",\n") 199 | 200 | return """ 201 | 202 | 204 | 205 | 206 | Label 207 | PearHID.KeyMapping 208 | ProgramArguments 209 | 210 | /usr/bin/hidutil 211 | property 212 | --set 213 | {"UserKeyMapping":[ 214 | \(items) 215 | ]} 216 | 217 | RunAtLoad 218 | 219 | 220 | 221 | """ 222 | } 223 | 224 | 225 | private func mappingToHexString(mapping: KeyMapping) -> String? { 226 | guard let fromHex = mapping.from?.hex, let toHex = mapping.to?.hex else { return nil } 227 | return """ 228 | { 229 | "HIDKeyboardModifierMappingSrc": 0x\(String(0x700000000 + UInt64(fromHex), radix: 16).uppercased()), 230 | "HIDKeyboardModifierMappingDst": 0x\(String(0x700000000 + UInt64(toHex), radix: 16).uppercased()) 231 | } 232 | """ 233 | } 234 | } 235 | 236 | extension MappingsViewModel { 237 | 238 | func loadExistingMappingsFromAPI() { 239 | let systemClient = IOHIDEventSystemClientCreateSimpleClient(kCFAllocatorDefault) 240 | 241 | guard let services = IOHIDEventSystemClientCopyServices(systemClient) as? [IOHIDServiceClient] else { 242 | printOS("Failed to get HID services") 243 | return 244 | } 245 | 246 | for service in services { 247 | if IOHIDServiceClientConformsTo(service, UInt32(kHIDPage_GenericDesktop), UInt32(kHIDUsage_GD_Keyboard)) != 0, 248 | let props = IOHIDServiceClientCopyProperty(service, kIOHIDUserKeyUsageMapKey as CFString) as? [[String: UInt64]] { 249 | 250 | let loadedMappings: [KeyMapping] = props.compactMap { mapping in 251 | let srcHex = mapping["HIDKeyboardModifierMappingSrc"] ?? 0 252 | let dstHex = mapping["HIDKeyboardModifierMappingDst"] ?? 0 253 | 254 | let srcKeyHex = UInt64(srcHex & 0xFF) 255 | let dstKeyHex = UInt64(dstHex & 0xFF) 256 | 257 | let fromKey = findKeyItem(forHex: srcKeyHex) 258 | let toKey = findKeyItem(forHex: dstKeyHex) 259 | 260 | guard let fromKey = fromKey, let toKey = toKey else { 261 | printOS("Could not find matching keys for src=\(String(format:"0x%X", srcKeyHex)), dst=\(String(format:"0x%X", dstKeyHex))") 262 | return nil 263 | } 264 | 265 | return KeyMapping(from: fromKey, to: toKey) 266 | } 267 | 268 | DispatchQueue.main.async { 269 | self.mappings = loadedMappings 270 | self.plistLoaded = true 271 | } 272 | return 273 | } 274 | } 275 | 276 | printOS("No UserKeyMapping found via API") 277 | } 278 | 279 | 280 | private func findKeyItem(forHex hex: UInt64) -> KeyItem? { 281 | for group in allKeys { 282 | if let key = group.keys.first(where: { $0.hex == hex }) { 283 | return key 284 | } 285 | } 286 | return nil 287 | } 288 | 289 | func setupLaunchDaemon(plistContent: String? = nil, install: Bool) throws { 290 | let plistPath = "/Library/LaunchDaemons/PearHID.KeyMapping.plist" 291 | 292 | if install { 293 | // Step 1: Validate the plist content 294 | guard let plistContent, let data = plistContent.data(using: .utf8) else { 295 | printOS("Invalid plist string encoding.") 296 | return 297 | } 298 | do { 299 | _ = try PropertyListSerialization.propertyList(from: data, options: [], format: nil) 300 | } catch { 301 | printOS("Plist content is invalid: \(error.localizedDescription)") 302 | return 303 | } 304 | // Step 2: Check if file already exists 305 | if FileManager.default.fileExists(atPath: plistPath) { 306 | // Overwrite the existing file with sudo 307 | let tempPath = "/tmp/temp_launch_daemon.plist" 308 | try data.write(to: URL(fileURLWithPath: tempPath)) 309 | 310 | let copy = "cp \(tempPath) \(plistPath)" 311 | let chown = "chown root:wheel \(plistPath)" 312 | let chmod = "chmod 644 \(plistPath)" 313 | 314 | let success = executeFileCommands("\(copy); \(chown); \(chmod)") 315 | if success { 316 | } else { 317 | printOS("Failed to setup launchd plist") 318 | } 319 | 320 | 321 | } else { 322 | // If the file doesn't exist, create it in the correct location 323 | let tempPath = "/tmp/temp_launch_daemon.plist" 324 | try data.write(to: URL(fileURLWithPath: tempPath)) 325 | 326 | let move = "mv \(tempPath) \(plistPath)" 327 | let chown = "chown root:wheel \(plistPath)" 328 | let chmod = "chmod 644 \(plistPath)" 329 | 330 | let success = executeFileCommands("\(move); \(chown); \(chmod)") 331 | if success { 332 | self.plistLoaded = true 333 | } else { 334 | printOS("Failed to setup launchd plist") 335 | } 336 | 337 | } 338 | } else { 339 | // Step 1: Remove plist file 340 | let success = executeFileCommands("rm -f \(plistPath)") 341 | if success { 342 | self.plistLoaded = false 343 | } else { 344 | printOS("Failed to remove launchd plist") 345 | } 346 | 347 | self.plistLoaded = false 348 | } 349 | 350 | 351 | 352 | } 353 | 354 | func applyKeyMappingsAPI(mappings: [KeyMapping]) { 355 | @AppStorage("settings.persistReboot") var persistReboot: Bool = true 356 | 357 | let array: [[String: UInt64]] = mappings.compactMap { 358 | guard let from = $0.from?.hex, let to = $0.to?.hex else { return nil } 359 | return [ 360 | "HIDKeyboardModifierMappingSrc": 0x700000000 + UInt64(from), 361 | "HIDKeyboardModifierMappingDst": 0x700000000 + UInt64(to) 362 | ] 363 | } 364 | 365 | let systemClient = IOHIDEventSystemClientCreateSimpleClient(kCFAllocatorDefault) 366 | 367 | guard let services = IOHIDEventSystemClientCopyServices(systemClient) as? [IOHIDServiceClient] else { 368 | printOS("Failed to get HID services") 369 | return 370 | } 371 | 372 | for service in services { 373 | if IOHIDServiceClientConformsTo(service, UInt32(kHIDPage_GenericDesktop), UInt32(kHIDUsage_GD_Keyboard)) != 0 { 374 | let success = IOHIDServiceClientSetProperty(service, 375 | kIOHIDUserKeyUsageMapKey as CFString, 376 | array as CFArray) 377 | if !success { 378 | printOS("Failed to apply mapping to service") 379 | } 380 | } 381 | } 382 | 383 | if persistReboot { 384 | do { 385 | if mappings.isEmpty { 386 | try setupLaunchDaemon(install: false) 387 | } else { 388 | try setupLaunchDaemon(plistContent: generatePlist(), install: true) 389 | } 390 | } catch { 391 | printOS("Set Plist Error: \(error.localizedDescription)") 392 | } 393 | } 394 | 395 | } 396 | 397 | func clearHIDKeyMappings(show: Binding) { 398 | if isAccessibilityGranted() { 399 | mappings = [] 400 | applyKeyMappingsAPI(mappings: mappings) 401 | withAnimation { show.wrappedValue = true } 402 | } else { 403 | checkAndRequestAccessibilityPermission() 404 | } 405 | 406 | } 407 | 408 | func setHIDKeyMappings(show: Binding) { 409 | if isAccessibilityGranted() { 410 | applyKeyMappingsAPI(mappings: mappings) 411 | withAnimation { show.wrappedValue = true } 412 | } else { 413 | checkAndRequestAccessibilityPermission() 414 | } 415 | } 416 | } 417 | 418 | 419 | 420 | 421 | 422 | func executeFileCommands(_ commands: String) -> Bool { 423 | var status = false 424 | 425 | if HelperToolManager.shared.isHelperToolInstalled { 426 | let semaphore = DispatchSemaphore(value: 0) 427 | var success = false 428 | var output = "" 429 | 430 | Task { 431 | let result = await HelperToolManager.shared.runCommand(commands) 432 | success = result.0 433 | output = result.1 434 | semaphore.signal() 435 | } 436 | semaphore.wait() 437 | 438 | status = success 439 | if !success { 440 | printOS("Helper Error: \(output)") 441 | } 442 | } else { 443 | let result = performPrivilegedCommands(commands: commands) 444 | status = result.0 445 | if !status { 446 | printOS("Auth Services Error: \(result.1)") 447 | } 448 | 449 | } 450 | 451 | return status 452 | } 453 | -------------------------------------------------------------------------------- /PearHID/Keys.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Keys.swift 3 | // PearHID 4 | // 5 | // Created by Alin Lupascu on 12/20/24. 6 | // 7 | import Foundation 8 | 9 | // Struct for a key group and its keys 10 | struct KeyGroup: Identifiable { 11 | let id = UUID() 12 | let group: String 13 | let keys: [KeyItem] 14 | } 15 | 16 | struct KeyItem: Identifiable, Hashable { 17 | let id = UUID() 18 | let key: String 19 | let hex: UInt64 20 | } 21 | 22 | struct KeyMapping: Identifiable { 23 | let id = UUID() 24 | var from: KeyItem? 25 | var to: KeyItem? 26 | } 27 | 28 | let allKeys: [KeyGroup] = [ 29 | KeyGroup(group: "Modifier keys", keys: [ 30 | KeyItem(key: "caps_lock", hex: 0x39), 31 | KeyItem(key: "left_control", hex: 0xE0), 32 | KeyItem(key: "left_shift", hex: 0xE1), 33 | KeyItem(key: "left_option", hex: 0xE2), 34 | KeyItem(key: "left_command", hex: 0xE3), 35 | KeyItem(key: "right_control", hex: 0xE4), 36 | KeyItem(key: "right_shift", hex: 0xE5), 37 | KeyItem(key: "right_option", hex: 0xE6), 38 | KeyItem(key: "right_command", hex: 0xE7), 39 | KeyItem(key: "fn", hex: 0x0003 + 0xFF00000000 - 0x700000000) 40 | ]), 41 | KeyGroup(group: "Controls and symbols", keys: [ 42 | KeyItem(key: "return_or_enter", hex: 0x28), 43 | KeyItem(key: "escape", hex: 0x29), 44 | KeyItem(key: "delete_or_backspace", hex: 0x2A), 45 | KeyItem(key: "delete_forward", hex: 0x4C), 46 | KeyItem(key: "tab", hex: 0x2B), 47 | KeyItem(key: "spacebar", hex: 0x2C), 48 | KeyItem(key: "hyphen (-)", hex: 0x2D), 49 | KeyItem(key: "equal_sign (=)", hex: 0x2E), 50 | KeyItem(key: "open_bracket [", hex: 0x2F), 51 | KeyItem(key: "close_bracket ]", hex: 0x30), 52 | KeyItem(key: "backslash (\\)", hex: 0x31), 53 | KeyItem(key: "non_us_pound", hex: 0x32), 54 | KeyItem(key: "semicolon (;)", hex: 0x33), 55 | KeyItem(key: "quote (')", hex: 0x34), 56 | KeyItem(key: "grave_accent_and_tilde (`)", hex: 0x35), 57 | KeyItem(key: "comma (,)", hex: 0x36), 58 | KeyItem(key: "period (.)", hex: 0x37), 59 | KeyItem(key: "slash (/)", hex: 0x38), 60 | KeyItem(key: "non_us_backslash (\\), section (§)", hex: 0x64) 61 | ]), 62 | KeyGroup(group: "Arrow keys", keys: [ 63 | KeyItem(key: "up_arrow", hex: 0x52), 64 | KeyItem(key: "down_arrow", hex: 0x51), 65 | KeyItem(key: "left_arrow", hex: 0x50), 66 | KeyItem(key: "right_arrow", hex: 0x4F), 67 | KeyItem(key: "page_up", hex: 0x4B), 68 | KeyItem(key: "page_down", hex: 0x4E), 69 | KeyItem(key: "home", hex: 0x4A), 70 | KeyItem(key: "end", hex: 0x4D) 71 | ]), 72 | KeyGroup(group: "Letter keys", keys: [ 73 | KeyItem(key: "a", hex: 0x04), 74 | KeyItem(key: "b", hex: 0x05), 75 | KeyItem(key: "c", hex: 0x06), 76 | KeyItem(key: "d", hex: 0x07), 77 | KeyItem(key: "e", hex: 0x08), 78 | KeyItem(key: "f", hex: 0x09), 79 | KeyItem(key: "g", hex: 0x0A), 80 | KeyItem(key: "h", hex: 0x0B), 81 | KeyItem(key: "i", hex: 0x0C), 82 | KeyItem(key: "j", hex: 0x0D), 83 | KeyItem(key: "k", hex: 0x0E), 84 | KeyItem(key: "l", hex: 0x0F), 85 | KeyItem(key: "m", hex: 0x10), 86 | KeyItem(key: "n", hex: 0x11), 87 | KeyItem(key: "o", hex: 0x12), 88 | KeyItem(key: "p", hex: 0x13), 89 | KeyItem(key: "q", hex: 0x14), 90 | KeyItem(key: "r", hex: 0x15), 91 | KeyItem(key: "s", hex: 0x16), 92 | KeyItem(key: "t", hex: 0x17), 93 | KeyItem(key: "u", hex: 0x18), 94 | KeyItem(key: "v", hex: 0x19), 95 | KeyItem(key: "w", hex: 0x1A), 96 | KeyItem(key: "x", hex: 0x1B), 97 | KeyItem(key: "y", hex: 0x1C), 98 | KeyItem(key: "z", hex: 0x1D) 99 | ]), 100 | KeyGroup(group: "Number keys", keys: [ 101 | KeyItem(key: "1", hex: 0x1E), 102 | KeyItem(key: "2", hex: 0x1F), 103 | KeyItem(key: "3", hex: 0x20), 104 | KeyItem(key: "4", hex: 0x21), 105 | KeyItem(key: "5", hex: 0x22), 106 | KeyItem(key: "6", hex: 0x23), 107 | KeyItem(key: "7", hex: 0x24), 108 | KeyItem(key: "8", hex: 0x25), 109 | KeyItem(key: "9", hex: 0x26), 110 | KeyItem(key: "0", hex: 0x27) 111 | ]), 112 | KeyGroup(group: "Function keys", keys: [ 113 | KeyItem(key: "f1", hex: 0x3A), 114 | KeyItem(key: "f2", hex: 0x3B), 115 | KeyItem(key: "f3", hex: 0x3C), 116 | KeyItem(key: "f4", hex: 0x3D), 117 | KeyItem(key: "f5", hex: 0x3E), 118 | KeyItem(key: "f6", hex: 0x3F), 119 | KeyItem(key: "f7", hex: 0x40), 120 | KeyItem(key: "f8", hex: 0x41), 121 | KeyItem(key: "f9", hex: 0x42), 122 | KeyItem(key: "f10", hex: 0x43), 123 | KeyItem(key: "f11", hex: 0x44), 124 | KeyItem(key: "f12", hex: 0x45), 125 | KeyItem(key: "f13", hex: 0x68), 126 | KeyItem(key: "f14", hex: 0x69), 127 | KeyItem(key: "f15", hex: 0x6A), 128 | KeyItem(key: "f16", hex: 0x6B), 129 | KeyItem(key: "f17", hex: 0x6C), 130 | KeyItem(key: "f18", hex: 0x6D), 131 | KeyItem(key: "f19", hex: 0x6E) 132 | ]), 133 | KeyGroup(group: "Media control keys", keys: [ 134 | KeyItem(key: "display_brightness_decrement", hex: UInt64(0xC00000070) - UInt64(0x700000000)), 135 | KeyItem(key: "display_brightness_increment", hex: UInt64(0xC0000006F) - UInt64(0x700000000)), 136 | KeyItem(key: "rewind", hex: UInt64(0xC000000B4) - UInt64(0x700000000)), 137 | KeyItem(key: "play_or_pause", hex: UInt64(0xC000000CD) - UInt64(0x700000000)), 138 | KeyItem(key: "fast_forward", hex: UInt64(0xC000000B3) - UInt64(0x700000000)), 139 | KeyItem(key: "mute", hex: UInt64(0xC000000E2) - UInt64(0x700000000)), 140 | KeyItem(key: "volume_decrement", hex: UInt64(0xC000000EA) - UInt64(0x700000000)), 141 | KeyItem(key: "volume_increment", hex: UInt64(0xC000000E9) - UInt64(0x700000000)) 142 | ]), 143 | KeyGroup(group: "Keys in pc keyboards", keys: [ 144 | KeyItem(key: "print_screen", hex: 0x46), 145 | KeyItem(key: "scroll_lock", hex: 0x47), 146 | KeyItem(key: "pause", hex: 0x48), 147 | KeyItem(key: "insert", hex: 0x49), 148 | KeyItem(key: "application", hex: 0x65), 149 | KeyItem(key: "help", hex: 0x75), 150 | KeyItem(key: "power", hex: 0x66), 151 | KeyItem(key: "execute", hex: 0x74), 152 | KeyItem(key: "menu", hex: 0x76), 153 | KeyItem(key: "select", hex: 0x77), 154 | KeyItem(key: "stop", hex: 0x78), 155 | KeyItem(key: "again", hex: 0x79), 156 | KeyItem(key: "undo", hex: 0x7A), 157 | KeyItem(key: "cut", hex: 0x7B), 158 | KeyItem(key: "copy", hex: 0x7C), 159 | KeyItem(key: "paste", hex: 0x7D), 160 | KeyItem(key: "find", hex: 0x7E), 161 | KeyItem(key: "mute", hex: 0x7F), 162 | KeyItem(key: "volume_up", hex: 0x80), 163 | KeyItem(key: "volume_down", hex: 0x81), 164 | KeyItem(key: "locking_caps_lock", hex: 0x82), 165 | KeyItem(key: "locking_num_lock", hex: 0x83), 166 | KeyItem(key: "locking_scroll_lock", hex: 0x84) 167 | ]), 168 | KeyGroup(group: "Keypad keys", keys: [ 169 | KeyItem(key: "keypad_num_lock", hex: 0x53), 170 | KeyItem(key: "keypad_slash (/)", hex: 0x54), 171 | KeyItem(key: "keypad_asterisk (*)", hex: 0x55), 172 | KeyItem(key: "keypad_hyphen (-)", hex: 0x56), 173 | KeyItem(key: "keypad_plus (+)", hex: 0x57), 174 | KeyItem(key: "keypad_enter", hex: 0x58), 175 | KeyItem(key: "keypad_1", hex: 0x59), 176 | KeyItem(key: "keypad_2", hex: 0x5A), 177 | KeyItem(key: "keypad_3", hex: 0x5B), 178 | KeyItem(key: "keypad_4", hex: 0x5C), 179 | KeyItem(key: "keypad_5", hex: 0x5D), 180 | KeyItem(key: "keypad_6", hex: 0x5E), 181 | KeyItem(key: "keypad_7", hex: 0x5F), 182 | KeyItem(key: "keypad_8", hex: 0x60), 183 | KeyItem(key: "keypad_9", hex: 0x61), 184 | KeyItem(key: "keypad_0", hex: 0x62), 185 | KeyItem(key: "keypad_period (.)", hex: 0x63), 186 | KeyItem(key: "keypad_equal_sign (=)", hex: 0x67), 187 | KeyItem(key: "keypad_comma (,)", hex: 0x85) 188 | ]) 189 | ] 190 | -------------------------------------------------------------------------------- /PearHID/Metal/LavaLampShader.metal: -------------------------------------------------------------------------------- 1 | // 2 | // LavaLampShader.metal 3 | // Playground 4 | // 5 | // Created by Alin Lupascu on 3/26/25. 6 | // 7 | 8 | #include 9 | using namespace metal; 10 | 11 | float metaball(float2 p, float2 center, float radius) { 12 | float2 diff = p - center; 13 | return radius * radius / dot(diff, diff); 14 | } 15 | 16 | vertex float4 vertex_passthrough(uint vertexID [[vertex_id]]) { 17 | float2 positions[6] = { 18 | {-1.0, -1.0}, {1.0, -1.0}, {-1.0, 1.0}, 19 | {-1.0, 1.0}, {1.0, -1.0}, {1.0, 1.0} 20 | }; 21 | return float4(positions[vertexID], 0.0, 1.0); 22 | } 23 | 24 | fragment float4 lavaLampFrag(float4 fragCoord [[position]], 25 | constant float2 *centers [[buffer(0)]], 26 | constant float *radii [[buffer(1)]], 27 | constant uint &count [[buffer(2)]], 28 | constant float &time [[buffer(3)]], 29 | constant float2 &resolution [[buffer(4)]]) { 30 | float2 uv = fragCoord.xy / resolution; 31 | float intensity = 0.0; 32 | float3 color = float3(0.0); 33 | 34 | for (uint i = 0; i < count; ++i) { 35 | float2 animated = centers[i] + 0.25 * float2(sin(time * 0.6 + float(i) * 1.7), cos(time * 0.4 + float(i) * 1.3)); 36 | float radius = radii[i] * (2.0 + 0.5 * sin(time + float(i) * 2.17)); // larger size 37 | float contrib = metaball(uv, animated, radius); 38 | if (i % 3 == 0) { 39 | color += contrib * float3(1.0, 0.4, 0.7) * 0.5; // pink (dimmed) 40 | } else if (i % 3 == 1) { 41 | color += contrib * float3(1.0, 0.6, 0.2) * 0.5; // orange (dimmed) 42 | } else { 43 | color += contrib * float3(0.4, 0.5, 1.0) * 0.5; // purple-blue (dimmed) 44 | } 45 | intensity += contrib; 46 | } 47 | 48 | float threshold = 1.0; 49 | return float4(smoothstep(threshold - 0.3, threshold + 0.3, intensity) * color, 1.0); 50 | } 51 | -------------------------------------------------------------------------------- /PearHID/Metal/LavaLampView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Metal.swift 3 | // Playground 4 | // 5 | // Created by Alin Lupascu on 3/26/25. 6 | // 7 | 8 | import SwiftUI 9 | import MetalKit 10 | 11 | struct MetalView: NSViewRepresentable { 12 | func makeCoordinator() -> Renderer { Renderer() } 13 | 14 | func makeNSView(context: Context) -> MTKView { 15 | let view = MTKView() 16 | view.device = MTLCreateSystemDefaultDevice() 17 | view.colorPixelFormat = .bgra8Unorm 18 | view.isPaused = false 19 | view.enableSetNeedsDisplay = false 20 | view.preferredFramesPerSecond = 60 21 | view.delegate = context.coordinator 22 | context.coordinator.mtkView(view, drawableSizeWillChange: view.drawableSize) 23 | return view 24 | } 25 | 26 | func updateNSView(_ nsView: MTKView, context: Context) {} 27 | } 28 | 29 | 30 | 31 | class Renderer: NSObject, MTKViewDelegate { 32 | var device: MTLDevice! 33 | var commandQueue: MTLCommandQueue! 34 | var pipelineState: MTLRenderPipelineState! 35 | 36 | var centers: [SIMD2] = [] 37 | var radii: [Float] = [] 38 | var time: Float = 0 39 | 40 | override init() { 41 | super.init() 42 | device = MTLCreateSystemDefaultDevice() 43 | commandQueue = device.makeCommandQueue() 44 | setupPipeline() 45 | setupBlobs() 46 | } 47 | 48 | func setupPipeline() { 49 | let library = device.makeDefaultLibrary() 50 | let pipelineDesc = MTLRenderPipelineDescriptor() 51 | pipelineDesc.vertexFunction = library?.makeFunction(name: "vertex_passthrough") 52 | pipelineDesc.fragmentFunction = library?.makeFunction(name: "lavaLampFrag") 53 | pipelineDesc.colorAttachments[0].pixelFormat = .bgra8Unorm 54 | pipelineState = try! device.makeRenderPipelineState(descriptor: pipelineDesc) 55 | } 56 | 57 | func setupBlobs() { 58 | for _ in 0..<6 { 59 | centers.append(SIMD2(Float.random(in: 0.2...0.8), Float.random(in: 0.2...0.8))) 60 | radii.append(Float.random(in: 0.05...0.15)) 61 | } 62 | } 63 | 64 | func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {} 65 | 66 | func draw(in view: MTKView) { 67 | guard let drawable = view.currentDrawable, 68 | let rpd = view.currentRenderPassDescriptor else { return } 69 | 70 | time += 0.016 71 | 72 | let cmdBuffer = commandQueue.makeCommandBuffer()! 73 | let encoder = cmdBuffer.makeRenderCommandEncoder(descriptor: rpd)! 74 | encoder.setRenderPipelineState(pipelineState) 75 | 76 | encoder.setFragmentBytes(¢ers, length: MemoryLayout>.stride * centers.count, index: 0) 77 | encoder.setFragmentBytes(&radii, length: MemoryLayout.stride * radii.count, index: 1) 78 | 79 | var count = UInt32(centers.count) 80 | encoder.setFragmentBytes(&count, length: MemoryLayout.stride, index: 2) 81 | encoder.setFragmentBytes(&time, length: MemoryLayout.stride, index: 3) 82 | 83 | var resolution = SIMD2(Float(view.drawableSize.width), Float(view.drawableSize.height)) 84 | encoder.setFragmentBytes(&resolution, length: MemoryLayout>.stride, index: 4) 85 | 86 | encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6) 87 | encoder.endEncoding() 88 | cmdBuffer.present(drawable) 89 | cmdBuffer.commit() 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /PearHID/PearHIDApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PearHIDApp.swift 3 | // PearHID 4 | // 5 | // Created by Alin Lupascu on 12/20/24. 6 | // 7 | 8 | import SwiftUI 9 | import AlinFoundation 10 | 11 | @main 12 | struct PearHIDApp: App { 13 | @StateObject var viewModel = MappingsViewModel() 14 | @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate 15 | @StateObject private var updater = Updater(owner: "alienator88", repo: "PearHID") 16 | @State private var windowController = WindowManager.shared 17 | 18 | var body: some Scene { 19 | WindowGroup { 20 | ContentView() 21 | .environmentObject(viewModel) 22 | .environmentObject(updater) 23 | .toolbar { Color.clear } 24 | } 25 | .windowStyle(.hiddenTitleBar) 26 | .windowToolbarStyle(.unified) 27 | .windowResizability(.contentSize) 28 | .commands { 29 | CommandGroup(after: .help) { 30 | Button { 31 | windowController.open(with: ConsoleView(), width: 600, height: 400) 32 | } label: { 33 | Text("Debug Console") 34 | } 35 | .keyboardShortcut("d", modifiers: .command) 36 | } 37 | } 38 | 39 | Settings { 40 | SettingsView() 41 | .environmentObject(updater) 42 | .toolbarBackground(.clear) 43 | } 44 | } 45 | } 46 | 47 | class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate { 48 | func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 49 | return true 50 | } 51 | func applicationDidFinishLaunching(_ notification: Notification) { 52 | checkAndRequestAccessibilityPermission() 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /PearHID/Settings/SettingsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsView.swift 3 | // PearHID 4 | // 5 | // Created by Alin Lupascu on 4/4/25. 6 | // 7 | 8 | import SwiftUI 9 | import Foundation 10 | import AlinFoundation 11 | 12 | struct SettingsView: View { 13 | @ObservedObject private var helperToolManager = HelperToolManager.shared 14 | @EnvironmentObject var updater: Updater 15 | @AppStorage("settings.persistReboot") private var persistReboot: Bool = true 16 | @AppStorage("settings.animatedBackground") private var animatedBackground: Bool = true 17 | @State private var commandOutput: String = "Command output will display here" 18 | @State private var commandToRun: String = "whoami" 19 | @State private var commandToRunManual: String = "" 20 | @State private var showTestingUI: Bool = false 21 | 22 | var body: some View { 23 | ScrollView { 24 | VStack(spacing: 20) { 25 | 26 | GroupBox(label: 27 | HStack { 28 | Text("Helper Tool").font(.title2) 29 | Spacer() 30 | Button(action: { 31 | helperToolManager.openSMSettings() 32 | }) { 33 | Label("Login Items", systemImage: "gear") 34 | .padding(4) 35 | } 36 | .buttonStyle(.borderedProminent) 37 | .contextMenu { 38 | Button("Kickstart Service") { 39 | let result = performPrivilegedCommands(commands: "launchctl kickstart -k system/com.alienator88.PearHID.Helper") 40 | if !result.0 { 41 | printOS("Helper Kickstart Error: \(result.1)") 42 | } 43 | } 44 | Button("Unregister Service") { 45 | Task { 46 | await helperToolManager.manageHelperTool(action: .uninstall) 47 | } 48 | } 49 | } 50 | } 51 | .padding(.bottom, 5) 52 | ) { 53 | VStack { 54 | HStack(spacing: 0) { 55 | Image(systemName: "lock") 56 | .resizable() 57 | .scaledToFit() 58 | .frame(width: 20, height: 20) 59 | .padding(.trailing, 5) 60 | .foregroundStyle(.primary) 61 | .onTapGesture { 62 | showTestingUI.toggle() 63 | } 64 | Text("Perform privileged operations seamlessly") 65 | .font(.callout) 66 | .foregroundStyle(.primary) 67 | .frame(maxWidth: .infinity, alignment: .leading) 68 | 69 | Toggle(isOn: Binding( 70 | get: { helperToolManager.isHelperToolInstalled }, 71 | set: { newValue in 72 | Task { 73 | if newValue { 74 | await helperToolManager.manageHelperTool(action: .install) 75 | } else { 76 | await helperToolManager.manageHelperTool(action: .uninstall) 77 | } 78 | } 79 | } 80 | ), label: { 81 | }) 82 | .toggleStyle(.switch) 83 | .frame(alignment: .trailing) 84 | } 85 | 86 | Divider() 87 | .padding(.vertical, 5) 88 | 89 | HStack { 90 | Text(helperToolManager.message) 91 | .font(.footnote) 92 | .foregroundStyle(.secondary) 93 | Spacer() 94 | } 95 | } 96 | .padding(5) 97 | } 98 | 99 | 100 | if showTestingUI { 101 | GroupBox(label: Text("Permission Testing").font(.title2).padding(.bottom, 5)) { 102 | VStack { 103 | 104 | Picker("Example privileged commands", selection: $commandToRun) { 105 | Text("whoami").tag("whoami") 106 | Text("systemsetup -getsleep").tag("systemsetup -getsleep") 107 | Text("systemsetup -getcomputername").tag("systemsetup -getcomputername") 108 | } 109 | .pickerStyle(MenuPickerStyle()) 110 | .onChange(of: commandToRun) { newValue in 111 | if helperToolManager.isHelperToolInstalled { 112 | Task { 113 | let (success, output) = await helperToolManager.runCommand(commandToRun) 114 | if success { 115 | commandOutput = output 116 | } else { 117 | commandOutput = "Error: \(output)" 118 | } 119 | } 120 | } 121 | } 122 | .onAppear{ 123 | if helperToolManager.isHelperToolInstalled { 124 | Task { 125 | let (success, output) = await helperToolManager.runCommand(commandToRun) 126 | if success { 127 | commandOutput = output 128 | } else { 129 | commandOutput = "Error: \(output)" 130 | } 131 | } 132 | } 133 | } 134 | 135 | TextField("Enter manual command here, Enter to run", text: $commandToRunManual) 136 | .padding(8) 137 | .background(RoundedRectangle(cornerRadius: 8).strokeBorder(Color.secondary.opacity(0.2), lineWidth: 1)) 138 | .textFieldStyle(.plain) 139 | .onSubmit { 140 | Task { 141 | let (success, output) = await helperToolManager.runCommand(commandToRunManual) 142 | if success { 143 | commandOutput = output 144 | } else { 145 | commandOutput = "Error: \(output)" 146 | } 147 | } 148 | } 149 | 150 | ScrollView { 151 | Text(commandOutput) 152 | .font(.system(.body, design: .monospaced)) 153 | .foregroundStyle(.secondary) 154 | .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) 155 | .padding() 156 | } 157 | .frame(height: 185) 158 | .frame(maxWidth: .infinity) 159 | .background(.tertiary.opacity(0.1)) 160 | .cornerRadius(8) 161 | } 162 | .padding(5) 163 | .disabled(!helperToolManager.isHelperToolInstalled) 164 | .opacity(helperToolManager.isHelperToolInstalled ? 1 : 0.5) 165 | } 166 | } 167 | 168 | GroupBox(label: Text("Persistence").font(.title2).padding(.bottom, 5), content: { 169 | HStack(spacing: 0) { 170 | Image(systemName: "autostartstop") 171 | .resizable() 172 | .scaledToFit() 173 | .frame(width: 20, height: 20) 174 | .padding(.trailing, 5) 175 | .foregroundStyle(.primary) 176 | Text("Keep changes on reboot via launch daemon") 177 | .font(.callout) 178 | .foregroundStyle(.primary) 179 | .frame(maxWidth: .infinity, alignment: .leading) 180 | 181 | Toggle("", isOn: $persistReboot) 182 | .toggleStyle(.switch) 183 | } 184 | .padding(5) 185 | .help("If this is enabled, a LaunchDaemon will be installed so the mappings survive reboots. Otherwise the mappings only affect the current session.") 186 | }) 187 | 188 | GroupBox(label: Text("Background").font(.title2).padding(.bottom, 5), content: { 189 | HStack(spacing: 0) { 190 | Image(systemName: "macwindow") 191 | .resizable() 192 | .scaledToFit() 193 | .frame(width: 20, height: 20) 194 | .padding(.trailing, 5) 195 | .foregroundStyle(.primary) 196 | Text("Animated background") 197 | .font(.callout) 198 | .foregroundStyle(.primary) 199 | .frame(maxWidth: .infinity, alignment: .leading) 200 | 201 | Toggle("", isOn: $animatedBackground) 202 | .toggleStyle(.switch) 203 | } 204 | .padding(5) 205 | 206 | }) 207 | 208 | 209 | UpdateView() 210 | .environmentObject(updater) 211 | 212 | // Spacer() 213 | 214 | } 215 | .onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in 216 | Task { 217 | await helperToolManager.manageHelperTool() 218 | } 219 | if helperToolManager.isHelperToolInstalled && showTestingUI { 220 | Task { 221 | let (success, output) = await helperToolManager.runCommand(commandToRun) 222 | if success { 223 | commandOutput = output 224 | } else { 225 | printOS("Helper: \(output)") 226 | } 227 | } 228 | } 229 | } 230 | } 231 | 232 | .frame(width: 500, height: 670, alignment: .top) 233 | .padding(20) 234 | 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /PearHID/Settings/UpdateView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UpdateView.swift 3 | // PearHID 4 | // 5 | // Created by Alin Lupascu on 4/4/25. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | import AlinFoundation 11 | 12 | struct UpdateView: View { 13 | @EnvironmentObject var updater: Updater 14 | 15 | var body: some View { 16 | VStack(spacing: 10) { 17 | 18 | GroupBox(label: Text("Frequency").font(.title2).padding(.bottom, 5), content: { 19 | FrequencyView(updater: updater) 20 | .padding(5) 21 | }) 22 | 23 | 24 | GroupBox(label: Text("Releases").font(.title2).padding(.bottom, 5), content: { 25 | RecentReleasesView(updater: updater) 26 | .frame(height: 200) 27 | .frame(maxWidth: .infinity) 28 | }) 29 | 30 | 31 | HStack(alignment: .center, spacing: 20) { 32 | 33 | Button { 34 | updater.checkForUpdates(sheet: false) 35 | } label: { EmptyView() } 36 | .buttonStyle(SimpleButtonStyle(icon: "arrow.uturn.left.circle", label: String(localized: "Refresh"), help: String(localized: "Refresh updater"))) 37 | .contextMenu { 38 | Button("Force Refresh") { 39 | updater.checkForUpdates(sheet: true, force: true) 40 | } 41 | } 42 | 43 | 44 | Button { 45 | updater.resetAnnouncementAlert() 46 | } label: { EmptyView() } 47 | .buttonStyle(SimpleButtonStyle(icon: "star", label: String(localized: "Announcement"), help: String(localized: "Show announcements badge again"))) 48 | 49 | 50 | Button { 51 | NSWorkspace.shared.open(URL(string: "https://github.com/alienator88/PearHID/releases")!) 52 | } label: { EmptyView() } 53 | .buttonStyle(SimpleButtonStyle(icon: "link", label: String(localized: "Releases"), help: String(localized: "View releases on GitHub"))) 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /PearHID/Styles.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Styles.swift 3 | // PearHID 4 | // 5 | // Created by Alin Lupascu on 4/4/25. 6 | // 7 | 8 | import SwiftUI 9 | import Foundation 10 | import AlinFoundation 11 | 12 | struct HelperBadge: View { 13 | 14 | var body: some View { 15 | AlertNotification(label: "Enable Helper", icon: "lock", buttonAction: { 16 | openAppSettings() 17 | }, btnColor: Color.blue, hideLabel: true) 18 | 19 | } 20 | } 21 | 22 | struct CheckmarkOverlay: View { 23 | @Binding var show: Bool 24 | 25 | var body: some View { 26 | if show { 27 | Image(systemName: "checkmark") 28 | .font(.title) 29 | .padding() 30 | .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 12)) 31 | .transition(.scale) 32 | .onAppear { 33 | DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { 34 | withAnimation { 35 | show = false 36 | } 37 | } 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /PearHID/Utilities.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Untitled.swift 3 | // PearHID 4 | // 5 | // Created by Alin Lupascu on 12/20/24. 6 | // 7 | import Foundation 8 | import AppKit 9 | import AlinFoundation 10 | import SwiftUI 11 | 12 | func checkAndRequestAccessibilityPermission() { 13 | if !isAccessibilityGranted() { 14 | // Show alert to the user 15 | let alert = NSAlert() 16 | alert.messageText = "Accessibility Permission" 17 | alert.informativeText = "\(Bundle.main.name) needs Accessibility permission to set hidutil properties." 18 | alert.addButton(withTitle: "Open Settings") 19 | alert.addButton(withTitle: "Cancel") 20 | 21 | let response = alert.runModal() 22 | if response == .alertFirstButtonReturn { 23 | // Open the Accessibility System Settings pane 24 | openAccessibilitySettings() 25 | } 26 | } 27 | } 28 | 29 | func isAccessibilityGranted() -> Bool { 30 | let options: NSDictionary = [kAXTrustedCheckOptionPrompt.takeUnretainedValue() as NSString: false] 31 | return AXIsProcessTrustedWithOptions(options) 32 | } 33 | 34 | func openAccessibilitySettings() { 35 | let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility")! 36 | NSWorkspace.shared.open(url) 37 | } 38 | 39 | 40 | func openAppSettings() { 41 | if #available(macOS 14.0, *) { 42 | @Environment(\.openSettings) var openSettings 43 | openSettings() 44 | } else { 45 | NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /PearHIDHelper/com.alienator88.PearHID.Helper.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Label 6 | com.alienator88.PearHID.Helper 7 | BundleProgram 8 | Contents/MacOS/PearHIDHelper 9 | MachServices 10 | 11 | com.alienator88.PearHID.Helper 12 | 13 | 14 | AssociatedBundleIdentifiers 15 | 16 | com.alienator88.PearHID 17 | 18 | 19 | -------------------------------------------------------------------------------- /PearHIDHelper/main.swift: -------------------------------------------------------------------------------- 1 | // 2 | // main.swift 3 | // PearHIDHelper 4 | // 5 | // Created by Alin Lupascu on 4/4/25. 6 | // 7 | 8 | import Foundation 9 | 10 | @objc(HelperToolProtocol) 11 | public protocol HelperToolProtocol { 12 | func runCommand(command: String, withReply reply: @escaping (Bool, String) -> Void) 13 | } 14 | 15 | // XPC Communication setup 16 | class HelperToolDelegate: NSObject, NSXPCListenerDelegate, HelperToolProtocol { 17 | private var activeConnections = Set() 18 | 19 | override init() { 20 | super.init() 21 | } 22 | 23 | // Accept new XPC connections by setting up the exported interface and object. 24 | func listener(_ listener: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool { 25 | newConnection.exportedInterface = NSXPCInterface(with: HelperToolProtocol.self) 26 | newConnection.exportedObject = self 27 | newConnection.invalidationHandler = { [weak self] in 28 | self?.activeConnections.remove(newConnection) 29 | if self?.activeConnections.isEmpty == true { 30 | exit(0) // Exit when no active connections remain 31 | } 32 | } 33 | activeConnections.insert(newConnection) 34 | newConnection.resume() 35 | return true 36 | } 37 | 38 | // Execute the shell command and reply with output. 39 | func runCommand(command: String, withReply reply: @escaping (Bool, String) -> Void) { 40 | let process = Process() 41 | process.executableURL = URL(fileURLWithPath: "/bin/bash") 42 | process.arguments = ["-c", command] 43 | let pipe = Pipe() 44 | process.standardOutput = pipe 45 | process.standardError = pipe 46 | do { 47 | try process.run() 48 | process.waitUntilExit() 49 | } catch { 50 | reply(false, "Failed to run command: \(error.localizedDescription)") 51 | return 52 | } 53 | let data = pipe.fileHandleForReading.readDataToEndOfFile() 54 | let output = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" 55 | let success = (process.terminationStatus == 0) // Check if process exited successfully 56 | reply(success, output.isEmpty ? "No output" : output) 57 | } 58 | } 59 | 60 | // Set up and start the XPC listener. 61 | let delegate = HelperToolDelegate() 62 | let listener = NSXPCListener(machServiceName: "com.alienator88.PearHID.Helper") 63 | listener.delegate = delegate 64 | listener.resume() 65 | RunLoop.main.run() 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PearHID 2 |

3 | 4 |
5 | Status: Maintained 6 |
7 | Version: 1.0.0 (BETA) 8 |
9 | Download 10 | · 11 | Commits 12 |
13 |
14 |

15 |
16 | 17 | Easily swap keyboard keys with a nice SwiftUI frontend for IOKit.hid/hidutil 18 | 19 | 20 | ## Features 21 | - Save/clear multiple key combinations at once 22 | - Save to launchd plist to persist reboots 23 | - Turn off persist in settings to only affect the current session and disable launch daemon 24 | - Helper tool to perform the launchd plist editing without asking for user password each time 25 | - Custom auto-updater that pulls latest release notes and binaries from GitHub Releases 26 | 27 | 28 | ## Preview 29 | ![image](https://github.com/user-attachments/assets/e9887a1d-44d4-4b89-9b26-edc00551ca87) 30 | 31 | 32 | ## Requirements 33 | - MacOS 13.0+ (App uses some newer SwiftUI functions/modifiers which don't work on anything lower than 13.0) 34 | 35 | 36 | ## Getting PearHID 37 | 38 |
39 | Releases 40 | 41 | Pre-compiled, always up-to-date versions are available from my [releases](https://github.com/alienator88/PearHID/releases) page. 42 |
43 | 44 |
45 | Homebrew Coming Soon 46 | 47 | You can add the app via Homebrew: 48 | ``` 49 | 50 | ``` 51 |
52 | 53 | 54 | ## License 55 | > [!IMPORTANT] 56 | > PearHID is licensed under Apache 2.0 with [Commons Clause](https://commonsclause.com/). This means that you can do anything you'd like with the source, modify it, contribute to it, etc., but the license explicitly prohibits any form of monetization for PearHID or any modified versions of it. See full license [HERE](https://github.com/alienator88/Sentinel/blob/main/LICENSE.md) 57 | 58 | ## Thanks 59 | [This Gist](https://gist.github.com/bennlee/0f5bc8dc15a53b2cc1c81cd92363bf18) 60 | 61 | [hidutil-key-remapping-generator](https://github.com/amarsyla/hidutil-key-remapping-generator) 62 | 63 | ## Some of my apps 64 | 65 | [Pearcleaner](https://github.com/alienator88/Pearcleaner) - An opensource app cleaner with privacy in mind 66 | 67 | [Sentinel](https://github.com/alienator88/Sentinel) - A GUI for controlling gatekeeper status on your mac 68 | 69 | [Viz](https://github.com/alienator88/Viz) - Utility for extracting text from images, videos, qr/barcodes 70 | -------------------------------------------------------------------------------- /Resources/Icons/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alienator88/PearHID/a34e891ae2a96da3d7702a16a15f73157d5fe8c0/Resources/Icons/icon_128x128.png -------------------------------------------------------------------------------- /Resources/Icons/icon_128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alienator88/PearHID/a34e891ae2a96da3d7702a16a15f73157d5fe8c0/Resources/Icons/icon_128x128@2x.png -------------------------------------------------------------------------------- /Resources/Icons/icon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alienator88/PearHID/a34e891ae2a96da3d7702a16a15f73157d5fe8c0/Resources/Icons/icon_16x16.png -------------------------------------------------------------------------------- /Resources/Icons/icon_16x16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alienator88/PearHID/a34e891ae2a96da3d7702a16a15f73157d5fe8c0/Resources/Icons/icon_16x16@2x.png -------------------------------------------------------------------------------- /Resources/Icons/icon_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alienator88/PearHID/a34e891ae2a96da3d7702a16a15f73157d5fe8c0/Resources/Icons/icon_256x256.png -------------------------------------------------------------------------------- /Resources/Icons/icon_256x256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alienator88/PearHID/a34e891ae2a96da3d7702a16a15f73157d5fe8c0/Resources/Icons/icon_256x256@2x.png -------------------------------------------------------------------------------- /Resources/Icons/icon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alienator88/PearHID/a34e891ae2a96da3d7702a16a15f73157d5fe8c0/Resources/Icons/icon_32x32.png -------------------------------------------------------------------------------- /Resources/Icons/icon_32x32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alienator88/PearHID/a34e891ae2a96da3d7702a16a15f73157d5fe8c0/Resources/Icons/icon_32x32@2x.png -------------------------------------------------------------------------------- /Resources/Icons/icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alienator88/PearHID/a34e891ae2a96da3d7702a16a15f73157d5fe8c0/Resources/Icons/icon_512x512.png -------------------------------------------------------------------------------- /Resources/Icons/icon_512x512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alienator88/PearHID/a34e891ae2a96da3d7702a16a15f73157d5fe8c0/Resources/Icons/icon_512x512@2x.png -------------------------------------------------------------------------------- /announcements.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | } 4 | --------------------------------------------------------------------------------