Skip to content
Claude Merch

Plaude-Code

A macOS menu bar app that turns a PlayStation controller into keyboard shortcuts and snippets especially for Ghostty and Claude Code, so you can drive your terminal from the couch without a keyboard.

Core trick

Maps DualSense controller input through GameController and IOKit HID to Carbon key events and CoreGraphics pointer moves in a macOS menu bar app.

Tags

Built with

appkitgamecontrollerswiftswiftui

Description

Project story Inspiration I spend a lot of time in a terminal (Ghostty) and sometimes want to work from the couch with a DualSense instead of reaching for the keyboard. macOS doesn’t ship a good story for “controller → real keyboard input into arbitrary apps,” so I built a small bridge: map buttons to keys, snippets, and pointer control, with a cheatsheet so I don’t forget what I assigned. What I learned GameController is fine for sticks and buttons, but touchpad-style data on DualSense often means dropping to IOKit HID and reconciling two sources of truth. Accessibility isn’t optional if you want synthetic keys to land in the focused app; users have to grant it, and the UI has to stay honest about when injection will fail. SwiftUI + AppKit together are workable for a menu bar app, but window presentation ( openWindow , hidden opener windows) takes some care so flows like “open cheatsheet from the menu” don’t race launch order. How I built it The app is a Swift package producing a macOS executable. A ControllerManager reads gamepad state; bindings live in a MappingStore and route through an ActionRouter to emitters: Carbon -based key events, paste where needed, and CoreGraphics for pointer moves and clicks when the touchpad path is enabled. UI is SwiftUI ( MenuBarExtra , settings, cheatsheet) with AppKit where window behavior matters. Permission state is checked against Application Services APIs so the menu bar reflects reality. If you care about timing in one line: treating input as discrete samples every frame suggests a bound like \( \Delta t \leq 1/f_s \) for a sampling rate \( f_s \); in practice I cared more about debouncing and not double-firing on the same physical press than about formal sampling theory. For anything that deserved a clear “what happened” line in logs, a small NDJSON debug path helped without spamming the console. Challenges HID vs GameController : merging touchpad HID data with controller buttons without weird drift or missed gestures. Focus : optionally bringing Ghostty forward before sending keys so input doesn’t disappear into the wrong window. macOS permissions : explaining Accessibility in plain language and recovering when the user toggles it in System Settings without restarting the app. SwiftUI window IDs : cheatsheet and settings flows needed explicit registration so “open from menu bar” worked on a cold start. https://www.loom.com/share/7a8c57538e414f5da40748a92357572b

Team