GitHub - Mattie/joydex: Use your joystick/pad to control Codex hotkeys (alternative to Codex Micro keyboard)

GitHub

15 min read Original article ↗

Codex companion for a Joystick/Throttle

Why pay for a purpose-built Codex Micro keyboard when you already have a joystick, pad, or flight-sim throttle?

Just use Codex to set it up in a couple of hours and save yourself that extra $230. See the prompt I started with.

What is Joydex?

Joydex is a source example for turning a VIRPIL throttle into a physical control surface for the Codex desktop app. Mechanically, it reads the throttle through DirectInput, resolves the current Codex shortcuts, and injects the corresponding Windows input events. The result is a small Windows tray app.

It could be adapted pretty easily to other devices, but I wanted to show how easy it was to get going by using Codex itself to build the handlers. See the case study for the story of how this came together in just a few hours.

See the changelog for the latest updates.

Nothing rocket-science, but we live in an era where you can retrofit your own hardware to a new workflow without having to buy yet-another-device. Hopefully this inspires you to do more hacking.

My favorite bit is I can just flip a T3 switch and see a floating map of the throttle's current bindings. The map reads its labels from the active configuration, so remapped controls are reflected in the floating window and I just flip the switch off and it instantly vanishes.

Joydex floating CM3 quick-reference map

Quick start

Joydex is a source project, so the easiest path is to clone the repository, open it in Codex, and ask Codex to adapt it to the controllers and controls you actually use. The two checked-in configurations are safe starting points: joydex.example.json for a CM3 throttle, and joydex.advanced.example.json for the CM3 plus an Alpha/WarBRD stick. Both start in dry-run mode.

  1. Build and run Joydex with the example configuration closest to your hardware.
  2. Open Configure... from the tray. Use Load defaults if the CM3 Codex Micro layout is useful, then adjust or capture the devices, bindings, prompt pickers, and map controls you want.
  3. Open Testing / Advanced > Test controls... and exercise each button, encoder, mode, and maintained switch. Check both press and release events where the action depends on a held control.
  4. Save the configuration. Turn off Testing / Advanced > Dry run only after the event log matches the physical controls.

The task-status LEDs use Codex hooks, so they take a few extra steps to set up. Joydex can drive them directly over USB or through VIRPIL LinkTool.

What the experiment produced

Joydex runs as a Windows tray app and reads one or more controllers through background, non-exclusive DirectInput. It leaves controller firmware and VPC profiles alone. The included mapping uses the CM3's shifted button ranges to expose Codex controls across three dial positions, while device-qualified bindings can also use a VIRPIL Alpha/WarBRD or another attached controller.

flowchart LR
    A["Configured controller input"] --> B["Buffered DirectInput events"]
    B --> C["Binding and bank rules"]
    C --> D["Codex command ID"]
    D --> E["Current keybinding resolver"]
    E --> F["Windows SendInput"]
    F --> G["Codex desktop app"]
    B --> P["Prompt picker"]
    P --> F
    B --> T["Task-alert deep link"]
    T --> G
    H["Foreground and simulator guards"] --> F
    H --> T
    I["Dry run"] --> C
    I --> P
    I --> T
Loading

Screenshot of the UI bindings, but you can also just ask Codex to configure them for you if you wanted. UI is so much easier when it's just some "oh hey set M2 B3 to reject" and your agent does the rest.

Joydex configuration window showing the CM3 device and action mappings

The configuration window keeps bindings, prompt pickers, button maps, and general settings on separate tabs. Software banks live under General → Advanced because they are only needed for hardware modes that reuse logical button numbers.

Joydex General tab showing Safety and Open plus collapsed advanced software banks

FWIW, the checked-in code demonstrates:

  • Buffered joystick input, including encoder pulses that can begin and end between polling frames.
  • Banked mappings for VIRPIL's five-way shift profile, where the dial changes the logical button range instead of emitting its own button event.
  • A command catalog that resolves Codex's current shortcuts from %USERPROFILE%\.codex\keybindings.json immediately before dispatch.
  • Chords, sequences, held modifiers for push-to-talk dictation, mouse-wheel actions, and safe cleanup of injected key state.
  • Experimental live Voice Chat controls for starting and explicitly ending a call, including maintained-switch bindings. The maintained-switch flow was verified on physical VIRPIL hardware on 2026-08-05.
  • Foreground-process and simulator guards around every dispatched action.
  • Dry-run inspection, hot-plug recovery, configuration validation, and a floating hardware map.

