This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Ultimate Hacking Keyboard (UHK) firmware supporting two hardware generations:
- UHK80 (newer): Left half + Right half + Dongle, Zephyr/nRF Connect SDK, Bluetooth
- UHK60 (legacy): Single right half with optional modules, MCuXpresso SDK, USB only
./build.sh right make # Incremental build (normal development)
./build.sh right build # Full rebuild (after adding/moving files or fresh checkout)right/src/ - Main user-facing logic, shared between UHK60 and UHK80:
- Key mapping, macros, layer handling, and core features
macros/- Macro engine: commands, variables, displayconfig_parser/- Configuration parsingconfig_manager.c- Runtime configuration (Cfgstruct)
device/ - UHK80-specific hardware code:
- Peripheral drivers, OLED rendering, Bluetooth handling
- Similar role to
right/src/peripherals/for UHK60
shared/ - Code shared between platforms
In right/src/macros/commands.c:
- Create a dedicated function like
processMyCommand(parser_context_t* ctx)rather than inline code - Add case in
processCommand()switch (organized by first character) - Always consume arguments even in
Macros_DryRunmode - validation can be skipped but parsing must happen - Return
MacroResult_Finishedfor synchronous operations - Document in
doc-dev/reference-manual.md
Use proper parsing utilities:
IsEnd(ctx)to check if at end of input (handles context expansions)ConsumeToken(ctx, "keyword")to match specific keywordsConsumeAnyToken(ctx)to consume arbitrary tokensTokEnd(ctx->at, ctx->end)to find token boundaries before consuming
Avoid static buffers for string arguments - pass start/end pointers to called functions.
Don't manipulate ctx->at directly unless necessary - use Consume functions.
Example pattern for capturing token values:
const char* tokenStart = ctx->at;
const char* tokenEnd = TokEnd(ctx->at, ctx->end);
ConsumeAnyToken(ctx);
// Now use tokenStart/tokenEndIn str_utils.h:
string_segment_t- Pointer pair (start, end) for non-null-terminated stringsStrEqual(a, aEnd, b, bEnd)- Compare two segmentsIsEnd(ctx)- Check if parser is at end (use instead ofctx->at >= ctx->end)
In key_action.h:
KeyActionType_Keystroke- Regular key with optional modifiers and secondary roleKeyActionType_InlineMacro- Macro text pointerkeystroke.secondaryRole- Secondary role ID (seesecondary_role_driver.h)
ConfigManager_ResetConfiguration(false)- Reset config without reloading keymapMacro_ProcessSetCommand(ctx)- Execute a set command programmatically
doc-dev/reference-manual.md- Formal specification of all macro commandsdoc-dev/user-guide.md- User-facing macro documentationdoc-dev/other/- Internal docs (crash logs, testing, troubleshooting)
- 4 spaces indentation (no tabs), Unix line endings
- Always use explicit curly braces
- Extern functions:
UpperCamelCasewithGroupName_FunctionNamepattern - Static functions:
lowerCamelCase - Types:
snake_case_t - Do not use autoformatting tools - manually format code to match surrounding codebase style
Focus on functional aspects, not nitpicks. Only flag magic constants if they're used in multiple places and may need future changes. Don't require comments unless truly necessary.
Key gotchas discovered while setting up:
- Re-run
./build.sh updateafter any branch/manifest switch. The west modules (zephyr, nrf, …) are pinned perwest_nrfsdk.yml; if they don't match the current checkout, CMake configure fails withError finding board: uhk-80-right ... Malformed "build" section ... SchemaError. This is a Zephyr version mismatch, not aboard.ymlbug —./build.sh update(west update + patch) fixes it. - Single-device builds run in the foreground (
./build.sh right build), which is easiest for testing; multi-device (left right dongle) fans out into tmuxbuildsessionpanes. - Dongle link depends on
keyboard/sources.right/src/slave_drivers/kboot_driver.cis compiled for all UHK80 targets, butdevice/src/CMakeLists.txtonly compileskeyboard/*.c(which includesuart_modules.c) for left/right, not the dongle. So any symbolkboot_driver.cpulls fromuart_modules.c(e.g. theKbootUart_*UART-transport functions) must be stubbed/guarded for the dongle or the dongle link fails withundefined reference. - Agent npm shadow.
lib/agentis nested inside this repo; the firmware root'snode_modules/npm(11.8.0, pinned by the root lockfile) shadows nvm's npm during the Agent build and trips itscheck-node-version(>=11.13.0). Fix:npm install npm@11.13.0 --no-save --no-package-lockin the firmware root. Details in the reference doc above.