Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .Jules/palette.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,6 @@
**Learning:** Found a pattern where SwiftUI icon-only buttons or minimal UI elements were given `.help()` modifiers for hover tooltips but lacked `.accessibilityLabel()` modifiers for screen readers.
**Action:** When adding `.help()` to buttons, always pair it with a corresponding `.accessibilityLabel()` to ensure full accessibility.

## 2024-07-12 - [Dynamic Accessibility Labels for List Item Actions]
**Learning:** Icon-only buttons (like Edit/Delete) inside lists pose a major accessibility challenge for VoiceOver users when identical labels ("Edit") are repeated without context, making it impossible to know which row is being acted on.
**Action:** Always interpolate the dynamic item context (e.g., `item.name`) into both `.help()` tooltips and `.accessibilityLabel()` modifiers in repeated SwiftUI lists. Include fallback text for empty states (e.g., `"Unnamed Item"`).
## 2023-10-25 - Added Context to Icon-Only List Buttons
**Learning:** Found an icon-only list item checkbox button (`onToggleSelect` in `CommunityProfileRow`) missing accessibility/help labels. Unlike standard text buttons, icon-only interaction targets must always explicitly provide context (via `.help` and `.accessibilityLabel`), particularly within repeated lists where context comes from dynamic adjacent content (`profileInfo.displayName`).
**Action:** Always verify icon-only interactive elements in loops or lists. Append `.help` and `.accessibilityLabel` with dynamic context using the current loop variable's properties to explicitly specify *what* the button is selecting/deselecting.
4 changes: 0 additions & 4 deletions .Jules/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,3 @@
**Vulnerability:** The application was falling back to storing OBS passwords as plaintext strings inside exported/saved JSON models (`Macro.swift` and `SystemCommand.swift`) if saving to the macOS Keychain failed.
**Learning:** Saving secrets to unencrypted formats simply because secure storage fails is a critical anti-pattern known as "failing open" that results in data exposure.
**Prevention:** Always fail securely. If secure storage operations fail, discard the sensitive credential in memory rather than writing it insecurely to disk, even if it requires the user to re-authenticate later.
## 2026-06-26 - [Denial of Service via Pipe Deadlock]
**Vulnerability:** The application executed `Foundation.Process` shell commands and waited for them to exit (`process.waitUntilExit()`) before attempting to read the standard output and error pipes.
**Learning:** If a child process writes more data than the operating system's pipe buffer can hold (typically ~64KB), the child process will block waiting for the parent to read the data. If the parent is blocked on `waitUntilExit()`, a deadlock occurs, resulting in a Denial of Service.
**Prevention:** Always read data from process pipes (e.g., using `fileHandleForReading.readDataToEndOfFile()`) *before* calling `process.waitUntilExit()` to ensure the pipe buffer is drained and the child process can finish executing. However, ensure that this fix does not introduce deadlocks when used with handlers like `readabilityHandler` or processes that don't close their streams until parent exit.
10 changes: 7 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,13 @@ jobs:
- name: Run tests
run: |
set -o pipefail
TEST_DERIVED_DATA=DerivedData \
TEST_DESTINATION='platform=macOS,arch=arm64' \
./Scripts/run-direct-xctest.sh \
xcodebuild test \
-project XboxControllerMapper/XboxControllerMapper.xcodeproj \
-scheme XboxControllerMapper \
-destination 'platform=macOS,arch=arm64' \
-derivedDataPath DerivedData \
CODE_SIGN_IDENTITY=- \
CODE_SIGNING_REQUIRED=NO \
2>&1 | tee xcodebuild.log | grep -vE '^\s*$' | tail -200

- name: Upload log on failure
Expand Down
6 changes: 1 addition & 5 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,7 @@ release/
DerivedData/