Codex building handlers for Codex

The first version could have hard-coded a table of function keys. That would have gone stale as soon as a shortcut changed. Joydex instead treats Codex command IDs as the durable interface. A physical control maps to an action such as composer.submit or toggleSidebar; the resolver then finds the user's current keybinding and turns it into a Windows input sequence.

This led to several concrete edge cases:

  • A shortcut can be a chord, a multi-step sequence, a bare modifier used for push-to-talk, or explicitly unbound.
  • Codex may rewrite keybindings.json while Joydex is reading it, so the resolver retries and retains its last valid snapshot.
  • Prefix collisions can make two sequences ambiguous. Joydex detects those cases and blocks the action.
  • Held keys need cleanup after disconnects, shutdown, partial injection failures, and rejected releases.
  • A valid shortcut is still unsafe when Codex is in the background.

Those cases are covered in the test suite. The code remains small enough to trace from a DirectInput event through binding resolution to the final SendInput calls.

Joydex dry-run inspector showing raw CM3 input and resolved actions

Included CM3 layout

The checked-in starter configuration follows the Codex Micro controls for a CM3 five-way shift profile:

CM3 control Dial position Role
Base buttons B1-B6 M2 Reject, Fork, Plan, Approve, Open, Submit
Base buttons B1-B6 M3 Plan, Back, Sidebar, Forward, New task, Skills
Base buttons B1-B6 M4 Agents 1-6
Grip encoder EN (EN3/EN2/EN1) Any Prompt 1 up/down; push inserts selected or default prompt
Base encoder E1 Any Reasoning up/down; push toggles Fast mode
Five-way hat Any Plan, Forward, Sidebar, Back
Toggle T3 Any Hold the floating button map open

The floating map reads its labels from the active configuration, so remapped controls are reflected in the UI. Additional controllers and their map controls can be added separately.

Task-status LEDs

The LEDs turn the throttle into a ten-slot task monitor. Four primary tasks use B1, B2, B4, and B5 across M2-M4; those positions stay dark until assigned, while B3 and B6 retain each bank's normal color. Six overflow tasks use B1-B6 on M1, where every unassigned button stays dark. M5 remains available for ordinary commands with a medium-pink baseline. The Alpha grip LED shows the highest-priority state across all ten slots.

When a primary task clears, its button remains dark for five seconds before the earliest M1 overflow task moves into that position. The remaining overflow tasks then compact in their existing order.

Dim gray means running, yellow means the task needs attention, and low green means completed. Red is reserved for a future fault source. Pressing an assigned button opens that Codex task. Running and attention states remain assigned after navigation; opening a completed task clears its slot and restores the button's normal binding. Here is my CM3 lit with a few example tasks.

Set up task-status LEDs

These steps target a CM3 throttle and an Alpha/WarBRD stick. VIRPIL Controls LinkTool v3 remains the proven default. Direct USB is an experimental option for the same devices while the remaining hardware recovery checks are completed. Other hardware may need different LED mappings.

  1. Connect both VIRPIL devices, then start Joydex.
  2. Open Testing / Advanced > Task alerts / ignored tasks..., choose Configure LEDs..., and select Direct USB. Close LinkTool and any VPC utilities before you confirm the change. Direct USB does not need a LinkTool profile or listener.
  3. If you prefer LinkTool, select VIRPIL LinkTool, choose Show LED profile, load joydex-linktool.led.json, and start its listener on 127.0.0.1:4123.
  4. In the same Joydex window, choose Install / Repair hooks and confirm the status reads Hooks: installed. If Codex marks the new handlers for review, open its Hooks screen and trust the Joydex handlers; untrusted command hooks do not run.
  5. Make sure Task alerts is checked in the top level of the Joydex tray menu.
  6. Submit a test prompt in Codex. The Task Alerts Event stream should record it, Current state should gain a running assignment, and the corresponding LED should light.

Joydex task-alert status window showing current task assignments and integration controls

Joydex updates the LEDs whenever a task changes or you turn the M1-M5 dial. In Direct USB mode, Joydex writes temporary LED colors directly to the devices. In LinkTool mode, it sends the task state to LinkTool's local listener. Neither mode changes controller firmware or VPC profiles.

