This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
This is a ZMK firmware configuration repository for custom split mechanical keyboards. It contains configurations for main keyboard: Hillside View. The repository uses GitHub Actions to automatically build firmware images.
-
boards/shields/- Shield definitions at repo root (discovered via module system)hillside_view/- Hillside View hardware definitions (.dtsi, .overlay, Kconfig files)
-
config/- Main configuration directory containing keymaps and per-user settings*.keymap- User-facing keymap files (hillside_view.keymap)*.conf- Configuration files for keyboard features*.json- Keymap editor JSON exports (editable via https://nickcoutsos.github.io/keymap-editor/)west.yml- West manifest defining ZMK version and external module dependencies
-
zephyr/module.yml- Declares this repo as a Zephyr module withboard_root: . -
build.yaml- Build matrix configuration defining which board+shield combinations to build -
.github/workflows/- GitHub Actions workflows for automated firmware builds
This repository is a Zephyr module declared via zephyr/module.yml (contains board_root: .). This means:
- Zephyr automatically discovers shield definitions in
./boards/shields/when this repo is listed as a module - Local builds pass
$(CURDIR)(the repo root) as the first entry inZMK_EXTRA_MODULES— the Makefile handles this automatically - CI uses ZMK's shared workflow (
zmkfirmware/zmk/.github/workflows/build-user-config.yml@v0.3) which also handles module discovery viabuild.yaml
This repository uses ZMK main branch in config/west.yml. To pin to a specific commit:
- Change the
revisionfield frommainto a specific commit hash - Test the build to ensure compatibility
- Update both
config/west.ymland the ZMK build directory'swest.ymlif building locally
Builds are defined in build.yaml and run via GitHub Actions using the ZMK shared workflow (zmkfirmware/zmk/.github/workflows/build-user-config.yml@v0.3). The build matrix includes:
- Hillside View: Split keyboard with nice!nano v2, nice_view_gem display and studio-rpc-usb-uart snippet
- Settings Reset: Utility builds for both nice!nano v2 and Seeeduino XIAO BLE
Build outputs are generated as firmware artifacts by the GitHub Actions workflow.
The repository uses several ZMK modules defined in config/west.yml:
nice-view-gem(M165437) - Nice!View custom display widgets with animationsprospector-zmk-module(carrefinho/feat/new-status-screens) - Status screen widgets and sensor support for Cygnus dongle
Note: Split peripheral input relay is now built into ZMK core as zmk,input-split (since PR #2477, Dec 2024)
Hillside View:
- 46-key split keyboard with Nice!View e-paper display (nice-view-gem custom widgets)
- Custom display status screen on left (central) side with
CONFIG_ZMK_DISPLAY_STATUS_SCREEN_CUSTOM=y - Display disabled on right (peripheral) side (
CONFIG_ZMK_DISPLAY=n) to save resources CONFIG_ZMK_USB=yis set inhillside_view_left.conf(notKconfig.defconfig) to avoid Kconfig warnings on non-central builds- Cirque Glidepoint trackpad on right (peripheral) side relayed via
zmk,input-split - Input processors for y-axis inversion and 2x scaling
- Temporary mouse layer (MOUSE) activates during trackpad movement (300ms timeout)
- Right click on left thumb key (position 41, was LEFT_ALT)
- Left click on right thumb key (position 42, was RIGHT_ALT)
- Trackpad uses I2C bus on peripheral with DR (data ready) GPIO
- Conditional layer activation (ADJ layer activates when both SYM and NUM are held)
Both keyboards follow similar patterns:
- Layer definitions: DEF (default), SYM (symbols), NUM (numbers), ADJ (adjust), MOUSE (temp mouse clicks)
- Home row mods with custom behaviors (
hml,hmr) - Hold-tap behaviors with quick-tap configuration (175ms quick-tap, 150-200ms tapping term)
- Sticky keys with 600ms release-after
- Caps word combos for quick capitalization
- Custom bootloader tap-dance behavior
- Temporary layers activated by input processors (e.g., MOUSE layer during trackpad movement)
Automated builds: Firmware builds automatically on push/PR via GitHub Actions. The build workflow outputs .uf2 files for flashing to keyboards.
Local builds: Use the provided Makefile for simplified builds:
# First time setup: Clone external modules
make modules/setup ZMK_ROOT=~/workspace/dactyl/zmk
# Build commands
make hsv/left ZMK_ROOT=~/workspace/dactyl/zmk
make hsv/right ZMK_ROOT=~/workspace/dactyl/zmk
# Or set ZMK_ROOT permanently
export ZMK_ROOT=~/workspace/dactyl/zmk
make hsv/all
# Firmware output: <zmk-root>/app/build/hsv/{left,right}/zephyr/zmk.uf2Manual builds (advanced):
cd <zmk-root>/app
source <zmk-root>/.venv/bin/activate
# Build with external modules (semicolon-separated for CMake)
west build -p -d build/hsv/left -b nice_nano \
-- -DSHIELD="hillside_view_left nice_view_gem" \
-DZMK_CONFIG=$(realpath <config-path>/config/) \
-DZMK_EXTRA_MODULES="<repo-root>;<modules-path>/nice-view-gem;..."Required Python packages in venv:
westpyelftoolssetuptoolsprotobufgrpcio-tools
External Modules Setup:
This repository uses ZMK_EXTRA_MODULES instead of modifying ZMK's west.yml:
- Run
make modules/setupto clone all external modules to./modules/ - Build commands automatically include
-DZMK_EXTRA_MODULESpointing to cloned modules - Update modules with
make modules/update
Modules included:
nice-view-gem(M165437/main) - Custom display widgets with animationsprospector-zmk-module(carrefinho/feat/new-status-screens) - Status screen support for Cygnus dongle
The repo root ($(CURDIR)) is always included as the first ZMK_EXTRA_MODULES entry so Zephyr discovers the shields in ./boards/shields/ via zephyr/module.yml.
Build flags:
-p= pristine build (clean)-d <dir>= build directory-b <board>= board name (nice_nano for v2.0+, xiao_ble; usenice_nano//zmkqualifier for Cygnus peripherals)-DSHIELD= shield(s) to build-DZMK_CONFIG= path to this config repository-DZMK_EXTRA_MODULES= semicolon-separated paths to modules (CMake list separator; colons also work on Linux but semicolons are canonical)
Board Names (Zephyr 4.1+):
- Use
nice_nano(not nice_nano_v2) - Use
nice_view_gemfor display shield (custom widgets with animations)
Tip: Use grep -E "(Wrote|FAILED|error:|warning:|Memory region)" to filter build output and save tokens.
Makefile Usage:
The Makefile reads all build configurations from build.yaml (single source of truth). Targets use the format build/<first_shield>-<board> or convenience aliases.
# Set ZMK_ROOT in Makefile or as environment variable
export ZMK_ROOT=~/path/to/zmk
# List all available targets from build.yaml
make list
# Build using direct targets (read from build.yaml)
make build/hillside_view_left-nice_nano
# Or use convenience aliases
# Hillside View
make hsv/all # Build left + right
make hsv/left # Build left (central)
make hsv/right # Build right (peripheral)
make hsv/upload/left # Upload left firmware
make hsv/upload/right # Upload right firmware
# Maintenance
make update # Update west dependencies
make clean # Clean build artifacts
make help # Show help
# Chain commands
make hsv/left hsv/upload/left # Build and upload in one commandMakefile Implementation Details:
The Makefile dynamically reads build.yaml to generate build targets:
- Uses
yqto parse board, shield, cmake-args, and snippet fields - Consolidates multiple yq calls into single invocations for efficiency (4→1 for build, 2→1 for upload, 3N+1→1 for modules/setup)
- Always prepends
$(CURDIR)(repo root) as firstZMK_EXTRA_MODULESentry for module discovery - Auto-discovers additional external modules from
./modules/directory - Validates firmware exists before upload attempt
- Provides both explicit targets (
build/<shield>-<board>) and convenience aliases (hsv/left)
Adding new keyboards to build.yaml:
- Add entry to
build.yamlinclude list with board, shield, and optional cmake-args/snippet - Run
make listto verify the new target appears - Build with
make build/<first_shield>-<board>or add convenience alias to Makefile
- Edit
.keymapfiles directly inconfig/for code-level changes - Use the keymap editor (https://nickcoutsos.github.io/keymap-editor/) for visual editing
- Export from editor updates the
.jsonfiles - Manual synchronization between
.jsonand.keymapmay be needed
- Export from editor updates the
- Keyboard features: Edit
.conffiles inconfig/ - Build matrix: Edit
build.yamlto add/remove build targets - Hardware definitions: Edit files in
boards/shields/<keyboard>/ - ZMK modules: Modify
config/west.ymlto change dependencies or ZMK version
- Create shield directory in
boards/shields/<keyboard_name>/ - Add hardware definition files (
.dtsi,.overlay, Kconfig files) - Create keymap files in
config/root - Update
build.yamlto include new build targets
Use conditional layers for automatic activation (e.g., ADJ activates when SYM+NUM are both active).
Define behaviors in the behaviors node with clear labels. Common patterns include:
lq- Layer toggle quick (tap-preferred)ht- Hold tap (tap-preferred)hml/hmr- Home row mods (left/right)bootldr- Tap-dance to bootloader
For trackpads on split peripherals, use the integrated zmk,input-split:
-
Shared .dtsi file:
- Define
zmk,input-splitdevice with uniqueregvalue - Define
zmk,input-listener(disabled by default) referencing the split device
- Define
-
Peripheral overlay:
- Override split device with
device = <&physical_trackpad>
- Override split device with
-
Central overlay:
- Enable the listener with
status = "okay"
- Enable the listener with
-
Input processors:
- Include
<input/processors.dtsi>in keymap - Use
&zip_xy_transformfor axis transformations (invert, swap) - Use
&zip_xy_scalerfor sensitivity scaling (multiplier, divisor) - Use
&zip_temp_layerfor temporary layer activation (layer_number, timeout_ms) - Apply via
input-processorsproperty on listener nodes
- Include
To enable a temporary layer during pointer movement:
-
Include pointing header:
#include <dt-bindings/zmk/pointing.h>
-
Add temp_layer processor to input listener:
input-processors = <&zip_temp_layer LAYER_NUM TIMEOUT_MS>;
LAYER_NUM: Layer index to activate (e.g., 4 for MOUSE layer)TIMEOUT_MS: Duration to keep layer active after last movement (e.g., 300)
-
Create layer with mouse click bindings:
mouse_layer { bindings = < &trans &trans &mkp LCLK &mkp RCLK // ... positions >; };
Mouse Click Defines:
&mkp LCLK- Left click (MB1)&mkp RCLK- Right click (MB2)&mkp MCLK- Middle click (MB3)&mkp MB4- Back button&mkp MB5- Forward button
Example: Hillside View uses 300ms timeout for near-instant layer deactivation when trackpad movement stops.