Skip to content

Commit 90f1cdb

Browse files
refactor: move macro sidebar grouping helpers to uhk-web
Keep persisted MacroGroupingSettings in uhk-common and colocate display-time grouping, tree types, and layout-derived max depth with the Agent UI. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent b32b250 commit 90f1cdb

11 files changed

Lines changed: 172 additions & 119 deletions

File tree

packages/uhk-common/src/models/macro-grouping-settings.ts

Lines changed: 0 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -7,51 +7,9 @@ export interface MacroGroupingSettings {
77

88
export const MACRO_GROUPING_MIN_DEPTH = 1;
99

10-
// Mirrors side-menu.component.scss ($side-menu-width, $macro-tree-base-indent, $macro-tree-indent-step).
11-
const MACRO_TREE_SIDE_MENU_WIDTH_PX = 250;
12-
const MACRO_TREE_LEVEL1_PADDING_PX = 24;
13-
const MACRO_TREE_BASE_INDENT_PX = 32;
14-
const MACRO_TREE_INDENT_STEP_PX = 12;
15-
const MACRO_TREE_RESERVED_RIGHT_PX = 48;
16-
const MACRO_TREE_MIN_LABEL_WIDTH_PX = 72;
17-
18-
/**
19-
* Deepest macro-tree depth the sidebar layout can fit before labels get too narrow.
20-
* The CSS indent is unbounded; this limit comes from the 250px sidebar width.
21-
*/
22-
export const MACRO_GROUPING_MAX_DEPTH = Math.max(
23-
MACRO_GROUPING_MIN_DEPTH,
24-
Math.floor(
25-
(MACRO_TREE_SIDE_MENU_WIDTH_PX
26-
- MACRO_TREE_LEVEL1_PADDING_PX
27-
- MACRO_TREE_BASE_INDENT_PX
28-
- MACRO_TREE_RESERVED_RIGHT_PX
29-
- MACRO_TREE_MIN_LABEL_WIDTH_PX)
30-
/ MACRO_TREE_INDENT_STEP_PX
31-
)
32-
);
33-
3410
export const DEFAULT_MACRO_GROUPING_SETTINGS: MacroGroupingSettings = {
3511
camelCaseSeparation: false,
3612
enabled: false,
3713
maxDepth: 1,
3814
minChildren: 2,
3915
};
40-
41-
export function normalizeMacroGroupingSettings(
42-
settings?: Partial<MacroGroupingSettings>
43-
): MacroGroupingSettings {
44-
const merged: MacroGroupingSettings = {
45-
...DEFAULT_MACRO_GROUPING_SETTINGS,
46-
...settings,
47-
};
48-
49-
return {
50-
...merged,
51-
maxDepth: Math.min(
52-
MACRO_GROUPING_MAX_DEPTH,
53-
Math.max(MACRO_GROUPING_MIN_DEPTH, merged.maxDepth)
54-
),
55-
minChildren: Math.min(10, Math.max(2, merged.minChildren)),
56-
};
57-
}

packages/uhk-common/src/util/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ export * from './find-uhk-module-by-id.js';
1010
export * from './get-formatted-timestamp.js';
1111
export * from './get-md5-hash-from-file-name.js';
1212
export * from './get-slot-id-name.js';
13-
export * from './group-macros-by-name.js';
1413
export * from './helpers.js';
1514
export * from './is-bit-set.js';
1615
export * from './is-device-protocol-support-firmware-checksum.js';

packages/uhk-web/src/app/components/agent/settings/settings.component.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { Observable } from 'rxjs';
44
import { map } from 'rxjs/operators';
55
import { faCog } from '@fortawesome/free-solid-svg-icons';
66

7-
import { AppTheme, AppThemeSelect, MACRO_GROUPING_MAX_DEPTH, MacroGroupingSettings } from 'uhk-common';
7+
import { AppTheme, AppThemeSelect, MacroGroupingSettings } from 'uhk-common';
88
import {
99
AppState,
1010
appUpdateSettingsState,
@@ -17,6 +17,7 @@ import {
1717
getSupportedThemes,
1818
keyboardHalvesAlwaysJoined
1919
} from '../../../store';
20+
import { MACRO_GROUPING_MAX_DEPTH } from '../../../util/group-macros-by-name';
2021
import { State as UpdateSettingsState } from '../../../store/reducers/auto-update-settings';
2122
import {
2223
CheckForUpdateNowAction,

packages/uhk-web/src/app/components/side-menu/side-menu.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@
234234
<li *ngIf="node.type === 'macro'" class="sidebar__macro-tree-item">
235235
<div class="sidebar__macro-tree-entry" [routerLinkActive]="['active']">
236236
<a [routerLink]="['/macro', node.macro.id]"
237-
[class.disabled]="state.updatingFirmware">{{ getMacroDisplayName(node) }}</a>
237+
[class.disabled]="state.updatingFirmware">{{ node.macro.name }}</a>
238238
<span class="sidebar__macro_count badge rounded-pill"
239239
ngbTooltip="This is the number of times the macro is used across all keymaps."
240240
placement="bottom"

packages/uhk-web/src/app/components/side-menu/side-menu.component.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ $level-1-item-spacing: 0.25rem 1rem 0.25rem 1.5rem;
66
$level-2-item-spacing: 0.125rem 0 0.125rem 2rem;
77
$macro-tree-base-indent: 2rem;
88
$macro-tree-indent-step: 0.75rem;
9-
// Keep MACRO_GROUPING_MAX_DEPTH in uhk-common in sync with these layout values.
9+
// Keep MACRO_GROUPING_MAX_DEPTH in util/group-macros-by-name.ts in sync with these layout values.
1010

1111
:host {
1212
background-color: var(--color-sidemenu-bg);

packages/uhk-web/src/app/components/side-menu/side-menu.component.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,13 @@ import { Store } from '@ngrx/store';
3232

3333
import { combineLatest, Subscription } from 'rxjs';
3434
import { distinctUntilChanged, map } from 'rxjs/operators';
35-
import { findMacroGroupAncestorPaths, MAX_ALLOWED_MACROS_TOOLTIP, UHK_80_DEVICE } from 'uhk-common';
35+
import { MAX_ALLOWED_MACROS_TOOLTIP, UHK_80_DEVICE } from 'uhk-common';
3636

3737
import { AppState, getSelectedMacro, getSideMenuPageState } from '../../store';
3838
import { AddMacroAction } from '../../store/actions/macro';
3939
import { RenameUserConfigurationAction } from '../../store/actions/user-config';
4040
import { DeviceUiStates, MacroMenuTreeNode, SideMenuPageState } from '../../models';
41+
import { findMacroGroupAncestorPaths } from '../../util/group-macros-by-name';
4142

4243
interface SideMenuItemState {
4344
icon: IconDefinition;
@@ -195,10 +196,6 @@ export class SideMenuComponent implements OnChanges, OnInit, OnDestroy {
195196
: faChevronRight;
196197
}
197198

198-
getMacroDisplayName(node: MacroMenuTreeNode): string {
199-
return node.macro?.name || '';
200-
}
201-
202199
addMacro() {
203200
this.store.dispatch(new AddMacroAction());
204201
}
@@ -228,7 +225,7 @@ export class SideMenuComponent implements OnChanges, OnInit, OnDestroy {
228225
private syncMacroGroupState(): void {
229226
const nextState: Record<string, SideMenuItemState> = {};
230227

231-
for (const path of this.collectMacroGroupPaths(this.state?.macroTree || [])) {
228+
for (const path of this.collectMacroGroupPaths(this.state.macroTree)) {
232229
nextState[path] = this.macroGroupState[path] || {
233230
icon: faChevronUp,
234231
animation: 'active'
@@ -240,11 +237,11 @@ export class SideMenuComponent implements OnChanges, OnInit, OnDestroy {
240237

241238
private collectMacroGroupPaths(nodes: MacroMenuTreeNode[]): string[] {
242239
return nodes.flatMap(node => {
243-
if (node.type !== 'group' || !node.path) {
240+
if (node.type !== 'group') {
244241
return [];
245242
}
246243

247-
return [node.path, ...this.collectMacroGroupPaths(node.children || [])];
244+
return [node.path, ...this.collectMacroGroupPaths(node.children)];
248245
});
249246
}
250247
}
Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,21 @@
11
import { MacroMenuItem } from './macro-menu-item';
2-
import { MacroMenuTreeNode as CommonMacroMenuTreeNode } from 'uhk-common';
32

4-
export type MacroMenuTreeNode = CommonMacroMenuTreeNode<MacroMenuItem>;
3+
export interface GroupableMacroItem {
4+
id: number;
5+
name: string;
6+
}
7+
8+
export interface MacroMenuTreeGroupNode<TMacro extends GroupableMacroItem = MacroMenuItem> {
9+
children: MacroMenuTreeNode<TMacro>[];
10+
label: string;
11+
path: string;
12+
type: 'group';
13+
}
14+
15+
export interface MacroMenuTreeMacroNode<TMacro extends GroupableMacroItem = MacroMenuItem> {
16+
macro: TMacro;
17+
type: 'macro';
18+
}
19+
20+
export type MacroMenuTreeNode<TMacro extends GroupableMacroItem = MacroMenuItem> =
21+
MacroMenuTreeGroupNode<TMacro> | MacroMenuTreeMacroNode<TMacro>;

packages/uhk-web/src/app/store/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import {
2121
LEFT_KEY_CLUSTER_MODULE,
2222
LeftSlotModules,
2323
MAX_ALLOWED_MACROS,
24-
groupMacrosByName,
2524
PlayMacroAction,
2625
RIGHT_TRACKPOINT_MODULE,
2726
RightSlotModules,
@@ -60,6 +59,7 @@ import { PrivilagePageSate } from '../models/privilage-page-sate';
6059
import { SelectOptionData } from '../models/select-option-data';
6160
import { defaultUhkThemeColors } from '../util/default-uhk-theme-colors';
6261
import { escapeHtml } from '../util/escape-html';
62+
import { groupMacrosByName } from '../util/group-macros-by-name';
6363
import { parseStatusBuffer } from '../util/status-buffer-parser';
6464
import { addMissingModuleConfigs } from './reducers/add-missing-module-configs';
6565

packages/uhk-web/src/app/store/reducers/app.reducer.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import {
44
CommandLineArgs,
55
DEFAULT_MACRO_GROUPING_SETTINGS,
66
disableAgentUpgradeProtection,
7-
normalizeMacroGroupingSettings,
87
HardwareConfiguration,
98
KeyboardLayout,
109
MacroGroupingSettings,
@@ -14,6 +13,8 @@ import {
1413
UserConfiguration,
1514
} from 'uhk-common';
1615

16+
import { normalizeMacroGroupingSettings } from '../../util/group-macros-by-name';
17+
1718
import * as App from '../actions/app';
1819
import { ActionTypes as UserConfigActionTypes, SaveUserConfigSuccessAction } from '../actions/user-config';
1920
import { ActionTypes as DeviceActionTypes, ConnectionStateChangedAction } from '../actions/device';

packages/uhk-common/src/util/group-macros-by-name.test.ts renamed to packages/uhk-web/src/app/util/group-macros-by-name.test.ts

Lines changed: 66 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
import { describe, it } from 'node:test';
22

3-
import {
4-
DEFAULT_MACRO_GROUPING_SETTINGS,
5-
MACRO_GROUPING_MAX_DEPTH,
6-
normalizeMacroGroupingSettings,
7-
} from '../models/macro-grouping-settings.js';
3+
import { DEFAULT_MACRO_GROUPING_SETTINGS } from 'uhk-common';
4+
85
import {
96
findMacroGroupAncestorPaths,
107
groupMacrosByName,
118
GroupableMacroItem,
9+
MACRO_GROUPING_MAX_DEPTH,
10+
normalizeMacroGroupingSettings,
1211
splitMacroName
1312
} from './group-macros-by-name.js';
1413

@@ -77,11 +76,13 @@ describe('groupMacrosByName', () => {
7776

7877
assert.equal(result.length, 2);
7978
assert.deepEqual(result.map(node => node.type), ['macro', 'group']);
80-
assert.equal(result[0].macro?.name, 'Brightness up');
81-
assert.equal(result[1].label, 'Doom');
82-
assert.equal(result[1].children?.length, 2);
83-
assert.equal(result[1].children?.[0].macro?.name, 'Doom: Chainsaw');
84-
assert.equal(result[1].children?.[1].macro?.name, 'Doom: Plasma gun');
79+
assert.equal(result[0].type === 'macro' ? result[0].macro.name : '', 'Brightness up');
80+
assert.equal(result[1].type === 'group' ? result[1].label : '', 'Doom');
81+
assert.equal(result[1].type === 'group' ? result[1].children.length : 0, 2);
82+
if (result[1].type === 'group') {
83+
assert.equal(result[1].children[0].type === 'macro' ? result[1].children[0].macro.name : '', 'Doom: Chainsaw');
84+
assert.equal(result[1].children[1].type === 'macro' ? result[1].children[1].macro.name : '', 'Doom: Plasma gun');
85+
}
8586
});
8687

8788
it('does not create a group when there are fewer macros than minChildren', ({ assert }) => {
@@ -94,8 +95,8 @@ describe('groupMacrosByName', () => {
9495

9596
assert.equal(result.length, 2);
9697
assert.deepEqual(result.map(node => node.type), ['macro', 'macro']);
97-
assert.equal(result[0].macro?.name, 'Brightness up');
98-
assert.equal(result[1].macro?.name, 'Doom: Chainsaw');
98+
assert.equal(result[0].type === 'macro' ? result[0].macro.name : '', 'Brightness up');
99+
assert.equal(result[1].type === 'macro' ? result[1].macro.name : '', 'Doom: Chainsaw');
99100
});
100101

101102
it('groups by camel case when enabled', ({ assert }) => {
@@ -112,10 +113,15 @@ describe('groupMacrosByName', () => {
112113

113114
assert.equal(result.length, 2);
114115
assert.deepEqual(result.map(node => node.type), ['group', 'macro']);
115-
assert.equal(result[0].label, 'bind');
116-
assert.equal(result[0].children?.length, 2);
117-
assert.deepEqual(result[0].children?.map(child => child.macro?.name), ['bindMouseLeft', 'bindMouseRight']);
118-
assert.equal(result[1].macro?.name, 'brightnessUp');
116+
assert.equal(result[0].type === 'group' ? result[0].label : '', 'bind');
117+
if (result[0].type === 'group') {
118+
assert.equal(result[0].children.length, 2);
119+
assert.deepEqual(
120+
result[0].children.map(child => child.type === 'macro' ? child.macro.name : ''),
121+
['bindMouseLeft', 'bindMouseRight']
122+
);
123+
}
124+
assert.equal(result[1].type === 'macro' ? result[1].macro.name : '', 'brightnessUp');
119125
});
120126

121127
it('groups dollar-prefixed smart macro names under the dollar prefix', ({ assert }) => {
@@ -131,8 +137,13 @@ describe('groupMacrosByName', () => {
131137

132138
assert.equal(result.length, 1);
133139
assert.equal(result[0].type, 'group');
134-
assert.equal(result[0].label, '$on');
135-
assert.deepEqual(result[0].children?.map(child => child.macro?.name), ['$onInit', '$onJoin']);
140+
if (result[0].type === 'group') {
141+
assert.equal(result[0].label, '$on');
142+
assert.deepEqual(
143+
result[0].children.map(child => child.type === 'macro' ? child.macro.name : ''),
144+
['$onInit', '$onJoin']
145+
);
146+
}
136147
});
137148

138149
it('groups non-English macro names that share a prefix', ({ assert }) => {
@@ -145,8 +156,13 @@ describe('groupMacrosByName', () => {
145156

146157
assert.equal(result.length, 1);
147158
assert.equal(result[0].type, 'group');
148-
assert.equal(result[0].label, 'Az');
149-
assert.deepEqual(result[0].children?.map(child => child.macro?.name), ['Az én: első', 'Az én: második']);
159+
if (result[0].type === 'group') {
160+
assert.equal(result[0].label, 'Az');
161+
assert.deepEqual(
162+
result[0].children.map(child => child.type === 'macro' ? child.macro.name : ''),
163+
['Az én: első', 'Az én: második']
164+
);
165+
}
150166
});
151167

152168
it('keeps parent-level remainders when deeper nesting does not apply', ({ assert }) => {
@@ -161,8 +177,13 @@ describe('groupMacrosByName', () => {
161177
});
162178

163179
assert.equal(result.length, 1);
164-
assert.equal(result[0].label, 'Open');
165-
assert.deepEqual(result[0].children?.map(child => child.macro?.name), ['Open daily sites', 'Open weekly sites']);
180+
if (result[0].type === 'group') {
181+
assert.equal(result[0].label, 'Open');
182+
assert.deepEqual(
183+
result[0].children.map(child => child.type === 'macro' ? child.macro.name : ''),
184+
['Open daily sites', 'Open weekly sites']
185+
);
186+
}
166187
});
167188

168189
it('places a macro named like a group inside that group', ({ assert }) => {
@@ -176,12 +197,13 @@ describe('groupMacrosByName', () => {
176197

177198
assert.equal(result.length, 1);
178199
assert.equal(result[0].type, 'group');
179-
assert.equal(result[0].label, 'Doom');
180-
assert.deepEqual(result[0].children?.map(child => child.macro?.name), [
181-
'Doom',
182-
'Doom: Chainsaw',
183-
'Doom: Plasma gun',
184-
]);
200+
if (result[0].type === 'group') {
201+
assert.equal(result[0].label, 'Doom');
202+
assert.deepEqual(
203+
result[0].children.map(child => child.type === 'macro' ? child.macro.name : ''),
204+
['Doom', 'Doom: Chainsaw', 'Doom: Plasma gun']
205+
);
206+
}
185207
});
186208

187209
it('places an exact subgroup name inside a nested group', ({ assert }) => {
@@ -197,15 +219,18 @@ describe('groupMacrosByName', () => {
197219
});
198220

199221
assert.equal(result.length, 1);
200-
assert.equal(result[0].label, 'Open');
201-
assert.equal(result[0].children?.length, 1);
202-
assert.equal(result[0].children?.[0].type, 'group');
203-
assert.equal(result[0].children?.[0].label, 'daily');
204-
assert.deepEqual(result[0].children?.[0].children?.map(child => child.macro?.name), [
205-
'Open: daily',
206-
'Open: daily: evening',
207-
'Open: daily: morning',
208-
]);
222+
if (result[0].type === 'group') {
223+
assert.equal(result[0].label, 'Open');
224+
assert.equal(result[0].children.length, 1);
225+
assert.equal(result[0].children[0].type, 'group');
226+
if (result[0].children[0].type === 'group') {
227+
assert.equal(result[0].children[0].label, 'daily');
228+
assert.deepEqual(
229+
result[0].children[0].children.map(child => child.type === 'macro' ? child.macro.name : ''),
230+
['Open: daily', 'Open: daily: evening', 'Open: daily: morning']
231+
);
232+
}
233+
}
209234
});
210235

211236
it('keeps a same-named macro outside when the group is not created', ({ assert }) => {
@@ -217,7 +242,10 @@ describe('groupMacrosByName', () => {
217242
const result = groupMacrosByName(macros, ENABLED_MACRO_GROUPING_SETTINGS);
218243

219244
assert.equal(result.length, 2);
220-
assert.deepEqual(result.map(node => node.macro?.name), ['Doom', 'Doom: Chainsaw']);
245+
assert.deepEqual(
246+
result.map(node => node.type === 'macro' ? node.macro.name : ''),
247+
['Doom', 'Doom: Chainsaw']
248+
);
221249
});
222250
});
223251

0 commit comments

Comments
 (0)