Your DualSense is a 18-button programmable keyboard for macOS.
Navigate terminals, browsers, and AI chat apps from your couch — without touching the keyboard. Context-aware mappings, Wispr voice dictation on one button, haptic feedback, zero dependencies.
You're on a call, reading docs, or chatting with an AI assistant. You keep reaching for the keyboard just to scroll, switch tabs, or trigger voice dictation. Your PS5 controller is sitting right there.
mac-dualsense maps every button to any keystroke, per app, with no drivers:
- In Warp: D-pad scrolls blocks, L1/R1 navigates tabs, Options runs the command
- In Claude/ChatGPT: Cross sends, Triangle triggers Wispr voice dictation, L1/R1 moves between chats
- In Arc/Chrome: Circle goes back, L1/R1 switches tabs, L3 focuses the URL bar
- Everywhere: D-pad = arrows, Cross = Return, Circle = Escape — the basics just work
Mappings switch automatically when you change the focused app.
- Grab the latest
.zipfrom Releases → - Drag mac-dualsense.app to
/Applications - Launch — a controller icon appears in the menu bar
- System Settings → Privacy & Security → Accessibility → enable mac-dualsense
macOS Gatekeeper warning on first launch: right-click the app → Open → Open.
Requires Xcode 16+ / Swift 6.
git clone https://github.com/sour4bh/mac-dualsense.git
cd mac-dualsense
native/scripts/install_app.shThis builds, installs to /Applications, and launches.
- Connect your DualSense via USB or Bluetooth
- Open the app — it auto-detects the controller
- The default config works immediately (D-pad, face buttons, shoulder buttons all mapped)
- Edit
~/Library/Application Support/cc-controller/mappings.yamlto customize — the app hot-reloads on save
Hold Triangle (or the PS button) to activate Wispr Flow. Dictate, release, done. Eight trigger modes available:
| Mode | How it works |
|---|---|
rcmd_hold |
Holds right ⌘ while the button is pressed |
rcmd_toggle |
Toggles right ⌘ on/off with each press |
rcmd_pulse |
Taps right ⌘ for hold_ms then releases |
fn_hold |
Holds Fn while pressed |
cmd_right |
Sends Cmd+Right |
Set settings.wispr.mode in your config to switch modes.
- Context-aware mappings — different actions per frontmost app, with a global fallback
- Profiles — define multiple full mapping sets and switch between them
- Haptic feedback — configurable intensity/duration patterns on connect, action, and error
- Visual controller map — interactive button layout in Preferences, click to assign
- Extended key support — function keys, media keys, special characters
- Modifier management — hold, release, or toggle any modifier key (useful for gaming-style hold-to-run)
- Multi-controller — auto-detects DualSense or Pro Controller, or pin a preference
- No runtime dependencies — native GameController framework, no HID drivers, no Homebrew
| Controller | Connection |
|---|---|
| Sony DualSense (PS5) | USB / Bluetooth |
| Nintendo Switch Pro Controller | USB / Bluetooth |
Global fallback (all apps)
| Button | Action |
|---|---|
| D-pad | Arrow keys |
| Cross (×) | Return |
| Circle (○) | Escape |
| Square (□) | Tab |
| Triangle (△) / PS | Wispr voice dictation |
| L1 / R1 | Cmd+Shift+[ / ] |
| L2 / R2 | Alt+↑ / Alt+↓ |
| L3 | Ctrl+C |
| R3 | Ctrl+D |
Per-app overrides (Warp, Arc, Chrome, Slack, ChatGPT, Claude)
Full bindings in native/Sources/CCControllerNative/Resources/mappings.yaml.
Config lives at ~/Library/Application Support/cc-controller/mappings.yaml, seeded from native/Sources/CCControllerNative/Resources/mappings.yaml on first run.
Full config structure
version: 2
settings:
controller:
preferred: auto # auto | dualsense | pro_controller
wispr:
mode: rcmd_hold
hold_ms: 450
profiles:
active: default
items:
default:
mappings:
default: # global fallback
cross:
type: keystroke
key: return
triangle:
type: wispr
warp: # active when Warp is frontmost
square:
type: keystroke
key: k
modifiers: [cmd]
haptics:
enabled: true
patterns:
confirm:
intensity: 128
duration_ms: 50Button names
| Button | Name | Button | Name | |
|---|---|---|---|---|
| Cross (×) | cross |
L1 | l1 |
|
| Circle (○) | circle |
R1 | r1 |
|
| Triangle (△) | triangle |
L2 | l2 |
|
| Square (□) | square |
R2 | r2 |
|
| D-pad up | dpad_up |
L3 (click) | l3 |
|
| D-pad down | dpad_down |
R3 (click) | r3 |
|
| D-pad left | dpad_left |
PS | ps |
|
| D-pad right | dpad_right |
Options | options |
|
| Touchpad click | touchpad |
Share/Create | share |
Action types
# Keystroke with optional modifiers
type: keystroke
key: k
modifiers: [cmd, shift] # cmd | shift | alt | ctrl
# Wispr voice dictation
type: wispr
# Hold/release/toggle a modifier key
type: modifier
modifier: rcmd # rcmd | lcmd | rshift | lshift | ralt | lalt | rctrl | lctrl
action: hold # hold | release | toggleAdding a new app context
- Find the app's bundle ID:
osascript -e 'id of app "AppName"' - Add it to the
contextsdict inAppFocus.swift - Add it to
knownContextsinConfigStore.swift - Add a mapping block in
mappings.yamlunderprofiles.items.<profile>.mappings.<context>
PRs welcome — especially new app context mappings and controller support. See AGENTS.md.
MIT — © 2025 Sourabh Sharma