# Debug/diagnostic tools
Tools/*
!Tools/oura-calibration/
!Tools/oura-calibration/**
Tools/oura-calibration/captures/
Tools/oura-calibration/__pycache__/
Tools/

# Marketing docs
marketing/
Expand Down
21 changes: 0 additions & 21 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,20 +238,16 @@ Invariants the store integration maintains:
|---------|------|---------------|
| `ControllerService` | Services/ControllerService.swift | Controller connection, raw input handling, haptics, battery, DualSense HID |
| `MappingEngine` | Services/MappingEngine.swift | Button->action mapping, chords, long hold, double tap, macros, system commands |
| `ControllerInputEvent` | Services/Mapping/ControllerInputEvent.swift | Event envelope and queue policy for MappingEngine controller callbacks |
| `InputSimulator` | Services/InputSimulator.swift | CGEvent-based keyboard/mouse output |
| `ModifierKeyEmissionPolicy` | Services/Input/ModifierKeyEmissionPolicy.swift | Pure side-aware modifier key selection for InputSimulator |
| `ProfileManager` | Services/ProfileManager.swift | Profile CRUD, persistence, backup |
| `AppMonitor` | Services/AppMonitor.swift | Active app detection for profile auto-switching |
| `XboxGuideMonitor` | Services/XboxGuideMonitor.swift | IOKit HID for Xbox Guide button (swallowed by GameController) |
| `GameControllerDatabase` | Services/GameControllerDatabase.swift | SDL database parsing and lookup |
| `GenericHIDController` | Services/GenericHIDController.swift | Raw HID input for third-party controllers |
| `HIDControllerDriverDescriptor` | Services/Controller/HIDControllerDriverDescriptor.swift | Testable raw-HID matching criteria for specialized controller backends |
| `OnScreenKeyboardManager` | Services/OnScreenKeyboardManager.swift | Floating keyboard overlay window |
| `CommandWheelManager` | Services/CommandWheelManager.swift | Radial menu for app/website switching |
| `InputLogService` | Services/InputLogService.swift | Debug input logging |
| `SystemCommandExecutor` | Services/SystemCommandExecutor.swift | Shell commands, app launch, URL open |
| `ControllerVisualDescriptor` | Views/MainWindow/ControllerVisualDescriptor.swift | Preview capability/row descriptor for controller mapping canvas |

---

Expand Down Expand Up @@ -348,20 +344,3 @@ grep -oE '[0-9]+ NSDisplayCycleFlush' /tmp/ck_sample.txt | awk '{sum+=$1} END {p
- **Reduce mouse event rate to 60Hz with velocity-adaptive posting** — post at 120Hz during slow/precise movement, drop to 60Hz during fast sweeps where individual frames aren't perceptible. Would halve IPC cost during fast movement.
- **Separate NSHostingView for ControllerAnalogOverlay** — isolates the analog overlay's `@State` updates from the main view tree's layout pass.
- **CALayer-based analog overlay** — replace SwiftUI analog rendering with Core Graphics/CALayer. Layer property updates don't trigger view tree diffing.

## Pref-verification gotcha

ControllerKeys is NOT sandboxed (entitlements set `com.apple.security.app-sandbox = false`). The leftover `~/Library/Containers/KevinTang.XboxControllerMapper/` dir is a red herring — real prefs live at `~/Library/Preferences/KevinTang.XboxControllerMapper.plist` (`defaults read KevinTang.XboxControllerMapper`).

Do NOT verify a UI-pref change by `defaults write` then relaunching — cfprefsd caching makes the running app read a stale value even when `defaults read` shows the new one. Click the real control instead (synthetic Quartz `CGEventPost` clicks work). Also: `codesign -d --entitlements -` prints the `app-sandbox` key even when its value is `false` — use `--xml … | plutil -p -` for the actual boolean.

Project-file note: the Xcode project is objectVersion 77 (`PBXFileSystemSynchronizedRootGroup`) — Swift sources are auto-discovered; do NOT run `add-xcode-files.py` here (only frameworks need explicit PBXBuildFile entries).

## Licensing & release internals

- **Trial clock** anchored in the login keychain (service `com.controllerkeys.license`, account `trialStart`) so delete+reinstall does NOT reset the 14-day trial. License key + `licensedConfirmed` flag stored the same way; a confirmed license stays valid offline.
- **Gumroad verify:** POSTs to `https://api.gumroad.com/v2/licenses/verify` with `product_id=9AsXgsuZVzxgNYfoVcFW1A==` (percent-encode so the trailing `==` survives), `increment_uses_count=false`; rejects refunded/disputed/chargebacked. The Gumroad product MUST have license keys enabled.
- **Enforcement:** `LicenseManager.enforce { mappingEngine.isEnabled = false }` in `XboxControllerMapperApp.init` (skipped in screenshot mode); fires at startup + on transition to expired. Dev builds compile in `DEV_BYPASS_LICENSE` via `make install`; the release never sets it.
- **Sparkle:** `SUFeedURL = https://raw.githubusercontent.com/NSEvent/xbox-controller-mapper/main/appcast.xml`, `SUPublicEDKey = UE8E+aypYDGJ7X8LznvFn1YBdENZDo5qrsX93K1/1Js=` (private EdDSA key in Kevin's login keychain — back it up). CFBundleVersion (BUILD_NUMBER) MUST increase every release or Sparkle won't detect the update.
- **Notarization re-sign (do not remove):** `Scripts/sign-and-notarize.sh` re-signs Sparkle's bundled `Updater.app`, `Autoupdate`, `XPCServices/*.xpc` inside-out, re-signs the framework, then re-seals the app — Sparkle ships them with its own signature, which Apple notarization rejects.
- **Release pipeline:** `Scripts/release.sh` EdDSA-signs the DMG from a clean staging dir (one-item feed), writes/commits/pushes `appcast.xml`, attaches the DMG to the GitHub release, and auto-bumps the homebrew tap cask (`NSEvent/homebrew-tap`, version + sha256). Public free download gated only by the in-app trial.
66 changes: 0 additions & 66 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,72 +5,6 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [2.6.0] - 2026-07-13

### Added

- **Oura Ring motion control**: ControllerKeys can now read an Oura smart ring as a controller—tilt your hand to steer the mouse, or route the same motion to scrolling, arrow keys, WASD, the D-pad, or four custom directional actions. A dedicated Ring tab (Settings → Ring) handles pairing plus a full calibration pass: orientation, invert X/Y, sensitivity, horizontal and left-hand boost for wide screens, deadzone, and smoothing, with Scan, Center, Pause Motion, and Forget Pairing controls. The ring auto-recenters when your hand drifts at rest so a stale neutral pose stops manufacturing phantom cursor drift, and the connection recovers on its own after Bluetooth drops or the stream stalls.

- **Oura Ring gestures**: Finger taps (single, double, triple, and five-tap), tap-and-hold, and directional flicks (up, down, left, right) register as bindable virtual buttons. Out of the box a tap left-clicks, a double-tap re-centers the ring, and a triple-tap toggles motion; remap any of them like a normal controller button. Gestures are recognized on-device by a Core ML classifier trained on labeled ring sessions, tuned to fire taps quickly while rejecting the hand-settle rebound that used to read as a phantom second tap.

### Changed

- **First-run onboarding requests Input Monitoring before Accessibility**: Granting Accessibility first could make macOS report input access as already granted without ever registering ControllerKeys in the Input Monitoring list, leaving you to add it by hand. The setup wizard now requests Input Monitoring first—pausing briefly so macOS finishes registering before System Settings opens the list—so the app appears on its own, and the manual-add fallback is spelled out as numbered steps.

### Fixed

- **No-controller chooser no longer drops rows after scrolling**: In a short window, scrolling the Buttons tab so the "No controller connected" chooser is partly clipped and then returning to the top could leave some controller-family buttons missing after SwiftUI refreshed the view. The chooser now uses an eager static grid instead of a nested `LazyVGrid`, so the small fixed set of controller buttons stays mounted and redraws reliably. Each pick also labels its console family (PS5, PS4, Switch, Xbox) under the name, so it's clear which layout you're choosing.

- **Live permission status stays current with more than one view open**: The permissions poller is now reference-counted, so closing one permissions view—like the first-run wizard—no longer stops the status pills in another open view (Settings → General → Permissions) from flipping to "Granted" the moment you toggle a switch in System Settings.

## [2.5.1] - 2026-07-03

### Added

- **Analog trigger precision control**: Use LT/L2, RT/R2, or either trigger as a continuous precision control for thumbstick and touchpad cursor movement. The deeper you pull, the slower the cursor moves, with sliders for minimum speed, trigger deadzone, and response curve. It is off by default and stacks with existing trigger button mappings instead of replacing them; the Joysticks settings UI now recommends RT/R2 first and warns when the selected trigger also has a button action or overlaps with left-trigger swipe typing.

## [2.5.0] - 2026-07-02

### Added

- **FPS / pointer-lock relative mouse mode**: Games that capture the mouse—browser FPS games, aim trainers, anything using the Pointer Lock API—need an endless stream of relative mouse motion, but stick aiming previously moved the real cursor to absolute screen positions, so aiming died at the screen edge and the cursor could escape the game onto the desktop. A new mode in Settings → Joysticks posts true relative movement while a game has the mouse captured: full 360° aiming, no edge stops, and the cursor stays pinned inside the game. Auto (the default) detects capture by watching for the game hiding the system cursor and switches back the moment it's released; Always is for dedicated per-app game profiles. One caveat: games that opt into raw input (`unadjustedMovement`) read the physical mouse hardware directly and can't see synthetic input from any macOS software—turn the game's raw-input setting off to aim with ControllerKeys.

- **Google Slides / PowerPoint community profile**: A presentation-remote profile with a Start Presentation button, so a controller can drive slide decks out of the box.

### Fixed

- **Controller switching is harder to break**: Handing control to a second connected controller now waits for the active controller to go quiet and return to neutral first, so a drifting stick or a held button can no longer yank control away mid-input. Haptic sessions are also invalidated on switch, so rumble queued by the previous controller no longer fires on the new one.

- **8BitDo Micro d-pad routes as a d-pad**: When macOS exposes the Micro's physical d-pad as a Direction Pad (Android mode), it now binds as real d-pad buttons instead of doubling as the left-stick fallback, matching what the mapping canvas shows.

## [2.4.0] - 2026-06-25

### Added

- **Independent left/right stick tuning**: Each analog stick now has its own sensitivity, acceleration, and deadzone, so you can keep one stick slow for pixel-precise aiming and the other fast to fling the cursor across a wide monitor—even with both sticks set to mouse mode. Previously the two sticks shared a single sensitivity, so tuning one changed the other. Set them independently in Settings → Joysticks.

- **Per-layer joystick overrides**: Any layer can now override a stick's mode and tuning while it's held, and anything you don't set falls through to your base settings—the same transparency model layers already use for button mappings. Hold a layer trigger to make the left stick fast and release for fine control, without touching the other stick. Configure them in the new Per-Layer Overrides section of the Joysticks tab.

### Fixed

- **Joystick deadzone and invert controls follow the selected mode**: The Deadzone slider and Invert Y toggle edited the scroll-mode field on the right stick and the mouse-mode field on the left regardless of which mode was actually selected, so they had no effect once a stick was switched away from its default mode. They now bind to the field the chosen mode actually uses.

## [2.3.0] - 2026-06-25

### Added

- **⌘K command palette**: A Spotlight-style jump bar — press ⌘K or the new toolbar button — to go straight to any section, controller button, or bound shortcut without walking the nested tab bar. Sections are searchable by what they do (typing "javascript" finds Scripts, "latency" finds Input), with arrow-key, Return, and Escape navigation.

### Fixed

- **Touchpad cursor lag and path replay**: Using the DualSense touchpad as a mouse could feel laggy and re-trace the path you just swiped under high-rate input — most noticeable through a Bluetooth-to-USB controller adapter, but reproducible wired too. Touchpad samples are now coalesced into a single net movement per drain, so a burst can no longer back up the input queue and replay the motion.

- **Expanded automation URL-scheme blocklist**: TriggerKit automation URL handling now also rejects `shortcuts`, `terminal`, `ssh`, `telnet`, `vnc`, `ftp`, `smb`, and `afp` schemes, closing local-execution and sandbox-escape vectors that the earlier `file` and system-scheme block didn't cover.

- **OBS WebSocket password no longer falls back to plaintext**: When Keychain storage failed, the OBS password used by automation commands was previously written to disk in plaintext as a fallback. It now fails securely and discards the password instead of persisting it unencrypted.

- **Accessibility labels for tooltip-only buttons**: Controls that previously exposed only a hover tooltip — the ⌘K command palette, Settings, Profiles, trial status, and several mapping panels — now carry VoiceOver accessibility labels.

## [2.2.1] - 2026-06-19

### Fixed
Expand Down
1 change: 0 additions & 1 deletion FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,6 @@ This document lists all features for verification after refactoring.
- [ ] Scroll acceleration
- [ ] Scroll boost multiplier
- [ ] Focus mode (precision control with modifier key)
- [ ] FPS / pointer-lock relative mouse mode (Auto / Off / Always) — 360° aiming in games that capture the mouse

## DualSense (PS5) Features

Expand Down
8 changes: 2 additions & 6 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ INFO_PLIST := XboxControllerMapper/XboxControllerMapper/Info.plist
HELPER_SRC := Helpers/XboxEliteHelper.swift
HELPER_NAME := XboxEliteHelper

.PHONY: build install clean release sign-and-notarize app-path help check-permissions check-version-plist test-regressions test-full test-full-xcodebuild test-clean refactor-gate screenshots demo-gifs sync-website marketing-assets
.PHONY: build install clean release sign-and-notarize app-path help check-permissions check-version-plist test-regressions test-full test-clean refactor-gate screenshots demo-gifs sync-website marketing-assets

help:
@echo "ControllerKeys - Build Commands"
Expand All @@ -78,8 +78,7 @@ help:
@echo " make release - Full release workflow (sign, notarize, create GitHub release)"
@echo " make app-path - Show the built app path"
@echo " make test-regressions - Run focused regression suite"
@echo " make test-full - Run full test suite via direct XCTest"
@echo " make test-full-xcodebuild - Run full suite through Xcode's app-host launcher"
@echo " make test-full - Run full test suite"
@echo " make test-clean - Remove cached test derived data"
@echo " make refactor-gate - Run full suite (gate for refactors)"
@echo " make screenshots - Capture marketing screenshots (all controller variants)"
Expand Down Expand Up @@ -186,9 +185,6 @@ test-regressions:
-only-testing:XboxControllerMapperTests/JoystickAndMouseMappingTests/testJoystickMouseMovement

test-full:
PROJECT="$(PROJECT)" SCHEME="$(SCHEME)" TEST_DERIVED_DATA="$(TEST_DERIVED_DATA)" ./Scripts/run-direct-xctest.sh

test-full-xcodebuild:
xcodebuild test -project $(PROJECT) -scheme $(SCHEME) -derivedDataPath $(TEST_DERIVED_DATA) -destination 'platform=macOS'

test-clean:
Expand Down
3 changes: 0 additions & 3 deletions OuraGestureModel/.gitignore

This file was deleted.

Loading
Loading