Skip to content

Use official dual Navigator configuration from Oryx #9

Use official dual Navigator configuration from Oryx

Use official dual Navigator configuration from Oryx #9

name: Fetch and build layout
on:
workflow_dispatch:
inputs:
layout_id:
description: "Layout id available in URL https://configure.zsa.io/voyager/layouts/[ID_IS_HERE]/latest"
required: true
default: "emVdK"
layout_geometry:
description: "Keyboard type"
type: choice
options:
- voyager
- moonlander/reva
- moonlander/revb
- ergodox_ez
- ergodox_ez/m32u4/glow
- ergodox_ez/m32u4/shine
- ergodox_ez/stm32
- ergodox_ez/stm32/glow
- ergodox_ez/stm32/shine
- planck_ez
- planck_ez/glow
default: voyager
# Temporary self-test trigger. Remove after this migration build succeeds.
push:
branches:
- main
paths:
- .github/workflows/fetch-and-build-layout.yml
env:
QMK_OPENRGB_REF: module_revD
LAYOUT_ID: ${{ github.event.inputs.layout_id || 'emVdK' }}
LAYOUT_GEOMETRY: ${{ github.event.inputs.layout_geometry || 'voyager' }}
permissions:
contents: write
jobs:
fetch-and-build-layout:
runs-on: ubuntu-latest
steps:
- name: Checkout Oryx source branch
uses: actions/checkout@v5
with:
ref: oryx
fetch-depth: 0
- name: Download latest Oryx layout source
id: download-layout-source
run: |
response=$(curl --fail --silent --show-error --location 'https://oryx.zsa.io/graphql' \
--header 'Content-Type: application/json' \
--data '{"query":"query getLayout($hashId: String!, $revisionId: String!, $geometry: String) {layout(hashId: $hashId, geometry: $geometry, revisionId: $revisionId) { revision { hashId, qmkVersion, title }}}","variables":{"hashId":"'"${LAYOUT_ID}"'","geometry":"'"${LAYOUT_GEOMETRY}"'","revisionId":"latest"}}')
hash_id=$(echo "${response}" | jq -r '.data.layout.revision.hashId')
firmware_version=$(printf '%.0f' "$(echo "${response}" | jq -r '.data.layout.revision.qmkVersion')")
change_description=$(echo "${response}" | jq -r '.data.layout.revision.title')
test -n "${hash_id}"
test "${hash_id}" != "null"
test -n "${firmware_version}"
if [[ -z "${change_description}" || "${change_description}" == "null" ]]; then
change_description="latest layout modification made with Oryx"
fi
echo "Oryx revision: ${hash_id}"
echo "Oryx QMK firmware: firmware${firmware_version}"
echo "Oryx title: ${change_description}"
curl --fail --silent --show-error --location \
"https://oryx.zsa.io/source/${hash_id}" \
-o source.zip
rm -rf "${LAYOUT_ID}"
mkdir -p "${LAYOUT_ID}"
unzip -oj source.zip '*_source/*' -d "${LAYOUT_ID}"
rm source.zip
echo "hash_id=${hash_id}" >> "$GITHUB_OUTPUT"
echo "firmware_version=${firmware_version}" >> "$GITHUB_OUTPUT"
echo "change_description=${change_description}" >> "$GITHUB_OUTPUT"
- name: Validate official dual Navigator configuration
if: ${{ env.LAYOUT_GEOMETRY == 'voyager' }}
run: |
# Dual Navigator support is now official ZSA/Oryx behavior. Do not
# synthesize either module here: the Oryx layout must own this config.
grep -q '"zsa/navigator_trackball"' "${LAYOUT_ID}/keymap.json"
grep -q '"zsa/navigator_trackpad"' "${LAYOUT_ID}/keymap.json"
- name: Save pristine Oryx source
env:
CHANGE_DESCRIPTION: ${{ steps.download-layout-source.outputs.change_description }}
run: |
git config --local user.name "github-actions"
git config --local user.email "github-actions@github.com"
git add "${LAYOUT_ID}"
if ! git diff --cached --quiet; then
printf "✨(oryx): %s\n" "$CHANGE_DESCRIPTION" | git commit -F -
git push origin HEAD:oryx
else
echo "Oryx source unchanged"
fi
- name: Replace main layout with latest Oryx source
run: |
git fetch origin main
git checkout -B main origin/main
rm -rf "${LAYOUT_ID}"
git checkout oryx -- "${LAYOUT_ID}"
- name: Apply OpenRGB integration
if: ${{ env.LAYOUT_GEOMETRY == 'voyager' }}
run: |
python3 - <<'PY'
import json
import os
import re
from pathlib import Path
layout = Path(os.environ["LAYOUT_ID"])
# Oryx owns all Navigator modules and settings. Our only keymap-level
# module addition is QMK-OpenRGB.
keymap_json = layout / "keymap.json"
data = json.loads(keymap_json.read_text())
modules = data.setdefault("modules", [])
if "openrgb" not in modules:
modules.append("openrgb")
keymap_json.write_text(json.dumps(data, indent=4) + "\n")
# OpenRGB plus the dedicated Oryx/Keymapp HID interface consumes one
# extra USB endpoint. Sharing QMK's mouse HID endpoint reclaims it.
rules = layout / "rules.mk"
text = rules.read_text()
pattern = r"^MOUSE_SHARED_EP\s*=.*$"
if re.search(pattern, text, flags=re.MULTILINE):
text = re.sub(pattern, "MOUSE_SHARED_EP = yes", text, flags=re.MULTILINE)
else:
if text and not text.endswith("\n"):
text += "\n"
text += "MOUSE_SHARED_EP = yes\n"
rules.write_text(text)
PY
- name: Save customized layout on main
run: |
git config --local user.name "github-actions"
git config --local user.email "github-actions@github.com"
git add "${LAYOUT_ID}"
git commit -m "✨(layout): Refresh from Oryx with OpenRGB integration" || echo "No customized layout change"
git push origin HEAD:main
- name: Update QMK firmware submodule to matching version (${{ steps.download-layout-source.outputs.firmware_version }})
run: |
git submodule update --init --remote --depth=1 --no-single-branch
cd qmk_firmware
git checkout -B "firmware${{ steps.download-layout-source.outputs.firmware_version }}" \
"origin/firmware${{ steps.download-layout-source.outputs.firmware_version }}"
git submodule update --init --recursive
cd ..
git add qmk_firmware
git commit -m "✨(qmk): Update firmware" || echo "No QMK change"
git push origin HEAD:main
- name: Install QMK-OpenRGB module
if: ${{ env.LAYOUT_GEOMETRY == 'voyager' }}
run: |
rm -rf qmk_firmware/modules/openrgb
git clone \
--depth 1 \
--branch "${QMK_OPENRGB_REF}" \
https://gitlab.com/OpenRGBDevelopers/QMK-OpenRGB.git \
qmk_firmware/modules/openrgb
echo "QMK-OpenRGB commit: $(git -C qmk_firmware/modules/openrgb rev-parse HEAD)"
- name: Apply OpenRGB and Keymapp dual-HID patch
if: ${{ env.LAYOUT_GEOMETRY == 'voyager' }}
run: |
python3 scripts/patch-voyager-dual-hid.py qmk_firmware
- name: Validate Voyager OpenRGB integration
if: ${{ env.LAYOUT_GEOMETRY == 'voyager' }}
run: |
# Navigator modules must still be the ones provided by Oryx.
grep -q '"zsa/navigator_trackball"' "${LAYOUT_ID}/keymap.json"
grep -q '"zsa/navigator_trackpad"' "${LAYOUT_ID}/keymap.json"
# Our custom layer consists only of OpenRGB + transport/endpoint work.
grep -q '"openrgb"' "${LAYOUT_ID}/keymap.json"
grep -q '^MOUSE_SHARED_EP = yes$' "${LAYOUT_ID}/rules.mk"
# Stock OpenRGB remains on QMK Raw HID / interface 1.
grep -q 'void raw_hid_receive(uint8_t \*data, uint8_t length)' \
qmk_firmware/modules/openrgb/openrgb.c
# ZSA Oryx remains on the dedicated second HID interface.
grep -q 'ORYX_INTERFACE' qmk_firmware/tmk_core/protocol/usb_descriptor.h
grep -q 'USB_ENDPOINT_IN_ORYX' qmk_firmware/tmk_core/protocol/chibios/usb_endpoints.c
grep -q 'oryx_hid_task' qmk_firmware/tmk_core/protocol/chibios/usb_main.c
grep -q 'void oryx_hid_receive(uint8_t \*data, uint8_t length)' \
qmk_firmware/modules/zsa/oryx/oryx.c
- name: Build qmk docker image
run: docker build -t qmk .
- name: Build the layout
id: build-layout
run: |
if [ "${{ steps.download-layout-source.outputs.firmware_version }}" -ge 24 ]; then
keyboard_directory="qmk_firmware/keyboards/zsa"
make_prefix="zsa/"
else
keyboard_directory="qmk_firmware/keyboards"
make_prefix=""
fi
rm -rf "${keyboard_directory}/${LAYOUT_GEOMETRY}/keymaps/${LAYOUT_ID}"
mkdir -p "${keyboard_directory}/${LAYOUT_GEOMETRY}/keymaps"
cp -r "${LAYOUT_ID}" \
"${keyboard_directory}/${LAYOUT_GEOMETRY}/keymaps/"
docker run \
-v ./qmk_firmware:/root \
--rm \
qmk \
/bin/sh -c "
qmk setup zsa/qmk_firmware \
-b firmware${{ steps.download-layout-source.outputs.firmware_version }} \
-y
make ${make_prefix}${LAYOUT_GEOMETRY}:${LAYOUT_ID}
"
normalized_layout_geometry="$(echo "${LAYOUT_GEOMETRY}" | sed 's/\//_/g')"
echo built_layout_file=$(find ./qmk_firmware \
-maxdepth 1 \
-type f \
-regex ".*${normalized_layout_geometry}.*\.\(bin\|hex\)$") >> "$GITHUB_OUTPUT"
echo normalized_layout_geometry=${normalized_layout_geometry} >> "$GITHUB_OUTPUT"
- name: Upload layout
uses: actions/upload-artifact@v7
with:
name: ${{ steps.build-layout.outputs.normalized_layout_geometry }}_${{ env.LAYOUT_ID }}
path: ${{ steps.build-layout.outputs.built_layout_file }}
if-no-files-found: error