From 89203daf3f61b31413b6ab493c505459bbb4900c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A1szl=C3=B3=20Monda?= Date: Sun, 9 Aug 2026 00:15:47 +0200 Subject: [PATCH 1/3] feat: add per-key notes with keyboard tooltips Allow adding notes to key mappings in the remap popover, persist them via KeyLabelAction, and show a corner marker plus hover tooltip on the keymap. Co-authored-by: Cursor --- .../config-items/key-action/key-action.ts | 4 +- .../key-action/key-label-action.ts | 6 -- .../config-items/module.test.ts | 90 +++++++++++++++++++ .../config-serializer/config-items/module.ts | 47 ++++++++-- .../components/popover/popover.component.html | 23 ++++- .../components/popover/popover.component.scss | 25 ++++++ .../components/popover/popover.component.ts | 43 ++++++++- .../tab/keypress/keypress-tab.component.scss | 2 +- .../svg-keyboard-key.component.html | 5 ++ .../svg-keyboard-key.component.scss | 5 ++ .../svg-keyboard-key.component.ts | 13 +++ .../svg-keyboard-key.model.ts | 1 + .../components/svg/module/svg-module.model.ts | 6 ++ .../svg/wrap/svg-keyboard-wrap.component.html | 6 +- .../svg/wrap/svg-keyboard-wrap.component.scss | 34 +++++-- .../svg/wrap/svg-keyboard-wrap.component.ts | 71 ++++++++++----- packages/uhk-web/src/styles/_global.scss | 18 ++++ packages/uhk-web/src/styles/themes/_dark.scss | 1 + .../uhk-web/src/styles/themes/_light.scss | 1 + 19 files changed, 350 insertions(+), 51 deletions(-) create mode 100644 packages/uhk-common/src/config-serializer/config-items/module.test.ts diff --git a/packages/uhk-common/src/config-serializer/config-items/key-action/key-action.ts b/packages/uhk-common/src/config-serializer/config-items/key-action/key-action.ts index 76f40c48ad6..a1975dbcde9 100644 --- a/packages/uhk-common/src/config-serializer/config-items/key-action/key-action.ts +++ b/packages/uhk-common/src/config-serializer/config-items/key-action/key-action.ts @@ -49,11 +49,13 @@ export abstract class KeyAction implements RgbColorInterface { @assertUInt8 b = DEFAULT_RGB_COLOR.b; @assertUInt8 g = DEFAULT_RGB_COLOR.g; @assertUInt8 r = DEFAULT_RGB_COLOR.r; + label = ''; - protected constructor(keyAction?: RgbColorInterface) { + protected constructor(keyAction?: RgbColorInterface & { label?: string }) { this.b = keyAction?.b ?? DEFAULT_RGB_COLOR.b; this.g = keyAction?.g ?? DEFAULT_RGB_COLOR.g; this.r = keyAction?.r ?? DEFAULT_RGB_COLOR.r; + this.label = keyAction?.label ?? ''; } // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/packages/uhk-common/src/config-serializer/config-items/key-action/key-label-action.ts b/packages/uhk-common/src/config-serializer/config-items/key-action/key-label-action.ts index e4db7334c44..97bcaf659cb 100644 --- a/packages/uhk-common/src/config-serializer/config-items/key-action/key-label-action.ts +++ b/packages/uhk-common/src/config-serializer/config-items/key-action/key-label-action.ts @@ -5,14 +5,8 @@ import { KeyAction } from './key-action.js'; export class KeyLabelAction extends KeyAction { - label: string; - constructor(other?: KeyLabelAction) { super(other); - - if (other) { - this.label = other.label; - } } // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/packages/uhk-common/src/config-serializer/config-items/module.test.ts b/packages/uhk-common/src/config-serializer/config-items/module.test.ts new file mode 100644 index 00000000000..b90bfe8f130 --- /dev/null +++ b/packages/uhk-common/src/config-serializer/config-items/module.test.ts @@ -0,0 +1,90 @@ +import { describe, it } from 'node:test'; + +import { UhkBuffer } from '../uhk-buffer.js'; +import { + KeystrokeAction, + KeystrokeType, + MacroArgumentAction, + Module, + PlayMacroAction, +} from './index.js'; +import { DEFAULT_SERIALISATION_INFO } from './serialisation-info.js'; + +describe('module key labels', () => { + const macros = [{ id: 1 }]; + const serialisationInfo = DEFAULT_SERIALISATION_INFO; + + function createLabeledKeystroke(): KeystrokeAction { + const keystrokeAction = new KeystrokeAction(); + keystrokeAction.type = KeystrokeType.basic; + keystrokeAction.scancode = 4; + keystrokeAction.label = 'Hello note'; + return keystrokeAction; + } + + it('should round-trip keystroke label through json', ({ assert }) => { + const module = new Module(); + module.id = 0; + module.keyActions = [createLabeledKeystroke()]; + + const json = module.toJsonObject(serialisationInfo, macros as any); + const restored = new Module().fromJsonObject(json, macros as any, serialisationInfo); + + assert.strictEqual(restored.keyActions[0].label, 'Hello note'); + assert.ok(restored.keyActions[0] instanceof KeystrokeAction); + }); + + it('should round-trip keystroke label through binary', ({ assert }) => { + const module = new Module(); + module.id = 0; + module.keyActions = [createLabeledKeystroke()]; + + const buffer = new UhkBuffer(); + module.toBinary(buffer, serialisationInfo, { macros } as any); + buffer.offset = 0; + + const restored = new Module().fromBinary(buffer, macros as any, serialisationInfo); + + assert.strictEqual(restored.keyActions.length, 1); + assert.strictEqual(restored.keyActions[0].label, 'Hello note'); + assert.ok(restored.keyActions[0] instanceof KeystrokeAction); + }); + + it('should keep play macro arguments and label in binary', ({ assert }) => { + const module = new Module(); + module.id = 0; + const playMacroAction = new PlayMacroAction(); + playMacroAction.macroId = 1; + const macroArgument = new MacroArgumentAction(); + macroArgument.value = 'arg1'; + playMacroAction.macroArguments = [macroArgument]; + playMacroAction.label = 'Macro note'; + module.keyActions = [playMacroAction]; + + const buffer = new UhkBuffer(); + module.toBinary(buffer, serialisationInfo, { macros } as any); + buffer.offset = 0; + + const restored = new Module().fromBinary(buffer, macros as any, serialisationInfo); + const restoredPlayMacro = restored.keyActions[0] as PlayMacroAction; + + assert.ok(restoredPlayMacro instanceof PlayMacroAction); + assert.strictEqual(restoredPlayMacro.label, 'Macro note'); + assert.strictEqual(restoredPlayMacro.macroArguments.length, 1); + assert.strictEqual(restoredPlayMacro.macroArguments[0].value, 'arg1'); + assert.strictEqual(module.getKeyActionsCount(), 3); + }); + + it('should omit empty labels from json', ({ assert }) => { + const module = new Module(); + module.id = 0; + const keystrokeAction = new KeystrokeAction(); + keystrokeAction.type = KeystrokeType.basic; + keystrokeAction.scancode = 4; + module.keyActions = [keystrokeAction]; + + const json = module.toJsonObject(serialisationInfo, macros as any); + + assert.strictEqual(json.keyActions[0].label, undefined); + }); +}); diff --git a/packages/uhk-common/src/config-serializer/config-items/module.ts b/packages/uhk-common/src/config-serializer/config-items/module.ts index ab908e6f70d..58fcee9c11e 100644 --- a/packages/uhk-common/src/config-serializer/config-items/module.ts +++ b/packages/uhk-common/src/config-serializer/config-items/module.ts @@ -90,7 +90,10 @@ export class Module { id: this.id, keyActions: this.keyActions.map(keyAction => { if (keyAction && (macros || !(keyAction instanceof PlayMacroAction || keyAction instanceof SwitchKeymapAction))) { - return keyAction.toJsonObject(serialisationInfo, macros); + return { + ...keyAction.toJsonObject(serialisationInfo, macros), + ...labelToJson(keyAction) + }; } return new NoneAction().toJsonObject(serialisationInfo); @@ -105,6 +108,7 @@ export class Module { const keyActions = this.getCompressedKeyActions() for (const keyAction of keyActions) { keyAction.toBinary(buffer, serialisationInfo, userConfiguration); + writeKeyLabelAction(buffer, keyAction); } } @@ -137,8 +141,12 @@ export class Module { // eslint-disable-next-line @typescript-eslint/no-explicit-any fromJsonObjectV1(jsonObject: any, macros: Macro[], serialisationInfo: SerialisationInfo): void { this.id = jsonObject.id; - this.keyActions = jsonObject.keyActions.map((keyAction) => { - return KeyActionHelper.fromJSONObject(keyAction, macros, serialisationInfo); + this.keyActions = jsonObject.keyActions.map((keyActionJson) => { + const keyAction = KeyActionHelper.fromJSONObject(keyActionJson, macros, serialisationInfo); + if (keyActionJson?.label) { + keyAction.label = keyActionJson.label; + } + return keyAction; }); } @@ -152,9 +160,11 @@ export class Module { while (processedKeyActionsCount < keyActionsLength) { const keyAction = KeyActionHelper.createKeyAction(buffer, macros, serialisationInfo) - if (KeyLabelAction instanceof KeyLabelAction) { - // TODO: implement it in other PR - // related to https://github.com/UltimateHackingKeyboard/agent/issues/2289 + if (keyAction instanceof KeyLabelAction) { + if (!lastKeyAction) { + throw Error(`${processedKeyActionsCount} key label has no preceding key action`); + } + lastKeyAction.label = keyAction.label; } else if (keyAction instanceof MacroArgumentAction) { if (lastKeyAction instanceof PlayMacroAction) { @@ -192,13 +202,14 @@ export class Module { for (let i = 0; i < this.keyActions.length;) { const keyAction = this.keyActions[i] || new NoneAction(); - if (keyAction instanceof NoneAction) { + if (keyAction instanceof NoneAction && !keyAction.label) { let blockCount = 1 for (let j = i + 1; j < this.keyActions.length; j++) { const nextAction = this.keyActions[j] || new NoneAction(); if (nextAction instanceof NoneAction + && !nextAction.label && keyAction.r === nextAction.r && keyAction.g === nextAction.g && keyAction.b === nextAction.b) { @@ -244,10 +255,30 @@ export class Module { count += keyAction.macroArguments.length; } - // TODO: Extend when implement KeyLabelAction + if (keyAction?.label) { + count++; + } } return count; } } + +function labelToJson(keyAction: KeyAction): { label?: string } { + if (keyAction.label) { + return { label: keyAction.label }; + } + + return {}; +} + +function writeKeyLabelAction(buffer: UhkBuffer, keyAction: KeyAction): void { + if (!keyAction.label) { + return; + } + + const keyLabelAction = new KeyLabelAction(); + keyLabelAction.label = keyAction.label; + keyLabelAction.toBinary(buffer); +} diff --git a/packages/uhk-web/src/app/components/popover/popover.component.html b/packages/uhk-web/src/app/components/popover/popover.component.html index cf1426a1350..af640462081 100644 --- a/packages/uhk-web/src/app/components/popover/popover.component.html +++ b/packages/uhk-web/src/app/components/popover/popover.component.html @@ -74,6 +74,16 @@ (validAction)="setKeyActionValidState($event)" > +
+ + +
@@ -117,7 +127,18 @@
-
+
+
-
+