From babb5032f667878cd25d6427b167ebc429c3193b Mon Sep 17 00:00:00 2001 From: Yoichiro Tanaka Date: Wed, 22 Apr 2026 16:02:02 +0900 Subject: [PATCH] feat: Migrate RGB underglow keycodes to UG_* with QMK build compat shim Rename 0x7820-0x782A keyInfoList entries to modern QMK UG_* canonical names (name.long/short, label, keywords). Workbench now emits UG_* in keymap.c and injects a preprocessor shim that defines UG_* from RGB_* on older QMK. Shim detection anchors on the #if !defined(UG_TOGG) && defined(RGB_TOG) signature so legacy/unmarked variants are also stripped and canonicalized. --- src/services/hid/KeycodeInfoList.test.ts | 108 ++++++++ src/services/hid/KeycodeInfoList.ts | 88 +++---- .../workbench/KeymapCGenerator.test.ts | 238 +++++++++++++++++- src/services/workbench/KeymapCGenerator.ts | 79 +++++- .../workbench/QmkKeycodeMapper.test.ts | 58 +++++ src/services/workbench/QmkKeycodeMapper.ts | 62 ++--- 6 files changed, 546 insertions(+), 87 deletions(-) create mode 100644 src/services/hid/KeycodeInfoList.test.ts diff --git a/src/services/hid/KeycodeInfoList.test.ts b/src/services/hid/KeycodeInfoList.test.ts new file mode 100644 index 00000000..aef410b7 --- /dev/null +++ b/src/services/hid/KeycodeInfoList.test.ts @@ -0,0 +1,108 @@ +import { describe, expect, it } from 'vitest'; +import { keyInfoList } from './KeycodeInfoList'; + +describe('keyInfoList', () => { + describe('UG_* keyword aliases for RGB/Underglow keycodes', () => { + const expected: Array<{ code: number; ugAlias: string }> = [ + { code: 0x7820, ugAlias: 'ug_togg' }, + { code: 0x7821, ugAlias: 'ug_next' }, + { code: 0x7822, ugAlias: 'ug_prev' }, + { code: 0x7823, ugAlias: 'ug_hueu' }, + { code: 0x7824, ugAlias: 'ug_hued' }, + { code: 0x7825, ugAlias: 'ug_satu' }, + { code: 0x7826, ugAlias: 'ug_satd' }, + { code: 0x7827, ugAlias: 'ug_valu' }, + { code: 0x7828, ugAlias: 'ug_vald' }, + { code: 0x7829, ugAlias: 'ug_spdu' }, + { code: 0x782a, ugAlias: 'ug_spdd' }, + ]; + + expected.forEach(({ code, ugAlias }) => { + it(`code 0x${code.toString(16)} carries '${ugAlias}' as a keyword`, () => { + const entry = keyInfoList.find( + (info) => info.keycodeInfo.code === code + ); + expect( + entry, + `keyInfoList must have entry for 0x${code.toString(16)}` + ).toBeDefined(); + expect(entry!.keycodeInfo.keywords).toContain(ugAlias); + }); + }); + + it('keeps the lowercase UG_* keyword so Autocomplete matches lowercased user input', () => { + const entry = keyInfoList.find( + (info) => info.keycodeInfo.code === 0x7820 + ); + expect(entry).toBeDefined(); + // The filterOptions in AutocompleteKeys lowercases the user's input but + // not the stored keywords; keeping the alias lowercase is what makes + // search work for "UG_TOGG", "ug_togg", and partial prefixes like "ug". + const hasLowercaseAlias = entry!.keycodeInfo.keywords.some( + (kwd) => kwd === kwd.toLowerCase() && kwd.startsWith('ug_') + ); + expect(hasLowercaseAlias).toBe(true); + }); + }); + + describe('canonical UG_* names and labels for underglow keycodes', () => { + const expected: Array<{ + code: number; + long: string; + short: string; + label: string; + }> = [ + { code: 0x7820, long: 'UG_TOGG', short: 'UG_TOGG', label: 'UG Toggle' }, + { code: 0x7821, long: 'UG_NEXT', short: 'UG_NEXT', label: 'UG Mode +' }, + { code: 0x7822, long: 'UG_PREV', short: 'UG_PREV', label: 'UG Mode -' }, + { code: 0x7823, long: 'UG_HUEU', short: 'UG_HUEU', label: 'UG HUE +' }, + { code: 0x7824, long: 'UG_HUED', short: 'UG_HUED', label: 'UG HUE -' }, + { code: 0x7825, long: 'UG_SATU', short: 'UG_SATU', label: 'UG SAT +' }, + { code: 0x7826, long: 'UG_SATD', short: 'UG_SATD', label: 'UG SAT -' }, + { + code: 0x7827, + long: 'UG_VALU', + short: 'UG_VALU', + label: 'UG Bright +', + }, + { + code: 0x7828, + long: 'UG_VALD', + short: 'UG_VALD', + label: 'UG Bright -', + }, + { + code: 0x7829, + long: 'UG_SPDU', + short: 'UG_SPDU', + label: 'UG Effect Speed +', + }, + { + code: 0x782a, + long: 'UG_SPDD', + short: 'UG_SPDD', + label: 'UG Effect Speed -', + }, + ]; + + expected.forEach(({ code, long, short, label }) => { + it(`code 0x${code.toString(16)} has canonical UG_* name and UG-prefixed label`, () => { + const entry = keyInfoList.find( + (info) => info.keycodeInfo.code === code + ); + expect(entry).toBeDefined(); + expect(entry!.keycodeInfo.name.long).toBe(long); + expect(entry!.keycodeInfo.name.short).toBe(short); + expect(entry!.keycodeInfo.label).toBe(label); + }); + }); + + it('retains legacy RGB_* as a lowercase keyword for backward search', () => { + const entry = keyInfoList.find( + (info) => info.keycodeInfo.code === 0x7820 + ); + expect(entry).toBeDefined(); + expect(entry!.keycodeInfo.keywords).toContain('rgb_tog'); + }); + }); +}); diff --git a/src/services/hid/KeycodeInfoList.ts b/src/services/hid/KeycodeInfoList.ts index 91696dbd..ed2534dd 100644 --- a/src/services/hid/KeycodeInfoList.ts +++ b/src/services/hid/KeycodeInfoList.ts @@ -6507,132 +6507,132 @@ export const keyInfoList: KeyInfo[] = [ desc: 'Toggle RGB lighting on or off', keycodeInfo: { code: 0x7820, // 30752 0b111100000100000 - label: 'RGB Toggle', + label: 'UG Toggle', name: { - long: 'RGB_TOG', - short: 'RGB_TOG', + long: 'UG_TOGG', + short: 'UG_TOGG', }, - keywords: ['Rgb Tog'], + keywords: ['Ug Togg', 'ug_togg', 'rgb_tog'], }, }, { desc: 'Cycle through modes, reverse direction when Shift is held', keycodeInfo: { code: 0x7821, // 30753 0b111100000100001 - label: 'RGB Mode +', + label: 'UG Mode +', name: { - long: 'RGB_MODE_FORWARD', - short: 'RGB_MOD', + long: 'UG_NEXT', + short: 'UG_NEXT', }, - keywords: ['Rgb Mode Forward'], + keywords: ['Ug Next', 'ug_next', 'rgb_mode_forward', 'rgb_mod'], }, }, { desc: 'Cycle through modes in reverse, forward direction when Shift is held', keycodeInfo: { code: 0x7822, // 30754 0b111100000100010 - label: 'RGB Mode -', + label: 'UG Mode -', name: { - long: 'RGB_MODE_REVERSE', - short: 'RGB_RMOD', + long: 'UG_PREV', + short: 'UG_PREV', }, - keywords: ['Rgb Mode Reverse'], + keywords: ['Ug Prev', 'ug_prev', 'rgb_mode_reverse', 'rgb_rmod'], }, }, { desc: 'Increase hue, decrease hue when Shift is held', keycodeInfo: { code: 0x7823, // 30755 0b111100000100011 - label: 'RGB HUE +', + label: 'UG HUE +', name: { - long: 'RGB_HUI', - short: 'RGB_HUI', + long: 'UG_HUEU', + short: 'UG_HUEU', }, - keywords: ['Rgb Hui'], + keywords: ['Ug Hueu', 'ug_hueu', 'rgb_hui'], }, }, { desc: 'Decrease hue, increase hue when Shift is held', keycodeInfo: { code: 0x7824, // 30756 0b111100000100100 - label: 'RGB HUE -', + label: 'UG HUE -', name: { - long: 'RGB_HUD', - short: 'RGB_HUD', + long: 'UG_HUED', + short: 'UG_HUED', }, - keywords: ['Rgb Hud'], + keywords: ['Ug Hued', 'ug_hued', 'rgb_hud'], }, }, { desc: 'Increase saturation, decrease saturation when Shift is held', keycodeInfo: { code: 0x7825, // 30757 0b111100000100101 - label: 'RGB SAT +', + label: 'UG SAT +', name: { - long: 'RGB_SAI', - short: 'RGB_SAI', + long: 'UG_SATU', + short: 'UG_SATU', }, - keywords: ['Rgb Sai'], + keywords: ['Ug Satu', 'ug_satu', 'rgb_sai'], }, }, { desc: 'Decrease saturation, increase saturation when Shift is held', keycodeInfo: { code: 0x7826, // 30758 0b111100000100110 - label: 'RGB SAT -', + label: 'UG SAT -', name: { - long: 'RGB_SAD', - short: 'RGB_SAD', + long: 'UG_SATD', + short: 'UG_SATD', }, - keywords: ['Rgb Sad'], + keywords: ['Ug Satd', 'ug_satd', 'rgb_sad'], }, }, { desc: 'Increase value (brightness), decrease value when Shift is held', keycodeInfo: { code: 0x7827, // 30759 0b111100000100111 - label: 'RGB Bright +', + label: 'UG Bright +', name: { - long: 'RGB_VAI', - short: 'RGB_VAI', + long: 'UG_VALU', + short: 'UG_VALU', }, - keywords: ['Rgb Vai'], + keywords: ['Ug Valu', 'ug_valu', 'rgb_vai'], }, }, { desc: 'Decrease value (brightness), increase value when Shift is held', keycodeInfo: { code: 0x7828, // 30760 0b111100000101000 - label: 'RGB Bright -', + label: 'UG Bright -', name: { - long: 'RGB_VAD', - short: 'RGB_VAD', + long: 'UG_VALD', + short: 'UG_VALD', }, - keywords: ['Rgb Vad'], + keywords: ['Ug Vald', 'ug_vald', 'rgb_vad'], }, }, { desc: 'Increase effect speed (does not support eeprom yet), decrease speed when Shift is held', keycodeInfo: { code: 0x7829, // 30761 0b111100000101001 - label: 'RGB Effect Speed +', + label: 'UG Effect Speed +', name: { - long: 'RGB_SPI', - short: 'RGB_SPI', + long: 'UG_SPDU', + short: 'UG_SPDU', }, - keywords: ['Rgb Spi'], + keywords: ['Ug Spdu', 'ug_spdu', 'rgb_spi'], }, }, { desc: 'Decrease effect speed (does not support eeprom yet), increase speed when Shift is held', keycodeInfo: { code: 0x782a, // 30762 0b111100000101010 - label: 'RGB Effect Speed -', + label: 'UG Effect Speed -', name: { - long: 'RGB_SPD', - short: 'RGB_SPD', + long: 'UG_SPDD', + short: 'UG_SPDD', }, - keywords: ['Rgb Spd'], + keywords: ['Ug Spdd', 'ug_spdd', 'rgb_spd'], }, }, { diff --git a/src/services/workbench/KeymapCGenerator.test.ts b/src/services/workbench/KeymapCGenerator.test.ts index c0b4b76b..4d522ea5 100644 --- a/src/services/workbench/KeymapCGenerator.test.ts +++ b/src/services/workbench/KeymapCGenerator.test.ts @@ -30,14 +30,7 @@ describe('KeymapCGenerator', () => { layers: [ { index: '0', - keycodeNames: [ - 'KC_A', - 'KC_B', - 'KC_C', - 'KC_D', - 'KC_E', - 'KC_F', - ], + keycodeNames: ['KC_A', 'KC_B', 'KC_C', 'KC_D', 'KC_E', 'KC_F'], }, ], postamble: '', @@ -131,6 +124,229 @@ void keyboard_post_init_user(void) { }); }); + describe('RGB/UG compatibility shim', () => { + const SHIM_BEGIN_MARKER = '/* BEGIN Remap shim: RGB/UG compat */'; + const SHIM_END_MARKER = '/* END Remap shim: RGB/UG compat */'; + + it('injects shim before keymaps array when UG_* keycodes are present', () => { + const parsed: ParsedKeymap = { + preamble: + '#include QMK_KEYBOARD_H\n\nconst uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {', + layoutMacroName: 'LAYOUT', + layers: [{ index: '0', keycodeNames: ['KC_A', 'UG_TOGG', 'KC_B'] }], + postamble: '', + }; + + const result = generateKeymapC(parsed); + expect(result).toContain(SHIM_BEGIN_MARKER); + expect(result).toContain(SHIM_END_MARKER); + expect(result).toContain('#if !defined(UG_TOGG) && defined(RGB_TOG)'); + expect(result).toContain('# define UG_TOGG'); + expect(result).toContain('# define UG_SPDD'); + + // Shim must appear before the keymaps array declaration. + const shimIdx = result.indexOf(SHIM_BEGIN_MARKER); + const keymapsIdx = result.indexOf('const uint16_t PROGMEM keymaps'); + expect(shimIdx).toBeGreaterThanOrEqual(0); + expect(keymapsIdx).toBeGreaterThan(shimIdx); + }); + + it('does not inject shim when no UG_* keycodes are present', () => { + const parsed: ParsedKeymap = { + preamble: + '#include QMK_KEYBOARD_H\n\nconst uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {', + layoutMacroName: 'LAYOUT', + layers: [{ index: '0', keycodeNames: ['KC_A', 'KC_B', 'KC_C'] }], + postamble: '', + }; + + const result = generateKeymapC(parsed); + expect(result).not.toContain(SHIM_BEGIN_MARKER); + expect(result).not.toContain('# define UG_TOGG'); + }); + + it('does not duplicate shim across round-trips', () => { + const original = `#include QMK_KEYBOARD_H + +const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = { + [0] = LAYOUT(KC_A, UG_TOGG, KC_B) +}; +`; + + let content = original; + for (let i = 0; i < 3; i++) { + const parsed = parseKeymapC(content); + expect(parsed).not.toBeNull(); + content = generateKeymapC(parsed!); + } + + const beginCount = ( + content.match(/BEGIN Remap shim: RGB\/UG compat/g) || [] + ).length; + const endCount = (content.match(/END Remap shim: RGB\/UG compat/g) || []) + .length; + expect(beginCount).toBe(1); + expect(endCount).toBe(1); + }); + + it('refreshes an existing shim rather than stacking a second one', () => { + // Preamble already contains a (possibly older) shim block. + const original = `#include QMK_KEYBOARD_H + +${SHIM_BEGIN_MARKER} +#if !defined(UG_TOGG) && defined(RGB_TOG) +# define UG_TOGG RGB_TOG +#endif +${SHIM_END_MARKER} + +const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = { + [0] = LAYOUT(KC_A, UG_TOGG, KC_B) +}; +`; + const parsed = parseKeymapC(original); + expect(parsed).not.toBeNull(); + const result = generateKeymapC(parsed!); + + const beginCount = ( + result.match(/BEGIN Remap shim: RGB\/UG compat/g) || [] + ).length; + expect(beginCount).toBe(1); + // Fresh shim must contain all eleven UG_* definitions. + expect(result).toContain('# define UG_SPDD'); + }); + + it('strips unmarked legacy shims that share our #if signature', () => { + // Real-world input: the user's file ended up with both a pre-existing + // unmarked legacy shim and a Remap-marked shim. A regenerate must + // collapse everything down to one canonical shim. + const legacyPlusMarked = `// Copyright 2023 QMK +// SPDX-License-Identifier: GPL-2.0-or-later + +#include QMK_KEYBOARD_H + +#if !defined(UG_TOGG) && defined(RGB_TOG) + #define UG_TOGG RGB_TOG + #define UG_NEXT RGB_MODE_FORWARD + #define UG_PREV RGB_MODE_REVERSE + #define UG_HUEU RGB_HUI + #define UG_HUED RGB_HUD + #define UG_SATU RGB_SAI + #define UG_SATD RGB_SAD + #define UG_VALU RGB_VAI + #define UG_VALD RGB_VAD + #define UG_SPDU RGB_SPI + #define UG_SPDD RGB_SPD +#endif + +${SHIM_BEGIN_MARKER} +#if !defined(UG_TOGG) && defined(RGB_TOG) +# define UG_TOGG RGB_TOG +#endif +${SHIM_END_MARKER} + +const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = { + [0] = LAYOUT(UG_TOGG) +}; +`; + const parsed = parseKeymapC(legacyPlusMarked); + expect(parsed).not.toBeNull(); + const result = generateKeymapC(parsed!); + + // Only one shim should remain, and the unmarked block must be gone. + const beginCount = ( + result.match(/BEGIN Remap shim: RGB\/UG compat/g) || [] + ).length; + expect(beginCount).toBe(1); + + const rawIfCount = ( + result.match(/#if\s+!defined\(UG_TOGG\) && defined\(RGB_TOG\)/g) || [] + ).length; + expect(rawIfCount).toBe(1); + + // The 4-space-indented legacy ` #define` must be purged. + expect(result).not.toMatch(/ {4}#define UG_TOGG/); + }); + + it('strips a lone unmarked legacy shim (no Remap markers at all)', () => { + const legacyOnly = `#include QMK_KEYBOARD_H + +#if !defined(UG_TOGG) && defined(RGB_TOG) + #define UG_TOGG RGB_TOG + #define UG_NEXT RGB_MODE_FORWARD +#endif + +const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = { + [0] = LAYOUT(UG_TOGG, KC_A) +}; +`; + const parsed = parseKeymapC(legacyOnly); + expect(parsed).not.toBeNull(); + const result = generateKeymapC(parsed!); + + const beginCount = ( + result.match(/BEGIN Remap shim: RGB\/UG compat/g) || [] + ).length; + expect(beginCount).toBe(1); + // The 4-space indentation legacy form must be gone. + expect(result).not.toMatch(/ {4}#define UG_TOGG/); + }); + + it('collapses two existing shim blocks into one', () => { + const doubled = `#include QMK_KEYBOARD_H + +${SHIM_BEGIN_MARKER} +#if !defined(UG_TOGG) && defined(RGB_TOG) +# define UG_TOGG RGB_TOG +#endif +${SHIM_END_MARKER} + +${SHIM_BEGIN_MARKER} +#if !defined(UG_TOGG) && defined(RGB_TOG) +# define UG_TOGG RGB_TOG +#endif +${SHIM_END_MARKER} + +const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = { + [0] = LAYOUT(KC_A, UG_TOGG, KC_B) +}; +`; + const parsed = parseKeymapC(doubled); + expect(parsed).not.toBeNull(); + const result = generateKeymapC(parsed!); + + const beginCount = ( + result.match(/BEGIN Remap shim: RGB\/UG compat/g) || [] + ).length; + expect(beginCount).toBe(1); + }); + + it('does not accumulate blank lines around the shim across round-trips', () => { + const original = `#include QMK_KEYBOARD_H + +const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = { + [0] = LAYOUT(KC_A, UG_TOGG, KC_B) +}; +`; + let content = original; + for (let i = 0; i < 5; i++) { + const parsed = parseKeymapC(content); + expect(parsed).not.toBeNull(); + content = generateKeymapC(parsed!); + } + + // At most one blank line between each adjacent pair of sections. + const blankBetweenIncludeAndShim = content + .split('#include QMK_KEYBOARD_H')[1] + .split(SHIM_BEGIN_MARKER)[0]; + expect(blankBetweenIncludeAndShim).toBe('\n\n'); + + const blankBetweenShimEndAndArray = content + .split(SHIM_END_MARKER)[1] + .split('const uint16_t')[0]; + expect(blankBetweenShimEndAndArray).toBe('\n\n'); + }); + }); + describe('round-trip: parse → modify → generate → parse', () => { it('round-trips preserving keycode data after modification', () => { const original = `#include QMK_KEYBOARD_H @@ -148,11 +364,7 @@ const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = { const generated = generateKeymapC(parsed1!); const parsed2 = parseKeymapC(generated); expect(parsed2).not.toBeNull(); - expect(parsed2!.layers[0].keycodeNames).toEqual([ - 'KC_A', - 'KC_Z', - 'KC_C', - ]); + expect(parsed2!.layers[0].keycodeNames).toEqual(['KC_A', 'KC_Z', 'KC_C']); }); it('round-trips preserving preamble and postamble', () => { diff --git a/src/services/workbench/KeymapCGenerator.ts b/src/services/workbench/KeymapCGenerator.ts index 2f096c3e..799a79d1 100644 --- a/src/services/workbench/KeymapCGenerator.ts +++ b/src/services/workbench/KeymapCGenerator.ts @@ -1,5 +1,72 @@ import { ParsedKeymap } from './KeymapCParser'; +const SHIM_BEGIN_MARKER = '/* BEGIN Remap shim: RGB/UG compat */'; +const SHIM_END_MARKER = '/* END Remap shim: RGB/UG compat */'; + +// Bridges modern UG_* names in the generated keymap back to the legacy +// RGB_* names that older QMK Firmware versions define. When built against +// modern QMK the UG_* names are already defined and this block is a no-op. +const RGB_UG_COMPAT_SHIM = [ + SHIM_BEGIN_MARKER, + '#if !defined(UG_TOGG) && defined(RGB_TOG)', + '# define UG_TOGG RGB_TOG', + '# define UG_NEXT RGB_MODE_FORWARD', + '# define UG_PREV RGB_MODE_REVERSE', + '# define UG_HUEU RGB_HUI', + '# define UG_HUED RGB_HUD', + '# define UG_SATU RGB_SAI', + '# define UG_SATD RGB_SAD', + '# define UG_VALU RGB_VAI', + '# define UG_VALD RGB_VAD', + '# define UG_SPDU RGB_SPI', + '# define UG_SPDD RGB_SPD', + '#endif', + SHIM_END_MARKER, +].join('\n'); + +const UG_NAME_PATTERN = + /\bUG_(?:TOGG|NEXT|PREV|HUEU|HUED|SATU|SATD|VALU|VALD|SPDU|SPDD)\b/; + +// Matches any UG/RGB compatibility shim block so it can be stripped and +// re-injected in a canonical form. The anchor is the `#if !defined(UG_TOGG) +// && defined(RGB_TOG)` condition — a signature unique to this shim — so +// legacy or hand-written variants without the BEGIN/END markers are also +// caught. Surrounding blank lines are consumed to prevent whitespace from +// accumulating across round-trips. +const EXISTING_SHIM_PATTERN = + /\n*(?:\/\* BEGIN Remap shim: RGB\/UG compat \*\/\s*\n)?#if\s+!defined\(UG_TOGG\)\s*&&\s*defined\(RGB_TOG\)[\s\S]*?#endif(?:\s*\n\/\* END Remap shim: RGB\/UG compat \*\/)?\n*/g; + +function hasUgKeycode(layers: ParsedKeymap['layers']): boolean { + for (const layer of layers) { + for (const name of layer.keycodeNames) { + if (UG_NAME_PATTERN.test(name)) { + return true; + } + } + } + return false; +} + +function stripShim(preamble: string): string { + // Collapse shim + its surrounding newlines into a single blank line, then + // squash any accidental runs of blank lines so the preamble cannot grow + // unbounded across successive parse/generate cycles. + return preamble + .replace(EXISTING_SHIM_PATTERN, '\n\n') + .replace(/\n{3,}/g, '\n\n'); +} + +function injectShim(preamble: string): string { + const stripped = stripShim(preamble); + const declIdx = stripped.search(/const\s+uint16_t\s+PROGMEM\s+keymaps/); + if (declIdx === -1) { + return stripped.replace(/\s*$/, '') + '\n\n' + RGB_UG_COMPAT_SHIM + '\n'; + } + const before = stripped.slice(0, declIdx).replace(/\s*$/, ''); + const after = stripped.slice(declIdx); + return before + '\n\n' + RGB_UG_COMPAT_SHIM + '\n\n' + after; +} + /** * Generate keymap.c content from a ParsedKeymap. * Preserves preamble and postamble exactly. Regenerates the keymaps array @@ -16,7 +83,11 @@ export function generateKeymapC( const indent = ' '; const keyIndent = indent + indent; - let result = parsed.preamble + '\n'; + const preamble = hasUgKeycode(parsed.layers) + ? injectShim(parsed.preamble) + : stripShim(parsed.preamble); + + let result = preamble + '\n'; parsed.layers.forEach((layer, i) => { result += `${indent}[${layer.index}] = ${parsed.layoutMacroName}(\n`; @@ -26,7 +97,11 @@ export function generateKeymapC( for (let row = 0; row < keysPerRow.length; row++) { const count = keysPerRow[row]; const rowKeys: string[] = []; - for (let j = 0; j < count && keyIndex < layer.keycodeNames.length; j++) { + for ( + let j = 0; + j < count && keyIndex < layer.keycodeNames.length; + j++ + ) { rowKeys.push(layer.keycodeNames[keyIndex]); keyIndex++; } diff --git a/src/services/workbench/QmkKeycodeMapper.test.ts b/src/services/workbench/QmkKeycodeMapper.test.ts index dd037d0c..179acb7c 100644 --- a/src/services/workbench/QmkKeycodeMapper.test.ts +++ b/src/services/workbench/QmkKeycodeMapper.test.ts @@ -395,6 +395,64 @@ describe('QmkKeycodeMapper', () => { }); }); + describe('UG_* output for RGB underglow keycodes', () => { + it('converts 0x7820 to UG_TOGG', () => { + expect(codeToName(0x7820)).toBe('UG_TOGG'); + }); + + it('converts 0x7821 to UG_NEXT', () => { + expect(codeToName(0x7821)).toBe('UG_NEXT'); + }); + + it('converts 0x7822 to UG_PREV', () => { + expect(codeToName(0x7822)).toBe('UG_PREV'); + }); + + it('converts 0x7823 to UG_HUEU', () => { + expect(codeToName(0x7823)).toBe('UG_HUEU'); + }); + + it('converts 0x7824 to UG_HUED', () => { + expect(codeToName(0x7824)).toBe('UG_HUED'); + }); + + it('converts 0x7825 to UG_SATU', () => { + expect(codeToName(0x7825)).toBe('UG_SATU'); + }); + + it('converts 0x7826 to UG_SATD', () => { + expect(codeToName(0x7826)).toBe('UG_SATD'); + }); + + it('converts 0x7827 to UG_VALU', () => { + expect(codeToName(0x7827)).toBe('UG_VALU'); + }); + + it('converts 0x7828 to UG_VALD', () => { + expect(codeToName(0x7828)).toBe('UG_VALD'); + }); + + it('converts 0x7829 to UG_SPDU', () => { + expect(codeToName(0x7829)).toBe('UG_SPDU'); + }); + + it('converts 0x782A to UG_SPDD', () => { + expect(codeToName(0x782a)).toBe('UG_SPDD'); + }); + + it('round-trips UG_TOGG through nameToCode and codeToName', () => { + const code = nameToCode('UG_TOGG'); + expect(code).not.toBeNull(); + expect(codeToName(code!)).toBe('UG_TOGG'); + }); + + it('normalizes legacy RGB_TOG input to UG_TOGG on output', () => { + const code = nameToCode('RGB_TOG'); + expect(code).not.toBeNull(); + expect(codeToName(code!)).toBe('UG_TOGG'); + }); + }); + describe('round-trip consistency', () => { const testCases = [ 'KC_A', diff --git a/src/services/workbench/QmkKeycodeMapper.ts b/src/services/workbench/QmkKeycodeMapper.ts index b72396f9..b957487d 100644 --- a/src/services/workbench/QmkKeycodeMapper.ts +++ b/src/services/workbench/QmkKeycodeMapper.ts @@ -102,30 +102,35 @@ const SHIFTED_ALIASES: Record = { // Reverse map: numeric shifted code → preferred short alias name const SHIFTED_CODE_TO_NAME: Record = {}; -// UG_* keycode aliases (QMK v0.0.4+) mapping to legacy RGB_* names in keyInfoList -const UG_ALIASES: Record = { - UG_TOGG: 'RGB_TOG', - QK_UNDERGLOW_TOGGLE: 'RGB_TOG', - UG_NEXT: 'RGB_MODE_FORWARD', - QK_UNDERGLOW_MODE_NEXT: 'RGB_MODE_FORWARD', - UG_PREV: 'RGB_MODE_REVERSE', - QK_UNDERGLOW_MODE_PREVIOUS: 'RGB_MODE_REVERSE', - UG_HUEU: 'RGB_HUI', - QK_UNDERGLOW_HUE_UP: 'RGB_HUI', - UG_HUED: 'RGB_HUD', - QK_UNDERGLOW_HUE_DOWN: 'RGB_HUD', - UG_SATU: 'RGB_SAI', - QK_UNDERGLOW_SATURATION_UP: 'RGB_SAI', - UG_SATD: 'RGB_SAD', - QK_UNDERGLOW_SATURATION_DOWN: 'RGB_SAD', - UG_VALU: 'RGB_VAI', - QK_UNDERGLOW_VALUE_UP: 'RGB_VAI', - UG_VALD: 'RGB_VAD', - QK_UNDERGLOW_VALUE_DOWN: 'RGB_VAD', - UG_SPDU: 'RGB_SPI', - QK_UNDERGLOW_SPEED_UP: 'RGB_SPI', - UG_SPDD: 'RGB_SPD', - QK_UNDERGLOW_SPEED_DOWN: 'RGB_SPD', +// Legacy keycode names that parse as modern UG_* canonical names in +// keyInfoList. Covers pre-refactor QMK RGB_* spellings and QMK's own verbose +// QK_UNDERGLOW_* long-form so that imported keymap.c sources built against +// older QMK continue to resolve to the right numeric code. +const LEGACY_UG_ALIASES: Record = { + RGB_TOG: 'UG_TOGG', + QK_UNDERGLOW_TOGGLE: 'UG_TOGG', + RGB_MODE_FORWARD: 'UG_NEXT', + RGB_MOD: 'UG_NEXT', + QK_UNDERGLOW_MODE_NEXT: 'UG_NEXT', + RGB_MODE_REVERSE: 'UG_PREV', + RGB_RMOD: 'UG_PREV', + QK_UNDERGLOW_MODE_PREVIOUS: 'UG_PREV', + RGB_HUI: 'UG_HUEU', + QK_UNDERGLOW_HUE_UP: 'UG_HUEU', + RGB_HUD: 'UG_HUED', + QK_UNDERGLOW_HUE_DOWN: 'UG_HUED', + RGB_SAI: 'UG_SATU', + QK_UNDERGLOW_SATURATION_UP: 'UG_SATU', + RGB_SAD: 'UG_SATD', + QK_UNDERGLOW_SATURATION_DOWN: 'UG_SATD', + RGB_VAI: 'UG_VALU', + QK_UNDERGLOW_VALUE_UP: 'UG_VALU', + RGB_VAD: 'UG_VALD', + QK_UNDERGLOW_VALUE_DOWN: 'UG_VALD', + RGB_SPI: 'UG_SPDU', + QK_UNDERGLOW_SPEED_UP: 'UG_SPDU', + RGB_SPD: 'UG_SPDD', + QK_UNDERGLOW_SPEED_DOWN: 'UG_SPDD', }; // Lazy-initialized reverse lookup map: QMK keycode name → numeric code @@ -168,11 +173,12 @@ function getNameToCodeMap(): Map { } } - // Register UG_* aliases by resolving to the same code as the legacy RGB_* name - for (const [ugName, rgbName] of Object.entries(UG_ALIASES)) { - const code = nameToCodeMap.get(rgbName); + // Register legacy RGB_* / QK_UNDERGLOW_* aliases by resolving to the same + // code as the canonical UG_* name already registered from keyInfoList. + for (const [legacyName, canonicalName] of Object.entries(LEGACY_UG_ALIASES)) { + const code = nameToCodeMap.get(canonicalName); if (code !== undefined) { - nameToCodeMap.set(ugName, code); + nameToCodeMap.set(legacyName, code); } }