If you have a noisy chat (e.g. GPT-live voice chat) that you wish to ignore for the throttle LEDs and wireless pad, you can do this in the UI by selecting it under Current state or Event stream, then click Ignore selected ▾ to set that up.

Joydex ignored task status sources window with one workspace rule and an explicit re-enable action

AFAICT, Codex hooks do not expose task titles or project names. Joydex uses the task ID and working-directory folder name where it can show that.

See the LED status guide for setup, troubleshooting, and the full behavior for LED status tracking.

Prompt pickers and multiple controllers

The tray's Prompt pickers... editor supports up to three named prompt lists. Each list has its own default entry and captured Up, Down, and Insert controls. The controls can come from any configured DirectInput device.

  • The first encoder detent opens the non-activating picker on its default. Later detents wrap through the list.
  • Insert types the selected prompt at the current Codex caret. A per-prompt option can run the resolved Codex Submit action afterward; it is off by default.
  • [Exit / Nevermind], Escape, or another controller button closes the picker without typing or submitting.

Joydex prompt-picker tab showing the default EN3, EN2, and EN1 controls

Configured devices reconnect independently. Each supported device map has its own tray item, floating window position, and optional hold-to-show control from any configured controller. Configure it in Configure Joydex → Button Maps: select the target map row, choose its Hold source, click Capture hold-to-show, then move the control. The CM3 and Alpha/WarBRD maps can be visible at the same time.

The checked-in advanced configuration is a sanitized copy of a working CM3 plus Alpha/WarBRD setup. It demonstrates two device profiles, cross-device map controls, two prompt pickers, custom task navigation, and two-notch scrolling. Its device GUIDs are removed and dry run is enabled. The prompt lists show one real workflow and are meant to be edited.

Joydex Button Maps tab showing a CM3 hold-to-show control

Joydex Alpha/WarBRD floating button map

Custom button-map images

The repository includes a small calibrate-button-maps agent skill for adapting Joydex to another controller or repairing a shifted label region. Give this skill to your agent to help it build these dynamic button map diagrams.

Experimental wireless touchscreen

Joydex also includes an experimental ESPHome example for the ESP32-4848S040C_I: a 4-inch, 480×480 capacitive touchscreen that joins the normal 2.4 GHz LAN. It displays the same four primary task states as the throttle LEDs and provides touch controls for Task 1 through Task 4. The original firmware adds one large PLAN MODE control, while the opt-in bridge-v2 firmware provides two local pages. The task page has five wider footer controls: PLAN, FAST, SIDE, MUTE, and >. The arrow opens a TASK CONTROLS page with large APPROVE, DECLINE, NEW TASK, and FORK controls plus a <, PREV, SUBMIT, NEXT, > footer. The left arrow returns to the task page, and the gray final arrow is reserved for a future third page. Arrow presses stay on the panel; named commands use Joydex's existing semantic actions. Each bridge-v2 task card also shows the available working-directory folder name in a small centered footer; long names end in an ellipsis and full paths never reach the panel. The panel uses authenticated REST and Server-Sent Events to communicate with Joydex; Home Assistant and MQTT are not required.

Screenshot of it working (Magic card for scale):

ESP32-4848S040C_I running the current Joydex task page with workspace labels and five bottom controls beside a Magic: The Gathering card for scale

The example was physically tested July 25–27, 2026 on one panel purchased from this AliExpress listing. I have no affiliation with the seller, it could be malware-laden, I dunno, but I've bought a number of these for home assistant projects and use them in different spots. YMMV. Listings and board revisions can change, so verify the ESP32-4848S040C_I model (or ask your agent to figure it out for you). I used the GUITION specification for the one I tested.

Start with the ESPHome firmware guide. The repository includes a neutral white skin, the original dark bridge-console skin, and an opt-in bridge-v2 command-row skin. The original files remain available as rollback configurations. All variants require local credentials, a trusted LAN, and a private unit-specific factory backup. Compiled firmware is deliberately not distributed because it embeds the credentials used to join and manage the panel.

The wireless research record explains the direct ESPHome approach, while the device reference records the tested timing, pins, flashing, redraw, and recovery findings.

Build and explore the source

This is really more of a proof-of-concept that you can have Codex build a handler for this kind of thing instead of buying yet another custom device.

But if you want to build it, Joydex targets Windows and pins .NET SDK 8.0.423 in global.json.

dotnet restore .\Joydex.sln
dotnet build .\Joydex.sln --configuration Release
dotnet test .\Joydex.sln --configuration Release --no-build

