From 226cc1e94b6300073d7317127d8e9555de547579 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kiss=20R=C3=B3bert?= Date: Sun, 22 Mar 2026 19:06:05 +0100 Subject: [PATCH 1/9] feat: command prompt for zephyr log --- .../uhk-agent/src/services/device.service.ts | 27 ++++++++++++++ .../src/services/zephyr-log.service.ts | 31 ++++++++++++++++ packages/uhk-common/src/util/ipcEvents.ts | 3 ++ packages/uhk-usb/src/uhk-operations.ts | 14 ++++++++ .../advanced-settings.page.component.html | 36 +++++++++++++++++-- .../advanced-settings.page.component.ts | 33 ++++++++++++++--- .../app/components/xterm/xterm.component.html | 2 +- .../app/components/xterm/xterm.component.scss | 8 +++-- .../app/services/device-renderer.service.ts | 12 +++++++ .../uhk-web/src/app/store/actions/device.ts | 24 +++++++++++++ .../uhk-web/src/app/store/effects/device.ts | 33 +++++++++++++++++ 11 files changed, 213 insertions(+), 10 deletions(-) diff --git a/packages/uhk-agent/src/services/device.service.ts b/packages/uhk-agent/src/services/device.service.ts index b2e69809e57..d4596092139 100644 --- a/packages/uhk-agent/src/services/device.service.ts +++ b/packages/uhk-agent/src/services/device.service.ts @@ -125,6 +125,7 @@ export class DeviceService { currentDeviceFn: getCurrentUhkDongleHID, logService: this.logService, ipcEvents: { + execShellCommand: IpcEvents.device.execShellCommandOnDongle, isZephyrLoggingEnabled: IpcEvents.device.isDongleZephyrLoggingEnabled, isZephyrLoggingEnabledReply: IpcEvents.device.isDongleZephyrLoggingEnabledReply, toggleZephyrLogging: IpcEvents.device.toggleDongleZephyrLogging, @@ -138,6 +139,7 @@ export class DeviceService { currentDeviceFn: getCurrenUhk80LeftHID, logService: this.logService, ipcEvents: { + execShellCommand: IpcEvents.device.execShellCommandOnLeftHalf, isZephyrLoggingEnabled: IpcEvents.device.isLeftHalfZephyrLoggingEnabled, isZephyrLoggingEnabledReply: IpcEvents.device.isLeftHalfZephyrLoggingEnabledReply, toggleZephyrLogging: IpcEvents.device.toggleLeftHalfZephyrLogging, @@ -195,6 +197,15 @@ export class DeviceService { }); }); + ipcMain.on(IpcEvents.device.execShellCommandOnRightHalf, (...args) => { + this.queueManager.add({ + method: this.execShellCommand, + bind: this, + params: args, + asynchronous: true + }); + }); + ipcMain.on(IpcEvents.device.toggleI2cDebugging, this.toggleI2cDebugging.bind(this)); ipcMain.on(IpcEvents.device.isRightHalfZephyrLoggingEnabled, (...args) => { @@ -973,6 +984,22 @@ export class DeviceService { event.sender.send(IpcEvents.device.eraseBleSettingsReply, response); } + public async execShellCommand(_: Electron.IpcMainEvent, [command]): Promise { + this.logService.misc(`[DeviceService] execute shell command: ${command}`); + + try { + await this.stopPollUhkDevice(); + await this.operations.execShellCommand(command); + this.logService.misc('[DeviceService] execute shell command success'); + } + catch(error) { + this.logService.error('[DeviceService] execute shell command failed', error); + } + finally { + this.startPollUhkDevice(); + } + } + public async startDonglePairing(event: Electron.IpcMainEvent): Promise { this.logService.misc('[DeviceService] start Dongle pairing'); try { diff --git a/packages/uhk-agent/src/services/zephyr-log.service.ts b/packages/uhk-agent/src/services/zephyr-log.service.ts index 1172f5ac0d1..4a56eed7a12 100644 --- a/packages/uhk-agent/src/services/zephyr-log.service.ts +++ b/packages/uhk-agent/src/services/zephyr-log.service.ts @@ -11,6 +11,7 @@ export interface ZephyrLogServiceOptions { currentDeviceFn: typeof getCurrenUhk80LeftHID | typeof getCurrentUhkDongleHID; logService: LogService; ipcEvents: { + execShellCommand: string; isZephyrLoggingEnabled: string; isZephyrLoggingEnabledReply: string; toggleZephyrLogging: string; @@ -31,6 +32,15 @@ export class ZephyrLogService { private operationLimiter = pLimit(1); constructor(private options: ZephyrLogServiceOptions) { + ipcMain.on(options.ipcEvents.execShellCommand, (...args) => { + this.queueManager.add({ + method: this.execShellCommand, + bind: this, + params: args, + asynchronous: true + }); + }); + ipcMain.on(options.ipcEvents.isZephyrLoggingEnabled, (...args) => { this.queueManager.add({ method: this.isZephyrLoggingEnabled, @@ -81,6 +91,27 @@ export class ZephyrLogService { this.options.logService.misc(`[ZephyrLogService | ${this.options.uhkDeviceProduct.logName}] Disabled`); } + private async execShellCommand(_: Electron.IpcMainEvent, [command]): Promise { + try { + await this.pauseLogging(); + + const operations = await this.getOperations(); + if (!operations) { + const logEntry: ZephyrLogEntry = { + log: "Device is not connected. Can't execute shell command", + level: 'error', + device: this.options.uhkDeviceProduct.logName, + } + this.options.win.webContents.send(IpcEvents.device.zephyrLog, logEntry) + return; + } + await operations.execShellCommand(command); + } + finally { + await this.resumeLogging(); + } + } + private async getOperations(logEarlierInited = true): Promise { if (logEarlierInited) { this.options.logService.misc(`[ZephyrLogService | ${this.options.uhkDeviceProduct.logName}] getOperations`); diff --git a/packages/uhk-common/src/util/ipcEvents.ts b/packages/uhk-common/src/util/ipcEvents.ts index 386a2cd303e..61916764982 100644 --- a/packages/uhk-common/src/util/ipcEvents.ts +++ b/packages/uhk-common/src/util/ipcEvents.ts @@ -31,6 +31,9 @@ export class Device { public static readonly dongleVersionInfoLoaded = 'device-dongle-version-info-loaded'; public static readonly eraseBleSettings = 'device-erase-ble-settings'; public static readonly eraseBleSettingsReply = 'device-erase-ble-settings-reply'; + public static readonly execShellCommandOnDongle = 'device-exec-shell-command-on-dongle'; + public static readonly execShellCommandOnLeftHalf = 'device-exec-shell-command-on-left-half'; + public static readonly execShellCommandOnRightHalf = 'device-exec-shell-command-on-right-half'; public static readonly hardwareModulesLoaded = 'device-hardware-modules-loaded'; public static readonly isDongleZephyrLoggingEnabled = 'device-is-dongle-zephyr-logging-enabled'; public static readonly isDongleZephyrLoggingEnabledReply = 'device-is-dongle-zephyr-logging-enabled-reply'; diff --git a/packages/uhk-usb/src/uhk-operations.ts b/packages/uhk-usb/src/uhk-operations.ts index 190b1e163f8..0d42e2a1bf3 100644 --- a/packages/uhk-usb/src/uhk-operations.ts +++ b/packages/uhk-usb/src/uhk-operations.ts @@ -936,4 +936,18 @@ export class UhkOperations { await this.device.write(buffer); } + + public async execShellCommand(cmd: string): Promise { + this.logService.usbOps('[DeviceOperation] USB[T]: Execute Shell Command'); + const b1 = Buffer.from([UsbCommand.ExecShellCommand]); + const b2 = Buffer.from(cmd); + const b0 = Buffer.from([0x00]); + const buffer = Buffer.concat([b1, b2, b0]); + + if (buffer.length > MAX_USB_PAYLOAD_SIZE) { + throw new Error('Shel command is too long. At most 61 characters are supported.') + } + + await this.device.write(buffer); + } } diff --git a/packages/uhk-web/src/app/components/device/advanced-settings/advanced-settings.page.component.html b/packages/uhk-web/src/app/components/device/advanced-settings/advanced-settings.page.component.html index e713b44383d..f5cd51252b2 100644 --- a/packages/uhk-web/src/app/components/device/advanced-settings/advanced-settings.page.component.html +++ b/packages/uhk-web/src/app/components/device/advanced-settings/advanced-settings.page.component.html @@ -79,7 +79,7 @@

-
- + +
diff --git a/packages/uhk-web/src/app/components/zephyr-terminal/zephyr-terminal.component.html b/packages/uhk-web/src/app/components/zephyr-terminal/zephyr-terminal.component.html new file mode 100644 index 00000000000..c74d5a40087 --- /dev/null +++ b/packages/uhk-web/src/app/components/zephyr-terminal/zephyr-terminal.component.html @@ -0,0 +1 @@ +
diff --git a/packages/uhk-web/src/app/components/zephyr-terminal/zephyr-terminal.component.scss b/packages/uhk-web/src/app/components/zephyr-terminal/zephyr-terminal.component.scss new file mode 100644 index 00000000000..4cfd8c1958a --- /dev/null +++ b/packages/uhk-web/src/app/components/zephyr-terminal/zephyr-terminal.component.scss @@ -0,0 +1,15 @@ +:host { + display: flex; + flex-direction: column; + align-items: stretch; + width: 100%; + height: 100%; +} + +.zephyr-terminal { + flex: 1; + min-height: 0; + background-color: var(--color-xterm-bg); + border: 1px solid var(--color-xterm-border); + padding: 4px; +} diff --git a/packages/uhk-web/src/app/components/zephyr-terminal/zephyr-terminal.component.ts b/packages/uhk-web/src/app/components/zephyr-terminal/zephyr-terminal.component.ts new file mode 100644 index 00000000000..8c63071cf37 --- /dev/null +++ b/packages/uhk-web/src/app/components/zephyr-terminal/zephyr-terminal.component.ts @@ -0,0 +1,87 @@ +import { + AfterViewInit, + ChangeDetectionStrategy, + Component, + ElementRef, + OnDestroy, + ViewChild, +} from '@angular/core'; +import { Actions, ofType } from '@ngrx/effects'; +import { Store } from '@ngrx/store'; +import { FitAddon } from '@xterm/addon-fit'; +import { Terminal } from '@xterm/xterm'; +import { Subscription } from 'rxjs'; +import { UHK_80_DEVICE } from 'uhk-common'; + +import { AppState } from '../../store'; +import { ActionTypes as AdvancedSettingsActionTypes, ZephyrLogAction } from '../../store/actions/advance-settings.action'; +import { ExecShellCommandOnRightHalfAction } from '../../store/actions/device'; + +/** + * Proof-of-concept VT100 terminal for the UHK 80 right half. + * + * Output: the raw shell buffer chunks emitted by the right-half poller (device === UHK 80 right) + * are written verbatim to xterm.js, so colors and cursor control render. + * + * Input: every keystroke (incl. ESC sequences for arrows, Tab, Ctrl-C, Enter) is forwarded to the + * device through the ExecShellCommand byte channel. Echo and history are produced by the firmware's + * shell, so they only light up once the firmware injects these bytes into the interactive shell + * input. NUL (0x00) is the only byte the transport can't carry, and VT100 treats NUL as ignorable + * fill, so nothing meaningful is lost. + */ +@Component({ + selector: 'zephyr-terminal', + standalone: false, + templateUrl: './zephyr-terminal.component.html', + styleUrls: ['./zephyr-terminal.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ZephyrTerminalComponent implements AfterViewInit, OnDestroy { + @ViewChild('terminal', { static: true }) terminalElement: ElementRef; + + private terminal: Terminal; + private fitAddon: FitAddon; + private logSubscription: Subscription; + private resizeObserver: ResizeObserver; + + constructor( + private store: Store, + private actions$: Actions, + ) {} + + ngAfterViewInit(): void { + this.terminal = new Terminal({ + convertEol: false, + fontFamily: 'monospace', + cursorBlink: true, + scrollback: 10000, + }); + this.fitAddon = new FitAddon(); + this.terminal.loadAddon(this.fitAddon); + this.terminal.open(this.terminalElement.nativeElement); + this.fitAddon.fit(); + + // Forward every keystroke (raw bytes, incl. ESC sequences) to the right half. + this.terminal.onData((data: string) => { + this.store.dispatch(new ExecShellCommandOnRightHalfAction(data)); + }); + + // Write raw shell output coming from the right half straight into the terminal. + this.logSubscription = this.actions$ + .pipe(ofType(AdvancedSettingsActionTypes.zephyrLog)) + .subscribe((action: ZephyrLogAction) => { + if (action.payload.device === UHK_80_DEVICE.logName) { + this.terminal.write(action.payload.log); + } + }); + + this.resizeObserver = new ResizeObserver(() => this.fitAddon.fit()); + this.resizeObserver.observe(this.terminalElement.nativeElement); + } + + ngOnDestroy(): void { + this.logSubscription?.unsubscribe(); + this.resizeObserver?.disconnect(); + this.terminal?.dispose(); + } +} diff --git a/packages/uhk-web/src/app/shared.module.ts b/packages/uhk-web/src/app/shared.module.ts index a4fbbe3bf1f..e2c7d547446 100644 --- a/packages/uhk-web/src/app/shared.module.ts +++ b/packages/uhk-web/src/app/shared.module.ts @@ -147,6 +147,7 @@ import { UpdateAgentPageComponent } from './pages/update-agent.page'; import { UpdateFirmwarePageComponent } from './pages/update-firmware.page'; import { UhkDeviceLoadingGuard } from './services/uhk-device-loading.guard'; import { XtermComponent } from './components/xterm/xterm.component'; +import { ZephyrTerminalComponent } from './components/zephyr-terminal/zephyr-terminal.component'; import { SliderWrapperComponent } from './components/slider-wrapper/slider-wrapper.component'; import { EditableTextComponent } from './components/editable-text/editable-text.component'; import { Autofocus } from './directives/autofocus/autofocus.directive'; @@ -271,6 +272,7 @@ import appInitFactory from './services/app-init-factory'; UpdateAgentPageComponent, UpdateFirmwarePageComponent, XtermComponent, + ZephyrTerminalComponent, SliderWrapperComponent, EditableTextComponent, Autofocus, From 48f6b7495bf653d38823b3fd262d0a500748e049 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kiss=20R=C3=B3bert?= Date: Sat, 6 Jun 2026 11:38:34 +0200 Subject: [PATCH 3/9] fix: use JetBrains Mono font for xterm terminal --- .../app/components/zephyr-terminal/zephyr-terminal.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/uhk-web/src/app/components/zephyr-terminal/zephyr-terminal.component.ts b/packages/uhk-web/src/app/components/zephyr-terminal/zephyr-terminal.component.ts index 8c63071cf37..0e945c0a854 100644 --- a/packages/uhk-web/src/app/components/zephyr-terminal/zephyr-terminal.component.ts +++ b/packages/uhk-web/src/app/components/zephyr-terminal/zephyr-terminal.component.ts @@ -52,7 +52,7 @@ export class ZephyrTerminalComponent implements AfterViewInit, OnDestroy { ngAfterViewInit(): void { this.terminal = new Terminal({ convertEol: false, - fontFamily: 'monospace', + fontFamily: 'JetBrains Mono', cursorBlink: true, scrollback: 10000, }); From fb44222bb74dad8b4b5f212553c1e9e0b324969c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kiss=20R=C3=B3bert?= Date: Sat, 6 Jun 2026 11:39:09 +0200 Subject: [PATCH 4/9] fix: move all dependencies to dev dependencies of uhk-web project --- packages/uhk-web/package-lock.json | 142 +++++++++++++++++------------ packages/uhk-web/package.json | 8 +- 2 files changed, 86 insertions(+), 64 deletions(-) diff --git a/packages/uhk-web/package-lock.json b/packages/uhk-web/package-lock.json index b08c3a9cd10..17bba2ddd91 100644 --- a/packages/uhk-web/package-lock.json +++ b/packages/uhk-web/package-lock.json @@ -8,11 +8,6 @@ "name": "uhk-web", "version": "1.0.0", "license": "See in LICENSE", - "dependencies": { - "@xterm/addon-fit": "^0.11.0", - "@xterm/xterm": "^6.0.0", - "file-saver": "2.0.5" - }, "devDependencies": { "@angular-devkit/build-angular": "20.3.26", "@angular/animations": "20.3.21", @@ -41,12 +36,15 @@ "@ngrx/store": "20.1.0", "@ngrx/store-devtools": "20.1.0", "@perfectmemory/ngx-contextmenu": "20.0.0", + "@xterm/addon-fit": "0.11.0", + "@xterm/xterm": "6.0.0", "angular-confirmation-popover": "7.0.0", "angular-eslint": "20.7.0", "angular-split": "20.0.0", "bootstrap": "5.3.8", "colord": "2.9.3", "dragula": "3.7.3", + "file-saver": "2.0.5", "gramli-angular-notifier": "18.0.0", "monaco-editor": "0.55.1", "naive-autocompletion-parser": "1.1.9", @@ -607,7 +605,6 @@ "integrity": "sha512-CVskZnF38IIxVVlKWi1VCz7YH/gHMJu2IY9bD1AVoBBGIe0xA4FRXJkW2Y+EDs9vQqZTkZZljhK5gL65Ro1PeQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@angular-eslint/bundled-angular-compiler": "20.7.0", "eslint-scope": "^9.0.0" @@ -638,7 +635,6 @@ "integrity": "sha512-HsZJzsFwwLp4l6XzhWIWPXjb1SN76XDSfLgp08PYdL3t1VKWAlvgA68xgmWICjkL/FXDMx4AqrdUV5v7qHmV1Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -754,7 +750,6 @@ "integrity": "sha512-7bZxc01URbiPiIBWThQ69XwOxVduqEKN4PhpbF2AAyfMc/W8Hcr4VoIJOwL0O1Nkq5beS8pCAqoOeIgFyXd/kg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "parse5": "^8.0.0", "tslib": "^2.3.0" @@ -806,7 +801,6 @@ "integrity": "sha512-BjVpJYaC0T73nIT4BCOR5jcFUcXAFe2kKPlSDfD8mjBl8+Ug3Uy/Xs3Blx/6IKDLwNkbCUAxo/IwT+zzpTYNpw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -824,7 +818,6 @@ "integrity": "sha512-XufWMAUS0gNKE29LkDFf21eTxdBN2WIcN/3ownqfYqKwDzMMpUAm5EEjPYfn0b/2/b5OYGNJrU4yeTHoz0Bt7g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -838,7 +831,6 @@ "integrity": "sha512-Bo0lCIm1YZ18b4abP7InqP9q3uxcOzbVxYwdZYTiht4mrV5HUTBjsI5Yn8GL7eXXpjmY4e1CrdDe81+nNDn+0A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/core": "7.28.3", "@jridgewell/sourcemap-codec": "^1.4.14", @@ -872,7 +864,6 @@ "integrity": "sha512-sZMWyxh6dkuT6F90epkM1F7E2WJeduHoA9PRHYChIIVFnBd0VKITxpfrwRQS44IPJAtR456/5twtrcCNBALweQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -899,7 +890,6 @@ "integrity": "sha512-2T2K+32pSCjLH0SmM6ORrQrtyZKOk70APDl3wLUu33aExnrtzI98A32TdJ+G8+FTtZKTUDT22b6aQj+xvSewpA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -929,7 +919,6 @@ "integrity": "sha512-hgd7hnL2tF41HoBEpwYEIY55AUqu8jF65qCrQhDf0v9Mqv5ei5hjaU0NaRdGOy6ktyFN34v+InHi4c0caMm6Pw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/core": "7.28.3", "@types/babel__core": "7.20.5", @@ -955,7 +944,6 @@ "integrity": "sha512-hhsB2dv4Iq8SMmh50fYkMA2IOQ7JGXfQSBS4pPC3zj5njVQZtSNEJ/il8QNjYyw/KsMSFffhA/if6vSAumYvUg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -998,7 +986,6 @@ "integrity": "sha512-8MLfjHVnVs7CLd6ZY9dbwtJfqFqKx9thr0dKzHdlPc/lorGckQ6aco6yOQnPuIeUVIp/zqxokFba27q7Z1aGxw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -1043,7 +1030,6 @@ "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -3219,6 +3205,7 @@ "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } @@ -3229,6 +3216,7 @@ "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", @@ -3243,7 +3231,8 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@eslint/config-array/node_modules/brace-expansion": { "version": "1.1.15", @@ -3251,6 +3240,7 @@ "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3262,6 +3252,7 @@ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -3275,6 +3266,7 @@ "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@eslint/core": "^0.17.0" }, @@ -3288,6 +3280,7 @@ "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@types/json-schema": "^7.0.15" }, @@ -3301,6 +3294,7 @@ "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ajv": "^6.14.0", "debug": "^4.3.2", @@ -3325,6 +3319,7 @@ "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -3341,7 +3336,8 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { "version": "1.1.15", @@ -3349,6 +3345,7 @@ "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3360,6 +3357,7 @@ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 4" } @@ -3369,7 +3367,8 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@eslint/eslintrc/node_modules/minimatch": { "version": "3.1.5", @@ -3377,6 +3376,7 @@ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -3390,6 +3390,7 @@ "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -3403,6 +3404,7 @@ "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", "dev": true, "license": "Apache-2.0", + "peer": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } @@ -3413,6 +3415,7 @@ "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" @@ -3523,6 +3526,7 @@ "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@humanfs/types": "^0.15.0" }, @@ -3536,6 +3540,7 @@ "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@humanfs/core": "^0.19.2", "@humanfs/types": "^0.15.0", @@ -3551,6 +3556,7 @@ "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", "dev": true, "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=18.18.0" } @@ -3561,6 +3567,7 @@ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=12.22" }, @@ -3575,6 +3582,7 @@ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=18.18" }, @@ -3819,7 +3827,6 @@ "integrity": "sha512-nqhDw2ZcAUrKNPwhjinJny903bRhI0rQhiDz1LksjeRxqa36i3l75+4iXbOy0rlDpLJGxqtgoPavQjmmyS5UJw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@inquirer/checkbox": "^4.2.1", "@inquirer/confirm": "^5.1.14", @@ -5095,7 +5102,6 @@ "integrity": "sha512-o8j3CGAGedm+BIb+QDhNXrVaU//n9uF0wH0HZWtXHmW1mjRBaQiUA+ZPMUkDwAeN8KuOcoIEC+2QUXxXGVI7ow==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.0.0" }, @@ -6442,7 +6448,6 @@ "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } @@ -6555,6 +6560,7 @@ "integrity": "sha512-QYb/sa74/s7OKMbACMjrYnGspj9Hs5YI5aaffSL65UfeBUzVzBJfVo3oWSpbzPurvm7yaCCo2Lk7lVj610HqKw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.60.0", @@ -6667,6 +6673,7 @@ "integrity": "sha512-SX46wEUtitCpq7AN38HkUU/+zvUpdKf7ephtWAFgckH8O7PQIyL5gvrhQgBLuEYgLfuKWOVvWVskMbuFHAz5xg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/types": "8.60.0", "@typescript-eslint/typescript-estree": "8.60.0", @@ -6692,7 +6699,6 @@ "integrity": "sha512-AsE7x2XaAK+CVbeih0Fvbn+r1qHxtpLDJ3XUuFcIinT318T90yHMJC+Zgv+jUuDjQQd06HKwxnDu6sz1IcTilA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -6765,7 +6771,6 @@ "integrity": "sha512-HtXuPfrHTyBDkameWpl+vJb1Uevu2tznAyahM1Oc4AENidCLTPiZDWIo4GfcxNdC/RcfGcadzzkqbRG87dUrQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.60.0", @@ -6993,12 +6998,14 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.11.0.tgz", "integrity": "sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==", + "dev": true, "license": "MIT" }, "node_modules/@xterm/xterm": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.0.0.tgz", "integrity": "sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg==", + "dev": true, "license": "MIT", "workspaces": [ "addons/*" @@ -7055,7 +7062,6 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -7082,6 +7088,7 @@ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, "license": "MIT", + "peer": true, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } @@ -7131,7 +7138,6 @@ "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -7677,7 +7683,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -7825,6 +7830,7 @@ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -8114,7 +8120,8 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/connect-history-api-fallback": { "version": "2.0.0", @@ -8432,7 +8439,8 @@ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/default-browser": { "version": "5.5.0", @@ -8604,7 +8612,6 @@ "integrity": "sha512-/rRg4zRhcpf81TyDhaHLtXt6sEywdfpv1cRUMeFFy7DuypH2U0WUL0GTdyAQvXegviT4PJK4KuMmOaIDpICseQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "contra": "1.9.4", "crossvent": "1.5.5" @@ -8865,6 +8872,7 @@ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -8971,6 +8979,7 @@ "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -8987,7 +8996,8 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/eslint/node_modules/brace-expansion": { "version": "1.1.15", @@ -8995,6 +9005,7 @@ "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -9006,6 +9017,7 @@ "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -9023,6 +9035,7 @@ "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -9036,6 +9049,7 @@ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 4" } @@ -9045,7 +9059,8 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/eslint/node_modules/minimatch": { "version": "3.1.5", @@ -9053,6 +9068,7 @@ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -9066,6 +9082,7 @@ "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", @@ -9084,6 +9101,7 @@ "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -9097,6 +9115,7 @@ "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", "dev": true, "license": "BSD-3-Clause", + "peer": true, "dependencies": { "estraverse": "^5.1.0" }, @@ -9200,7 +9219,6 @@ "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -9300,14 +9318,16 @@ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/fast-uri": { "version": "3.1.2", @@ -9373,6 +9393,7 @@ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "flat-cache": "^4.0.0" }, @@ -9384,6 +9405,7 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==", + "dev": true, "license": "MIT" }, "node_modules/fill-range": { @@ -9454,6 +9476,7 @@ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" @@ -9467,7 +9490,8 @@ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, - "license": "ISC" + "license": "ISC", + "peer": true }, "node_modules/follow-redirects": { "version": "1.16.0", @@ -9695,6 +9719,7 @@ "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -9788,7 +9813,6 @@ "integrity": "sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=16.9.0" } @@ -10105,6 +10129,7 @@ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=0.8.19" } @@ -10443,7 +10468,6 @@ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "dev": true, "license": "MIT", - "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -10496,7 +10520,8 @@ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/json-parse-even-better-errors": { "version": "5.0.0", @@ -10527,7 +10552,8 @@ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/json5": { "version": "2.2.3", @@ -10575,6 +10601,7 @@ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "json-buffer": "3.0.1" } @@ -10606,7 +10633,6 @@ "integrity": "sha512-kdTwsyRuncDfjEs0DlRILWNvxhDG/Zij4YLO4TMJgDLW+8OzpfkdPnRgrsRuY1o+oaxJGWsps5f/RVBgGmmN0w==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "copy-anything": "^2.0.1", "parse-node-version": "^1.0.1", @@ -10672,6 +10698,7 @@ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" @@ -10711,7 +10738,6 @@ "integrity": "sha512-SL0JY3DaxylDuo/MecFeiC+7pedM0zia33zl0vcjgwcq1q1FWWF1To9EIauPbl8GbMCU0R2e0uJ8bZunhYKD2g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "cli-truncate": "^4.0.0", "colorette": "^2.0.20", @@ -10849,7 +10875,8 @@ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/log-symbols": { "version": "6.0.0", @@ -11548,7 +11575,8 @@ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/needle": { "version": "3.5.0", @@ -11838,8 +11866,7 @@ "resolved": "https://registry.npmjs.org/nouislider/-/nouislider-15.8.1.tgz", "integrity": "sha512-93TweAi8kqntHJSPiSWQ1o/uZ29VWOmal9YKb6KKGGlCkugaNfAupT7o1qTHqdJvNQ7S0su5rO6qRFCjP8fxtw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/npm-bundled": { "version": "5.0.0", @@ -12080,6 +12107,7 @@ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", @@ -12524,7 +12552,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -12663,6 +12690,7 @@ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.8.0" } @@ -12726,6 +12754,7 @@ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=6" } @@ -13141,7 +13170,6 @@ "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -13180,7 +13208,6 @@ "integrity": "sha512-9GUyuksjw70uNpb1MTYWsH9MQHOHY6kwfnkafC24+7aOMZn9+rVMBxRbLvw756mrBFbIsFg6Xw9IkR2Fnn3k+Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", @@ -13977,6 +14004,7 @@ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -14044,7 +14072,6 @@ "integrity": "sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.14.0", @@ -14242,8 +14269,7 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true, - "license": "0BSD", - "peer": true + "license": "0BSD" }, "node_modules/tuf-js": { "version": "4.1.0", @@ -14266,6 +14292,7 @@ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "prelude-ls": "^1.2.1" }, @@ -14465,6 +14492,7 @@ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "punycode": "^2.1.0" } @@ -14523,7 +14551,6 @@ "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -15132,7 +15159,6 @@ "integrity": "sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -15235,7 +15261,6 @@ "integrity": "sha512-QcQ72gh8a+7JO63TAx/6XZf/CWhgMzu5m0QirvPfGvptOusAxG12w2+aua1Jkjr7hzaWDnJ2n6JFeexMHI+Zjg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/bonjour": "^3.5.13", "@types/connect-history-api-fallback": "^1.5.4", @@ -15852,6 +15877,7 @@ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -16110,7 +16136,6 @@ "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -16130,8 +16155,7 @@ "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.1.tgz", "integrity": "sha512-XE96n56IQpJM7NAoXswY3XRLcWFW83xe0BiAOeMD7K5k5xecOeul3Qcpx6GqEeeHNkW5DWL5zOyTbEfB4eti8w==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" } } } diff --git a/packages/uhk-web/package.json b/packages/uhk-web/package.json index 6da2caa6173..11e98eb00e7 100644 --- a/packages/uhk-web/package.json +++ b/packages/uhk-web/package.json @@ -43,12 +43,15 @@ "@ngrx/store": "20.1.0", "@ngrx/store-devtools": "20.1.0", "@perfectmemory/ngx-contextmenu": "20.0.0", + "@xterm/addon-fit": "0.11.0", + "@xterm/xterm": "6.0.0", "angular-confirmation-popover": "7.0.0", "angular-eslint": "20.7.0", "angular-split": "20.0.0", "bootstrap": "5.3.8", "colord": "2.9.3", "dragula": "3.7.3", + "file-saver": "2.0.5", "gramli-angular-notifier": "18.0.0", "monaco-editor": "0.55.1", "naive-autocompletion-parser": "1.1.9", @@ -63,10 +66,5 @@ "uhk-common": "file:../uhk-common", "xml-loader": "1.2.1", "zone.js": "0.15.1" - }, - "dependencies": { - "@xterm/addon-fit": "^0.11.0", - "@xterm/xterm": "^6.0.0", - "file-saver": "2.0.5" } } From aedfdbdcd5f1cd9ec8867694065f43c7cc089940 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kiss=20R=C3=B3bert?= Date: Sat, 6 Jun 2026 14:33:54 +0200 Subject: [PATCH 5/9] feat: support multiple halves --- .../uhk-agent/src/services/device.service.ts | 33 +++---------- .../src/services/zephyr-log.service.ts | 46 +++++++++++++++---- .../src/log/escape-zephyr-control-chars.ts | 24 ++++++++++ packages/uhk-common/src/log/index.ts | 1 + .../advanced-settings.page.component.html | 45 ++++++------------ .../advanced-settings.page.component.ts | 36 ++++++--------- .../zephyr-terminal.component.html | 9 +++- .../zephyr-terminal.component.scss | 22 +++++++-- .../zephyr-terminal.component.ts | 46 ++++++++++++++----- 9 files changed, 156 insertions(+), 106 deletions(-) create mode 100644 packages/uhk-common/src/log/escape-zephyr-control-chars.ts diff --git a/packages/uhk-agent/src/services/device.service.ts b/packages/uhk-agent/src/services/device.service.ts index 395c816e528..174011cada7 100644 --- a/packages/uhk-agent/src/services/device.service.ts +++ b/packages/uhk-agent/src/services/device.service.ts @@ -2,7 +2,6 @@ import { ipcMain } from 'electron'; import { cloneDeep, isEqual } from 'lodash'; import { rm } from 'node:fs/promises'; import os from 'node:os'; -import { UhkDeviceProduct } from 'uhk-common'; import { ALL_UHK_DEVICES, AreBleAddressesPairedIpcResponse, @@ -14,6 +13,7 @@ import { convertBleStringToNumberArray, CurrentlyUpdatingModuleInfo, DeviceConnectionState, + escapeZephyrControlChars, findUhkModuleById, FIRMWARE_UPGRADE_METHODS, FirmwareUpgradeIpcResponse, @@ -47,6 +47,7 @@ import { UHK_DONGLE, UHK_MODULE_IDS, UHK_MODULES, + UhkDeviceProduct, UpdateFirmwareData, UploadFileData, VERSIONS, @@ -991,6 +992,9 @@ export class DeviceService { await this.stopPollUhkDevice(); await this.operations.execShellCommand(command); this.logService.misc('[DeviceService] execute shell command success'); + // give some time for the command to complete + await snooze(5); + await this.readZephyrLog(); } catch(error) { this.logService.error('[DeviceService] execute shell command failed', error); @@ -1445,36 +1449,11 @@ export class DeviceService { } } - /** - * Render a chunk so every control/whitespace byte is visible in the log. Printable chars pass - * through; common controls get named escapes (\r \n \t \e), everything else control-range - * becomes \xNN, and the chunk is wrapped in ⟦…⟧ so leading/trailing spaces are obvious. - */ - private escapeControlChars(input: string): string { - let out = ''; - for (const ch of input) { - const code = ch.charCodeAt(0); - switch (ch) { - case '\x1b': out += '\\e'; break; - case '\r': out += '\\r'; break; - case '\n': out += '\\n'; break; - case '\t': out += '\\t'; break; - case '\\': out += '\\\\'; break; - default: - out += code < 0x20 || code === 0x7f - ? '\\x' + code.toString(16).padStart(2, '0') - : ch; - } - } - - return out; - } - private async readZephyrLog(): Promise { try { const uhkDeviceProduct = await getCurrentUhkDeviceProduct(this.options); const log = await this.operations.getVariable(UsbVariables.ShellBuffer) - this.logService.misc(`[DeviceService] Right half zephyr log (escaped): ⟦${this.escapeControlChars(log as string)}⟧`); + this.logService.misc(`[DeviceService] Right half zephyr log (escaped): ⟦${escapeZephyrControlChars(log as string)}⟧`); const logEntry: ZephyrLogEntry = { log: log as string, level: 'info', diff --git a/packages/uhk-agent/src/services/zephyr-log.service.ts b/packages/uhk-agent/src/services/zephyr-log.service.ts index 4a56eed7a12..08411720693 100644 --- a/packages/uhk-agent/src/services/zephyr-log.service.ts +++ b/packages/uhk-agent/src/services/zephyr-log.service.ts @@ -1,6 +1,13 @@ import { ipcMain } from 'electron'; import pLimit from 'p-limit'; -import { CommandLineArgs, IpcEvents, LogService, UhkDeviceProduct, ZephyrLogEntry } from 'uhk-common' +import { + CommandLineArgs, + escapeZephyrControlChars, + IpcEvents, + LogService, + UhkDeviceProduct, + ZephyrLogEntry, +} from 'uhk-common' import { UsbVariables } from 'uhk-usb'; import { getCurrentUhkDongleHID, getCurrenUhk80LeftHID, snooze, UhkHidDevice, UhkOperations, } from 'uhk-usb' @@ -106,6 +113,10 @@ export class ZephyrLogService { return; } await operations.execShellCommand(command); + this.options.logService.misc(`[ZephyrLogService | ${this.options.uhkDeviceProduct.logName}] execute shell command success`); + // give some time for the command to complete + await snooze(5); + await this.readZephyrLog(operations); } finally { await this.resumeLogging(); @@ -192,6 +203,28 @@ export class ZephyrLogService { this.options.logService.misc(`[ZephyrLogService | ${this.options.uhkDeviceProduct.logName}] paused logging`); } + private async readZephyrLog(operations: UhkOperations): Promise { + try { + const log = (await operations.getVariable(UsbVariables.ShellBuffer)) as string; + this.options.logService.misc(`[ZephyrLogService | ${this.options.uhkDeviceProduct.logName}] Zephyr log (escaped): ${escapeZephyrControlChars(log)}`); + const logEntry: ZephyrLogEntry = { + log: log as string, + level: 'info', + device: this.options.uhkDeviceProduct.logName, + } + this.options.win.webContents.send(IpcEvents.device.zephyrLog, logEntry) + } + catch (error) { + this.options.logService.error(`[ZephyrLogService | ${this.options.uhkDeviceProduct.logName}] can't read zephyr log`, error); + const logEntry: ZephyrLogEntry = { + log: error.message as string, + level: 'error', + device: this.options.uhkDeviceProduct.logName, + } + this.options.win.webContents.send(IpcEvents.device.zephyrLog, logEntry) + } + } + private async resumeLogging(): Promise { if (!this.isPaused) { return; @@ -219,18 +252,11 @@ export class ZephyrLogService { const deviceState = await this.uhkHidDevice.getDeviceState(); if (deviceState.isZephyrLogAvailable) { - const log = await operations.getVariable(UsbVariables.ShellBuffer) - this.options.logService.misc(`[ZephyrLogService | ${this.options.uhkDeviceProduct.logName}] Zephyr log: ${log}`); - const logEntry: ZephyrLogEntry = { - log: log as string, - level: 'info', - device: this.options.uhkDeviceProduct.logName, - } - this.options.win.webContents.send(IpcEvents.device.zephyrLog, logEntry) + await this.readZephyrLog(operations); } } catch (error) { - this.options.logService.error(`[ZephyrLogService | ${this.options.uhkDeviceProduct.logName}] Can't read log`, error); + this.options.logService.error(`[ZephyrLogService | ${this.options.uhkDeviceProduct.logName}] Can't poll log`, error); const logEntry: ZephyrLogEntry = { log: error.message, level: 'error', diff --git a/packages/uhk-common/src/log/escape-zephyr-control-chars.ts b/packages/uhk-common/src/log/escape-zephyr-control-chars.ts new file mode 100644 index 00000000000..0d38ad5f19b --- /dev/null +++ b/packages/uhk-common/src/log/escape-zephyr-control-chars.ts @@ -0,0 +1,24 @@ +/** + * Render a chunk so every control/whitespace byte is visible in the log. Printable chars pass + * through; common controls get named escapes (\r \n \t \e), everything else control-range + * becomes \xNN, and the chunk is wrapped in ⟦…⟧ so leading/trailing spaces are obvious. + */ +export function escapeZephyrControlChars(input: string): string { + let out = ''; + for (const ch of input) { + const code = ch.charCodeAt(0); + switch (ch) { + case '\x1b': out += '\\e'; break; + case '\r': out += '\\r'; break; + case '\n': out += '\\n'; break; + case '\t': out += '\\t'; break; + case '\\': out += '\\\\'; break; + default: + out += code < 0x20 || code === 0x7f + ? '\\x' + code.toString(16).padStart(2, '0') + : ch; + } + } + + return out; +} diff --git a/packages/uhk-common/src/log/index.ts b/packages/uhk-common/src/log/index.ts index fe987c84a65..51bb719bf55 100644 --- a/packages/uhk-common/src/log/index.ts +++ b/packages/uhk-common/src/log/index.ts @@ -1,4 +1,5 @@ export * from './default-log-options.js'; +export * from './escape-zephyr-control-chars.js'; export * from './get-log-options.js'; export * from './log-reg-exps.js'; export * from './log-user-config-helper.js'; diff --git a/packages/uhk-web/src/app/components/device/advanced-settings/advanced-settings.page.component.html b/packages/uhk-web/src/app/components/device/advanced-settings/advanced-settings.page.component.html index 3fd6cdab049..4dadf0ea812 100644 --- a/packages/uhk-web/src/app/components/device/advanced-settings/advanced-settings.page.component.html +++ b/packages/uhk-web/src/app/components/device/advanced-settings/advanced-settings.page.component.html @@ -102,41 +102,22 @@

- - -
- -
-
-
- - - + + + + - -
+ + + -
- - -
-
+ + + + +
+ diff --git a/packages/uhk-web/src/app/components/device/advanced-settings/advanced-settings.page.component.ts b/packages/uhk-web/src/app/components/device/advanced-settings/advanced-settings.page.component.ts index 4fa457f2e80..334696be7bd 100644 --- a/packages/uhk-web/src/app/components/device/advanced-settings/advanced-settings.page.component.ts +++ b/packages/uhk-web/src/app/components/device/advanced-settings/advanced-settings.page.component.ts @@ -10,7 +10,15 @@ import { import { faCog } from '@fortawesome/free-solid-svg-icons'; import { Store } from '@ngrx/store'; import { Observable, Subscription } from 'rxjs'; -import { KeyboardLayout, UHK_60_DEVICE, UHK_60_V2_DEVICE, UHK_80_DEVICE } from 'uhk-common'; +import { + KeyboardLayout, + UHK_60_DEVICE, + UHK_60_V2_DEVICE, + UHK_80_DEVICE, + UHK_80_DEVICE_LEFT, + UHK_DONGLE, + UhkDeviceProduct, +} from 'uhk-common'; import { advanceSettingsState, AppState, @@ -32,9 +40,6 @@ import { } from '../../../store/actions/advance-settings.action'; import { ChangeKeyboardLayoutAction, - ExecShellCommandOnDongleAction, - ExecShellCommandOnLeftHalfAction, - ExecShellCommandOnRightHalfAction, } from '../../../store/actions/device'; import { ActiveButton, initialState, State } from '../../../store/reducers/advanced-settings.reducer'; @@ -53,20 +58,20 @@ export class AdvancedSettingsPageComponent implements OnInit, OnDestroy { @ViewChild('audioPlayer', {static: true,}) audioPlayer: ElementRef; + connectedDevice: UhkDeviceProduct; isKeyboardLayoutChanging$: Observable; isHalvesPairingAllowed: boolean; isZephyrLoggingAllowed: boolean; keyboardLayout: KeyboardLayout; keyboardLayoutEnum = KeyboardLayout; - shellCommand = '' - execShellCommandOnDongle = false; - execShellCommandOnLeftHalf = false; - execShellCommandOnRightHalf = true; isDongleConnected: boolean; showI2CRecoverButton: boolean; isLeftHalfConnected: boolean; state: State; + UHK_DONGLE = UHK_DONGLE; + UHK_80_DEVICE_LEFT = UHK_80_DEVICE_LEFT; + private i2cErrorsLength = 0; private stateSubscription: Subscription; private connectedDeviceSubscription: Subscription; @@ -95,6 +100,7 @@ export class AdvancedSettingsPageComponent implements OnInit, OnDestroy { ngOnInit(): void { this.connectedDeviceSubscription = this.store.select(getConnectedDevice) .subscribe(connectedDevice => { + this.connectedDevice = connectedDevice; this.isHalvesPairingAllowed = connectedDevice?.id === UHK_80_DEVICE.id; this.isZephyrLoggingAllowed = !!connectedDevice; this.showI2CRecoverButton = connectedDevice?.id === UHK_60_DEVICE.id || connectedDevice?.id === UHK_60_V2_DEVICE.id; @@ -140,20 +146,6 @@ export class AdvancedSettingsPageComponent implements OnInit, OnDestroy { this.store.dispatch(new ChangeKeyboardLayoutAction(layout)); } - onExecShellCommand(): void { - if (this.isDongleConnected && this.execShellCommandOnDongle) { - this.store.dispatch(new ExecShellCommandOnDongleAction(this.shellCommand)); - } - - if (this.isLeftHalfConnected && this.execShellCommandOnLeftHalf) { - this.store.dispatch(new ExecShellCommandOnLeftHalfAction(this.shellCommand)); - } - - if (this.execShellCommandOnRightHalf) { - this.store.dispatch(new ExecShellCommandOnRightHalfAction(this.shellCommand)); - } - } - onToggleI2cDebug(): void { this.store.dispatch(new ToggleI2cDebuggingAction()); } diff --git a/packages/uhk-web/src/app/components/zephyr-terminal/zephyr-terminal.component.html b/packages/uhk-web/src/app/components/zephyr-terminal/zephyr-terminal.component.html index c74d5a40087..a6f58025102 100644 --- a/packages/uhk-web/src/app/components/zephyr-terminal/zephyr-terminal.component.html +++ b/packages/uhk-web/src/app/components/zephyr-terminal/zephyr-terminal.component.html @@ -1 +1,8 @@ -
+
+
+
+
+
diff --git a/packages/uhk-web/src/app/components/zephyr-terminal/zephyr-terminal.component.scss b/packages/uhk-web/src/app/components/zephyr-terminal/zephyr-terminal.component.scss index 4cfd8c1958a..3372107cd6c 100644 --- a/packages/uhk-web/src/app/components/zephyr-terminal/zephyr-terminal.component.scss +++ b/packages/uhk-web/src/app/components/zephyr-terminal/zephyr-terminal.component.scss @@ -6,10 +6,26 @@ height: 100%; } -.zephyr-terminal { +.zephyr-term-container { + display: flex; flex: 1; - min-height: 0; + flex-direction: column; + align-items: stretch; + position: relative; +} + +.zephyr-term-wrapper { background-color: var(--color-xterm-bg); border: 1px solid var(--color-xterm-border); - padding: 4px; + overflow: auto; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +.zephyr-terminal { + width: 100%; + height: 100%; } diff --git a/packages/uhk-web/src/app/components/zephyr-terminal/zephyr-terminal.component.ts b/packages/uhk-web/src/app/components/zephyr-terminal/zephyr-terminal.component.ts index 0e945c0a854..98f4692e8ae 100644 --- a/packages/uhk-web/src/app/components/zephyr-terminal/zephyr-terminal.component.ts +++ b/packages/uhk-web/src/app/components/zephyr-terminal/zephyr-terminal.component.ts @@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, Component, ElementRef, + Input, OnDestroy, ViewChild, } from '@angular/core'; @@ -11,15 +12,20 @@ import { Store } from '@ngrx/store'; import { FitAddon } from '@xterm/addon-fit'; import { Terminal } from '@xterm/xterm'; import { Subscription } from 'rxjs'; -import { UHK_80_DEVICE } from 'uhk-common'; +import { + UhkDeviceProduct, + UHK_DEVICE_IDS, +} from 'uhk-common'; import { AppState } from '../../store'; import { ActionTypes as AdvancedSettingsActionTypes, ZephyrLogAction } from '../../store/actions/advance-settings.action'; -import { ExecShellCommandOnRightHalfAction } from '../../store/actions/device'; +import { + ExecShellCommandOnDongleAction, + ExecShellCommandOnLeftHalfAction, + ExecShellCommandOnRightHalfAction, +} from '../../store/actions/device'; /** - * Proof-of-concept VT100 terminal for the UHK 80 right half. - * * Output: the raw shell buffer chunks emitted by the right-half poller (device === UHK 80 right) * are written verbatim to xterm.js, so colors and cursor control render. * @@ -37,12 +43,12 @@ import { ExecShellCommandOnRightHalfAction } from '../../store/actions/device'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class ZephyrTerminalComponent implements AfterViewInit, OnDestroy { + @Input() uhkDevice: UhkDeviceProduct; @ViewChild('terminal', { static: true }) terminalElement: ElementRef; private terminal: Terminal; private fitAddon: FitAddon; private logSubscription: Subscription; - private resizeObserver: ResizeObserver; constructor( private store: Store, @@ -59,29 +65,47 @@ export class ZephyrTerminalComponent implements AfterViewInit, OnDestroy { this.fitAddon = new FitAddon(); this.terminal.loadAddon(this.fitAddon); this.terminal.open(this.terminalElement.nativeElement); - this.fitAddon.fit(); + setTimeout(() => { + this.fitAddon.fit(); + }, 1); // Forward every keystroke (raw bytes, incl. ESC sequences) to the right half. this.terminal.onData((data: string) => { - this.store.dispatch(new ExecShellCommandOnRightHalfAction(data)); + switch (this.uhkDevice?.id) { + case UHK_DEVICE_IDS.UHK_DONGLE: { + this.store.dispatch(new ExecShellCommandOnDongleAction(data)); + break; + } + + case UHK_DEVICE_IDS.UHK80_LEFT: { + this.store.dispatch(new ExecShellCommandOnLeftHalfAction(data)); + break; + } + + case UHK_DEVICE_IDS.UHK80_RIGHT: { + this.store.dispatch(new ExecShellCommandOnRightHalfAction(data)); + break; + } + } }); // Write raw shell output coming from the right half straight into the terminal. this.logSubscription = this.actions$ .pipe(ofType(AdvancedSettingsActionTypes.zephyrLog)) .subscribe((action: ZephyrLogAction) => { - if (action.payload.device === UHK_80_DEVICE.logName) { + if (action.payload.device === this.uhkDevice?.logName) { this.terminal.write(action.payload.log); } }); - this.resizeObserver = new ResizeObserver(() => this.fitAddon.fit()); - this.resizeObserver.observe(this.terminalElement.nativeElement); } ngOnDestroy(): void { this.logSubscription?.unsubscribe(); - this.resizeObserver?.disconnect(); this.terminal?.dispose(); } + + onResized() { + this.fitAddon?.fit() + } } From b97727c720a730609ebffb04f92bbb418fc4f25e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kiss=20R=C3=B3bert?= Date: Sat, 6 Jun 2026 15:40:41 +0200 Subject: [PATCH 6/9] fix: use new angular control flow --- .../advanced-settings.page.component.html | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/packages/uhk-web/src/app/components/device/advanced-settings/advanced-settings.page.component.html b/packages/uhk-web/src/app/components/device/advanced-settings/advanced-settings.page.component.html index 4dadf0ea812..c869acf8c6f 100644 --- a/packages/uhk-web/src/app/components/device/advanced-settings/advanced-settings.page.component.html +++ b/packages/uhk-web/src/app/components/device/advanced-settings/advanced-settings.page.component.html @@ -102,20 +102,31 @@

- - + @if (state.activeButton === ActiveButton.ShowZephyrLogs) { + + @if (state.isLeftHalfZephyrLoggingEnabled) { + + } - - + @if (state.isRightHalfZephyrLoggingEnabled) { + + + } - + @if (state.isDongleZephyrLoggingEnabled) { + + } - + } + @else + { + + }
From fdeeb80f9f534c9add7214ddd670acea9eb03d32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kiss=20R=C3=B3bert?= Date: Sat, 6 Jun 2026 16:27:26 +0200 Subject: [PATCH 7/9] fix: keep zephyr history and clear --- .../advanced-settings.page.component.html | 12 +++- .../zephyr-terminal.component.ts | 64 +++++++++++++------ .../reducers/advanced-settings.reducer.ts | 21 +++--- 3 files changed, 67 insertions(+), 30 deletions(-) diff --git a/packages/uhk-web/src/app/components/device/advanced-settings/advanced-settings.page.component.html b/packages/uhk-web/src/app/components/device/advanced-settings/advanced-settings.page.component.html index c869acf8c6f..e4b0192d36b 100644 --- a/packages/uhk-web/src/app/components/device/advanced-settings/advanced-settings.page.component.html +++ b/packages/uhk-web/src/app/components/device/advanced-settings/advanced-settings.page.component.html @@ -106,19 +106,25 @@

@if (state.isLeftHalfZephyrLoggingEnabled) { - + } @if (state.isRightHalfZephyrLoggingEnabled) { - + } @if (state.isDongleZephyrLoggingEnabled) { - + } diff --git a/packages/uhk-web/src/app/components/zephyr-terminal/zephyr-terminal.component.ts b/packages/uhk-web/src/app/components/zephyr-terminal/zephyr-terminal.component.ts index 98f4692e8ae..e6dbb4a12c5 100644 --- a/packages/uhk-web/src/app/components/zephyr-terminal/zephyr-terminal.component.ts +++ b/packages/uhk-web/src/app/components/zephyr-terminal/zephyr-terminal.component.ts @@ -4,7 +4,9 @@ import { Component, ElementRef, Input, + OnChanges, OnDestroy, + SimpleChanges, ViewChild, } from '@angular/core'; import { Actions, ofType } from '@ngrx/effects'; @@ -15,6 +17,7 @@ import { Subscription } from 'rxjs'; import { UhkDeviceProduct, UHK_DEVICE_IDS, + ZephyrLogEntry, } from 'uhk-common'; import { AppState } from '../../store'; @@ -42,10 +45,13 @@ import { styleUrls: ['./zephyr-terminal.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class ZephyrTerminalComponent implements AfterViewInit, OnDestroy { +export class ZephyrTerminalComponent implements AfterViewInit, OnChanges, OnDestroy { @Input() uhkDevice: UhkDeviceProduct; + @Input() zephyrLogs: ZephyrLogEntry[] = []; + @ViewChild('terminal', { static: true }) terminalElement: ElementRef; + private isLogRestored = false; private terminal: Terminal; private fitAddon: FitAddon; private logSubscription: Subscription; @@ -69,26 +75,13 @@ export class ZephyrTerminalComponent implements AfterViewInit, OnDestroy { this.fitAddon.fit(); }, 1); - // Forward every keystroke (raw bytes, incl. ESC sequences) to the right half. + // Forward every keystroke (raw bytes, incl. ESC sequences). this.terminal.onData((data: string) => { - switch (this.uhkDevice?.id) { - case UHK_DEVICE_IDS.UHK_DONGLE: { - this.store.dispatch(new ExecShellCommandOnDongleAction(data)); - break; - } - - case UHK_DEVICE_IDS.UHK80_LEFT: { - this.store.dispatch(new ExecShellCommandOnLeftHalfAction(data)); - break; - } - - case UHK_DEVICE_IDS.UHK80_RIGHT: { - this.store.dispatch(new ExecShellCommandOnRightHalfAction(data)); - break; - } - } + this.dispatchTerminalInput(data); }); + this.restoreLogHistory(); + // Write raw shell output coming from the right half straight into the terminal. this.logSubscription = this.actions$ .pipe(ofType(AdvancedSettingsActionTypes.zephyrLog)) @@ -97,7 +90,12 @@ export class ZephyrTerminalComponent implements AfterViewInit, OnDestroy { this.terminal.write(action.payload.log); } }); + } + ngOnChanges(changes: SimpleChanges) { + if(changes.zephyrLogs?.firstChange) { + this.restoreLogHistory(); + } } ngOnDestroy(): void { @@ -108,4 +106,34 @@ export class ZephyrTerminalComponent implements AfterViewInit, OnDestroy { onResized() { this.fitAddon?.fit() } + + private dispatchTerminalInput(data: string): void { + switch (this.uhkDevice?.id) { + case UHK_DEVICE_IDS.UHK_DONGLE: { + this.store.dispatch(new ExecShellCommandOnDongleAction(data)); + break; + } + + case UHK_DEVICE_IDS.UHK80_LEFT: { + this.store.dispatch(new ExecShellCommandOnLeftHalfAction(data)); + break; + } + + case UHK_DEVICE_IDS.UHK80_RIGHT: { + this.store.dispatch(new ExecShellCommandOnRightHalfAction(data)); + break; + } + } + } + + private restoreLogHistory(): void { + if (this.terminal && this.uhkDevice && !this.isLogRestored) { + this.isLogRestored = true; + for (const log of this.zephyrLogs) { + if (log.device === this.uhkDevice.logName) { + this.terminal.write(log.log); + } + } + } + } } diff --git a/packages/uhk-web/src/app/store/reducers/advanced-settings.reducer.ts b/packages/uhk-web/src/app/store/reducers/advanced-settings.reducer.ts index 4b597e9fba5..163959c4da6 100644 --- a/packages/uhk-web/src/app/store/reducers/advanced-settings.reducer.ts +++ b/packages/uhk-web/src/app/store/reducers/advanced-settings.reducer.ts @@ -1,4 +1,4 @@ -import { getFormattedTimestamp, UhkDeviceProduct } from 'uhk-common'; +import { getFormattedTimestamp, UhkDeviceProduct, ZephyrLogEntry } from 'uhk-common'; import { XtermCssClass, XtermLog } from '../../models/xterm-log'; import { appendXtermLogs } from '../../util/merge-xterm-logs'; @@ -31,6 +31,7 @@ export interface State { isRightHalfZephyrLoggingEnabled: boolean; lastConnectedDevice?: UhkDeviceProduct; menuVisible: boolean; + zephyrLogs: ZephyrLogEntry[]; } export const initialState = (): State => ({ @@ -42,6 +43,7 @@ export const initialState = (): State => ({ isRightHalfZephyrLoggingEnabled: false, isLeftHalfPairing: false, menuVisible: false, + zephyrLogs: [], }); export function reducer(state = initialState(), action: Actions | App.Actions | Device.Actions) { @@ -199,7 +201,6 @@ export function reducer(state = initialState(), action: Actions | App.Actions | case ActionTypes.toggleZephyrLogging: { return { ...state, - i2cLogs: [], activeButton: state.activeButton === ActiveButton.ShowZephyrLogs ? ActiveButton.None : ActiveButton.ShowZephyrLogs, @@ -219,13 +220,15 @@ export function reducer(state = initialState(), action: Actions | App.Actions | const payload = (action as ZephyrLogAction).payload; const newState = {...state}; - newState.i2cLogs = [ - ...state.i2cLogs, - { - message: `${getFormattedTimestamp()} | ${payload.device.padEnd(15 )} | ${payload.log}`, - cssClass: payload.level === 'error' ? XtermCssClass.error : XtermCssClass.standard, - } - ]; + // the clear command sent so have to clear the history too + if (payload.log.startsWith('\\r\\n\\033[H\\033[2J\u001b')) { + newState.zephyrLogs.filter(log => log.device !== payload.device); + } + else { + newState.zephyrLogs = [...state.zephyrLogs]; + } + + newState.zephyrLogs.push(payload); return newState; } From a8a2f2f6c5e83498521b9fe8d61d9c3722b76dea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kiss=20R=C3=B3bert?= Date: Sat, 6 Jun 2026 16:33:51 +0200 Subject: [PATCH 8/9] fix: init the prompt --- .../zephyr-terminal/zephyr-terminal.component.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/uhk-web/src/app/components/zephyr-terminal/zephyr-terminal.component.ts b/packages/uhk-web/src/app/components/zephyr-terminal/zephyr-terminal.component.ts index e6dbb4a12c5..229ed054ca7 100644 --- a/packages/uhk-web/src/app/components/zephyr-terminal/zephyr-terminal.component.ts +++ b/packages/uhk-web/src/app/components/zephyr-terminal/zephyr-terminal.component.ts @@ -129,11 +129,21 @@ export class ZephyrTerminalComponent implements AfterViewInit, OnChanges, OnDest private restoreLogHistory(): void { if (this.terminal && this.uhkDevice && !this.isLogRestored) { this.isLogRestored = true; + + let foundHistory = false; + for (const log of this.zephyrLogs) { if (log.device === this.uhkDevice.logName) { this.terminal.write(log.log); + foundHistory = true; } } + + // initialize the terminal if no history found + // it will write the correct prompt + if (!foundHistory) { + this.dispatchTerminalInput('clear\n') + } } } } From 65e485a8d3606f28674a1821a4f350a7e6edca8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kiss=20R=C3=B3bert?= Date: Mon, 8 Jun 2026 16:01:03 +0200 Subject: [PATCH 9/9] fix: don't clear when initing prompt --- .../components/zephyr-terminal/zephyr-terminal.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/uhk-web/src/app/components/zephyr-terminal/zephyr-terminal.component.ts b/packages/uhk-web/src/app/components/zephyr-terminal/zephyr-terminal.component.ts index 229ed054ca7..af0e70d6b4e 100644 --- a/packages/uhk-web/src/app/components/zephyr-terminal/zephyr-terminal.component.ts +++ b/packages/uhk-web/src/app/components/zephyr-terminal/zephyr-terminal.component.ts @@ -139,10 +139,10 @@ export class ZephyrTerminalComponent implements AfterViewInit, OnChanges, OnDest } } - // initialize the terminal if no history found + // initialize the terminal if no history found, // it will write the correct prompt if (!foundHistory) { - this.dispatchTerminalInput('clear\n') + this.dispatchTerminalInput('\n') } } }