For a dry-run source session, copy the complete CM3 starter configuration to a scratch path and pass it to the app project:

$config = Join-Path $env:TEMP 'joydex-example.json'
Copy-Item .\config\joydex.example.json $config
dotnet run --project .\src\Joydex.App -- --config $config

The scratch path keeps this run separate from %LOCALAPPDATA%\Joydex\config.json. The starter selects the throttle by product name and omits machine-specific DirectInput GUIDs. It includes the bindings and prompt picker shown above with dryRun enabled, so raw control events and resolved actions appear in the test window without being sent to Codex.

To explore the two-controller version, copy config\joydex.advanced.example.json to the scratch path instead. Its broad MT-50CM3 and WarBRD product-name selectors may need adjustment if more than one matching controller is attached.

When adapting the example to another DirectInput profile, use the tracer:

dotnet run --project .\tools\Joydex.Trace -- list
dotnet run --project .\tools\Joydex.Trace -- trace `
  --name "VPC Throttle MT-50CM3" `
  --seconds 60

Trace output uses one-based button numbers, matching config.json. Move one control at a time and test B1-B6 in every dial position.

Repo map

Path Purpose
src/Joydex.Core Configuration, multi-device input models, bindings, prompt pickers, and task-alert state
src/Joydex.Virpil Shared VIRPIL device access, LED control, and checks for competing LED writers
src/Joydex.Windows DirectInput, shortcut resolution and injection, safety guards, task links, hooks, and task-alert LED services
src/Joydex.App Tray lifecycle, configuration UI, dry-run inspector, prompt overlays, diagnostics, and button maps
src/Joydex.HookRelay Native hook command that forwards Codex lifecycle events to Joydex
src/Joydex.Guardian Crash cleanup for active task-status LEDs
tools/Joydex.Trace DirectInput discovery and event tracing
tests/Joydex.Tests Unit and Windows interop coverage
skills/calibrate-button-maps Agent workflow and utility for custom button-map region calibration
config/joydex.example.json Safe, machine-neutral starter configuration
config/joydex.advanced.example.json Sanitized two-controller working example
docs/ Case study, setup guides, research notes, and images

Versions and Config

Command IDs, Windows defaults, aliases, and precedence behavior were last checked on 2026-08-05 against OpenAI Codex package 26.730.8199.0, bundled app release 26.730, build 0.147.0-alpha.1.2.

The voice-chat action uses Codex's current Toggle voice chat shortcut. Before mapping side-conversation, end-voice-chat, or toggle-voice-mic, make sure Open Side Chat, End Voice Chat, and Toggle Voice Chat microphone have shortcuts assigned in Codex's Keyboard shortcuts.

Source builds use %LOCALAPPDATA%\Joydex\config.json, with JOYDEX_CONFIG and --config available for alternate paths. The graphical editor is the normal way to change mappings. Both checked-in example configurations are intended for dry-run exploration and contain no device GUIDs.

Likely asked questions

Could I do this with Joystick Gremlin instead?

Oh sure, quite likely. Joystick Gremlin can read the CM3 and map its buttons to keyboard or mouse input. If you want a fixed set of Codex shortcuts on your own PC, Gremlin is a much faster way to get started.

Joydex is just a little more specific to the companion use case. It stores Codex command IDs and looks up your current shortcuts before it sends a key. It catches missing or conflicting shortcuts, releases held keys after a disconnect, and checks that Codex has focus. With a basic Gremlin profile, you would update the mapped keys by hand when your Codex shortcuts change. You would also pause the profile when you switch to another app.

Joydex talks straight to the throttle, so it does not need a virtual joystick. Gremlin's supported setup uses vJoy and gives you another profile to keep track of. If you use Gremlin for flight sims, you may already have all of that.

Can it support the VIRPIL LEDs?

Yes. Joydex's built-in task-status LEDs are described above. Gremlin can also drive VIRPIL LEDs through Python scripts if a fixed keyboard profile is a better fit.

License and trademarks

The source code is available under the MIT License. Visual-asset provenance and third-party attribution are recorded in Third-party notices.

Joydex is an independent project and is not affiliated with or endorsed by VIRPIL Controls, OpenAI, or Work Louder. VIRPIL, VPC, OpenAI, ChatGPT, Codex, Work Louder, and associated marks are the property of their respective owners.