From 02ace9a09c6ac51e367c794daf748a06e6e0949f Mon Sep 17 00:00:00 2001 From: pawel120 Date: Sat, 3 Oct 2026 13:19:35 +0200 Subject: [PATCH 1/7] UI --- .gitignore | 7 +- README.md | 148 +- index.html | 14 +- package-lock.json | 28 +- package.json | 4 +- public/neuorrun/README.txt | 58 +- src-tauri/capabilities/default.json | 4 +- src-tauri/gen/schemas/acl-manifests.json | 2010 ++++++++++++++- src-tauri/gen/schemas/capabilities.json | 25 +- src-tauri/gen/schemas/desktop-schema.json | 78 +- src-tauri/gen/schemas/windows-schema.json | 78 +- src-tauri/icons/icon.ico | Bin 4190 -> 37390 bytes src-tauri/icons/icon.png | Bin 962 -> 66679 bytes src-tauri/tauri.conf.json | 17 +- src/App.tsx | 29 +- src/components/DevicePanel.tsx | 157 +- src/components/Sidebar.tsx | 58 +- src/components/TitleBar.tsx | 23 +- src/config/appConfig.ts | 6 + src/core/brainMetrics/BrainMetricsService.ts | 22 +- src/core/brainMetrics/index.ts | 6 +- src/core/brainMetrics/models.ts | 4 +- .../brainaccess/BrainAccessEEGAdapter.test.ts | 63 +- .../brainaccess/BrainAccessEEGAdapter.ts | 66 +- .../adapters/brainlink/BrainLinkAdapter.ts | 60 +- .../adapters/brainlink/BrainLinkBridge.ts | 74 +- src/core/devices/adapters/brainlink/index.ts | 4 +- .../brainlink/tests/BrainLinkAdapter.test.ts | 14 +- .../contracts/BrainMetricsDeviceAdapter.ts | 14 +- src/core/devices/registerBuiltInDevices.ts | 36 +- src/core/eeg/EEGStreamService.test.ts | 6 +- src/core/eeg/EEGStreamService.ts | 350 +-- src/core/ssvep/SsvepClassifierBridge.ts | 314 +-- src/core/ssvep/SsvepClassifierService.ts | 83 +- src/core/ssvep/config.ts | 80 +- src/core/ssvep/index.ts | 11 +- src/core/ssvep/models.ts | 6 +- src/features/dashboard/Dashboard.tsx | 41 +- src/features/device/DeviceCard.tsx | 147 +- src/features/device/DevicePage.tsx | 87 +- src/features/modules/ModuleCard.tsx | 78 +- src/features/modules/ModuleErrorBoundary.tsx | 8 +- src/features/modules/ModuleHost.tsx | 12 +- .../modules/registerBuiltInModules.ts | 38 +- src/features/settings/SettingsPage.tsx | 20 +- src/main.tsx | 7 +- src/modules/cortex/CortexModule.tsx | 97 +- .../cortex/components/CortexViewport.tsx | 6 + src/modules/cortex/cortexModuleDefinition.ts | 28 +- .../cortex/eeg/CortexSignalProcessor.ts | 328 ++- src/modules/cortex/hooks/useCortexEEG.ts | 127 +- .../cortex/rendering/CortexRenderer.ts | 70 +- src/modules/cortex/styles/cortex.css | 517 ++-- src/modules/miner/MinerModule.tsx | 1290 +++------- src/modules/miner/components/DiamondIcon.tsx | 21 +- src/modules/miner/components/MinerIcon.tsx | 54 +- src/modules/miner/components/SsvepOverlay.tsx | 339 +-- src/modules/miner/eeg/TrialSampleCollector.ts | 79 +- src/modules/miner/game/minerGame.ts | 227 +- src/modules/miner/hooks/useMinerEEG.ts | 275 +- src/modules/miner/minerManifest.ts | 22 +- src/modules/miner/minerModuleDefinition.ts | 36 +- src/modules/miner/models/miner.ts | 36 +- src/modules/miner/styles/miner.css | 2110 +++------------- .../miner/tests/TrialSampleCollector.test.ts | 99 +- src/modules/miner/tests/minerGame.test.ts | 184 +- src/modules/miner/tests/ssvepConfig.test.ts | 102 +- src/modules/neuorrun/NeuorrunModule.tsx | 135 +- src/modules/neuorrun/hooks/useBrainMetrics.ts | 15 +- src/modules/neuorrun/neuorrunManifest.ts | 11 +- .../neuorrun/neuorrunModuleDefinition.ts | 29 +- src/modules/neuorrun/styles/neuorrun.css | 204 +- src/styles/app.css | 2245 ++++++----------- src/types/module.ts | 7 + vite.config.ts | 8 +- vitest.config.ts | 25 +- 76 files changed, 5747 insertions(+), 7374 deletions(-) diff --git a/.gitignore b/.gitignore index 123107e..afb2976 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,10 @@ dist/ src-tauri/target/ +# Generated Tauri sidecar binaries +src-tauri/binaries/* +!src-tauri/binaries/.gitkeep + # --------------------------------------------------------- # Python @@ -80,6 +84,3 @@ desktop.ini # --------------------------------------------------------- .cache/ - -# Tauri sidecar binaries — generated locally -src-tauri/binaries/*.exe \ No newline at end of file diff --git a/README.md b/README.md index 3190aaf..3cad7fa 100644 --- a/README.md +++ b/README.md @@ -315,7 +315,7 @@ No FBCCA is used in Neuorrun. # Repository structure ```text -KNeuronInterFace/ +KNeuron---Interface/ │ ├── brainaccess-sidecar/ ├── brainlink-sidecar/ @@ -362,7 +362,7 @@ KNeuronInterFace/ # Quick start on a new machine -KNeuron includes bootstrap scripts for both Windows and Linux. +KNeuron is primarily supported and validated on Windows. An experimental Linux bootstrap is also included for development and platform testing. The goal is that after cloning the repository, the development environment and Python sidecars can be recreated locally instead of storing generated dependencies and build artifacts in Git. @@ -372,7 +372,7 @@ The goal is that after cloning the repository, the development environment and P ```powershell git clone -cd KNeuronInterFace +cd KNeuron---Interface ``` ### 2. Run the bootstrap @@ -427,15 +427,17 @@ The first build can take significantly longer because Node packages, Rust depend --- -# Linux quick start +# Linux experimental setup -The included Linux bootstrap currently targets **Ubuntu/Debian-family distributions**. +Linux support is currently experimental and has not been validated end-to-end with the full KNeuron hardware stack. + +The included Linux bootstrap targets **Ubuntu/Debian-family distributions** and is intended primarily for development and platform testing. ### 1. Clone the repository ```bash git clone -cd KNeuronInterFace +cd KNeuron---Interface ``` ### 2. Make the bootstrap executable @@ -462,7 +464,9 @@ Python virtual environments all three Python sidecars ``` -It also checks device permissions used by serial/Bluetooth devices. +The script attempts to prepare all three Python sidecars and checks device permissions used by serial/Bluetooth devices. + +Successful setup does not guarantee that every hardware integration is supported by the underlying vendor SDK on Linux. ### Prepare without launching @@ -486,6 +490,8 @@ If the script adds the current user to: dialout ``` +BrainAccess support on Linux is not currently considered production-validated. The bootstrap can build the BrainAccess sidecar only if the BrainAccess SDK/dependency used by `brainaccess-sidecar/requirements.txt` is available and compatible with the target Linux environment. Real-device connectivity must be verified separately. + log out and log back in before using serial/RFCOMM devices. The BrainAccess Python SDK is the component most likely to require platform-specific verification. The bootstrap can rebuild the sidecar only if the BrainAccess SDK/dependency used by `brainaccess-sidecar/requirements.txt` is available for the target Linux environment. @@ -682,6 +688,7 @@ generated executables They are build artifacts, not application source. ### `src-tauri/binaries/* + !src-tauri/binaries/.gitkeep` The platform-specific sidecar executables can be rebuilt locally by the bootstrap/build scripts: @@ -1096,7 +1103,7 @@ Windows NSIS output is typically created under: src-tauri/target/release/bundle/nsis/ ``` -Linux package output is created by Tauri under the corresponding bundle directories for the configured Linux targets. +Linux packaging is currently experimental and should not be treated as a production-supported release path until it has been validated on the target distribution. --- @@ -1473,9 +1480,7 @@ The existing contract contains the project-level module identity and capabilitie A typical manifest follows this pattern: ```ts -import type { - KNeuronModuleManifest, -} from "../../types/module"; +import type { KNeuronModuleManifest } from "../../types/module"; export const neurofeedbackManifest: KNeuronModuleManifest = { schemaVersion: 1, @@ -1486,8 +1491,7 @@ export const neurofeedbackManifest: KNeuronModuleManifest = { version: "1.0.0", - description: - "Example EEG neurofeedback module.", + description: "Example EEG neurofeedback module.", category: "VISUALIZATION", @@ -1520,21 +1524,14 @@ The current module component contract uses `KNeuronModuleProps`. Example: ```tsx -import type { - KNeuronModuleProps, -} from "../../features/modules/moduleDefinition"; +import type { KNeuronModuleProps } from "../../features/modules/moduleDefinition"; -export function NeurofeedbackModule({ - onRequestClose, -}: KNeuronModuleProps) { +export function NeurofeedbackModule({ onRequestClose }: KNeuronModuleProps) { return (

Neurofeedback

-
@@ -1551,20 +1548,13 @@ The production module registry operates on complete definitions. Example: ```ts -import { - NeurofeedbackModule, -} from "./NeurofeedbackModule"; +import { NeurofeedbackModule } from "./NeurofeedbackModule"; -import { - neurofeedbackManifest, -} from "./neurofeedbackManifest"; +import { neurofeedbackManifest } from "./neurofeedbackManifest"; -import type { - KNeuronModuleDefinition, -} from "../../features/modules/moduleDefinition"; +import type { KNeuronModuleDefinition } from "../../features/modules/moduleDefinition"; -export const neurofeedbackModuleDefinition: - KNeuronModuleDefinition = { +export const neurofeedbackModuleDefinition: KNeuronModuleDefinition = { manifest: neurofeedbackManifest, component: NeurofeedbackModule, }; @@ -1587,9 +1577,7 @@ src/features/modules/registerBuiltInModules.ts Import the definition there: ```ts -import { - neurofeedbackModuleDefinition, -} from "../../modules/neurofeedback/neurofeedbackModuleDefinition"; +import { neurofeedbackModuleDefinition } from "../../modules/neurofeedback/neurofeedbackModuleDefinition"; ``` and include it in the returned/registered definitions using the same pattern as the existing Cortex, Miner and Neuorrun definitions. @@ -1653,9 +1641,7 @@ Declare normalized EEG labels, not physical device indexes. Allowed: ```ts -import { - eegStreamService, -} from "../../core/eeg"; +import { eegStreamService } from "../../core/eeg"; ``` Not allowed: @@ -1696,10 +1682,7 @@ const handle = await eegStreamService.acquire({ Read a buffered historical window: ```ts -const window = eegStreamService.getLatestWindow( - 2.0, - ["O1", "Oz", "O2"], -); +const window = eegStreamService.getLatestWindow(2.0, ["O1", "Oz", "O2"]); ``` Do not create another global hardware ring buffer inside the module. @@ -1862,9 +1845,7 @@ src/core/devices/adapters/openbci/ For raw EEG: ```ts -export class OpenBCIAdapter - implements EEGDeviceAdapter -{ +export class OpenBCIAdapter implements EEGDeviceAdapter { // ... } ``` @@ -2165,19 +2146,19 @@ one JSON message per line Example request: ```json -{"id":17,"command":"status"} +{ "id": 17, "command": "status" } ``` Example response: ```json -{"id":17,"ok":true,"result":{"state":"connected"}} +{ "id": 17, "ok": true, "result": { "state": "connected" } } ``` Example asynchronous event: ```json -{"event":"samples","payload":{"sampleRateHz":250,"sampleCount":8}} +{ "event": "samples", "payload": { "sampleRateHz": 250, "sampleCount": 8 } } ``` The exact commands belong to the sidecar/TypeScript bridge protocol. @@ -2450,30 +2431,32 @@ git grep -n "brainaccess-bridge" src-tauri Resource ownership must always be explicit. -| Resource | Owner | Required cleanup | -| --- | --- | --- | -| Physical device connection | Adapter / DeviceManager | disconnect | -| Physical raw EEG stream | EEGStreamService + adapter | stop after final consumer | -| EEG consumer | Module/hook calling `acquire()` | `handle.release()` | -| Brain-metrics subscription | Module/hook | unsubscribe | -| Python sidecar process | TypeScript bridge/adapter | terminate | -| Classifier request | classifier client/service | resolve/reject/cancel | -| DOM listener | component/hook | remove listener | -| Timer | creator | clear timer | -| `requestAnimationFrame` | renderer/module | cancel frame | -| Three.js resource | renderer/module/cache | dispose when not shared | -| Unity bridge listener | Neuorrun integration | remove hook | +| Resource | Owner | Required cleanup | +| -------------------------- | ------------------------------- | --------------------------------------------- | +| Physical device connection | Adapter / DeviceManager | disconnect | +| Physical raw EEG stream | EEGStreamService + adapter | stop on device disconnect or service disposal | +| EEG consumer | Module/hook calling `acquire()` | `handle.release()` | +| Brain-metrics subscription | Module/hook | unsubscribe | +| Python sidecar process | TypeScript bridge/adapter | terminate | +| Classifier request | classifier client/service | resolve/reject/cancel | +| DOM listener | component/hook | remove listener | +| Timer | creator | clear timer | +| `requestAnimationFrame` | renderer/module | cancel frame | +| Three.js resource | renderer/module/cache | dispose when not shared | +| Unity bridge listener | Neuorrun integration | remove hook | ## Shared EEG lifecycle -The shared service is reference-counted conceptually: +The shared service allows multiple modules to consume one physical EEG stream. ```text Cortex acquire consumer count = 1 +physical stream starts ↓ Miner acquire consumer count = 2 +same physical stream is reused ↓ Cortex release consumer count = 1 @@ -2481,9 +2464,14 @@ physical stream remains active ↓ Miner release consumer count = 0 +physical stream remains active + ↓ +device disconnect / service dispose physical stream stops ``` +A module releasing its handle removes only that consumer. It does not stop the physical EEG acquisition. + Therefore a module must never call: ```text @@ -2492,6 +2480,8 @@ adapter.stopStream() directly. +`EEGStreamService` owns the physical stream lifecycle and serializes adapter start/stop operations. + ## Device switching Do not switch to another raw EEG adapter while consumers of the current stream are still active. @@ -2500,11 +2490,12 @@ Correct order: ```text close/release consuming modules -stop shared stream through consumer lifecycle disconnect old device connect new device ``` +The physical EEG stream is stopped as part of device teardown/disconnect, not by a module's `release()`. + ## Async races A component may unmount while an async connection/acquire request is still resolving. @@ -2843,3 +2834,30 @@ It is not a medical device and is not intended for diagnosis, treatment, or clin ## KNeuron **Modular Brain-Computer Interface Platform** + +## Neuorrun source + +The complete Neuorrun Unity project is maintained in a separate repository: + +https://github.com/KN-Neuron/Neurorun + +The main KNeuron repository contains only: + +- the generated Neuorrun WebGL runtime in `public/neuorrun/`, +- KNeuron-specific integration code and patches in `unity-patch/`. + +Rebuilding Neuorrun requires the source project from the repository above. + +# License + +Unless otherwise noted, original KNeuron source code is licensed +under the Apache License 2.0. + +See: + +- `LICENSE` +- `NOTICE` +- `THIRD_PARTY.md` + +Third-party software, assets, vendor SDKs and runtime components +may be subject to separate license terms. diff --git a/index.html b/index.html index 2f0b845..5c891de 100644 --- a/index.html +++ b/index.html @@ -1 +1,13 @@ -KNeuron
+ + + + + + + KNeuron + + +
+ + + diff --git a/package-lock.json b/package-lock.json index fbf6377..ef4f400 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,8 @@ "version": "0.2.7", "license": "UNLICENSED", "dependencies": { + "@fontsource/barlow": "^5.3.0", + "@fontsource/barlow-semi-condensed": "^5.3.0", "@tauri-apps/api": "^2.8.0", "@tauri-apps/plugin-shell": "^2.4.0", "lucide-react": "^0.468.0", @@ -32,7 +34,7 @@ "eslint-config-prettier": "^10.1.8", "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.7", - "globals": "^17.12.0", + "globals": "^17.13.0", "jsdom": "^29.1.1", "prettier": "^3.9.9", "typescript": "~5.6.3", @@ -1173,6 +1175,24 @@ } } }, + "node_modules/@fontsource/barlow": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource/barlow/-/barlow-5.3.0.tgz", + "integrity": "sha512-tGR0y6QObRtKv71MOth44222+mdNFM/ZWFWDV1VLzNbG4oK9PNRlyJzmkck4vHAi9ibHBXgt4SlY51amFP55CA==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@fontsource/barlow-semi-condensed": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource/barlow-semi-condensed/-/barlow-semi-condensed-5.3.0.tgz", + "integrity": "sha512-EdpQ1GlLMofGqDsQhEw+g6DonNKmzt21p8y/oNbTl4BFzEkqYkFyE2fj25YGxmZBL/LZZGAxe74FlvEWcgc31w==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, "node_modules/@humanfs/core": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", @@ -3314,9 +3334,9 @@ } }, "node_modules/globals": { - "version": "17.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.12.0.tgz", - "integrity": "sha512-cezEd/DTyyht9cvSSURyygXPfy04GtWO/5e6ZPvH7fCtjKz9PYOmuawphw1Ctd1f6C+5JypXfGD7ahNMXvevBA==", + "version": "17.13.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.13.0.tgz", + "integrity": "sha512-RwMTC61u7hrG3ZJMkZQOK4JLJrKS8QtoTii63EmWvFXHqycY9FObBJQCbsLxSO8kjKhWE5kV1GC+Vb1g3RI0Zg==", "dev": true, "license": "MIT", "engines": { diff --git a/package.json b/package.json index b717efe..8fa9c7f 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,8 @@ "quality:full": "npm run quality && cargo check --manifest-path src-tauri/Cargo.toml" }, "dependencies": { + "@fontsource/barlow": "^5.3.0", + "@fontsource/barlow-semi-condensed": "^5.3.0", "@tauri-apps/api": "^2.8.0", "@tauri-apps/plugin-shell": "^2.4.0", "lucide-react": "^0.468.0", @@ -46,7 +48,7 @@ "eslint-config-prettier": "^10.1.8", "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.7", - "globals": "^17.12.0", + "globals": "^17.13.0", "jsdom": "^29.1.1", "prettier": "^3.9.9", "typescript": "~5.6.3", diff --git a/public/neuorrun/README.txt b/public/neuorrun/README.txt index 8b02fae..24180c9 100644 --- a/public/neuorrun/README.txt +++ b/public/neuorrun/README.txt @@ -1,7 +1,57 @@ -This directory is a placeholder. +KNeuron — Neuorrun WebGL Runtime -After building Neuorrun in Unity WebGL, run: +This directory contains the generated Unity WebGL runtime used by the Neuorrun module. -python unity-patch/prepare_webgl_build.py /public/neuorrun +It is NOT the full Neuorrun Unity source project. -The script replaces manifest.json and copies the Build/ files here. +Expected runtime layout: + +public/neuorrun/ + Build/ + *.loader.js + *.framework.js + *.data + *.wasm + manifest.json + README.txt + +KNeuron loads this runtime from the React/Tauri application and injects BrainLink +metrics into Unity through the KNeuron WebGL bridge. + +Runtime data flow: + +BrainLink Lite + -> brainlink-bridge + -> BrainLinkAdapter + -> BrainMetricsService + -> Neuorrun React module + -> Unity SendMessage + -> KNeuronBridge + -> TGCConnectionController + -> original game logic + +The main KNeuron repository keeps the generated WebGL runtime so developers can +run Neuorrun without installing Unity. + +To rebuild or modify Neuorrun itself, use the original Unity source project. +The supported Unity version for the current integration is Unity 2022.3.7f1. + +KNeuron-specific Unity integration patches are stored under: + +unity-patch/ + +After rebuilding the Unity project, install the WebGL output with: + +python ./unity-patch/prepare_webgl_build.py ./public/neuorrun + +After installation, verify that: + +public/neuorrun/manifest.json + +contains: + +{ + "ready": true +} + +Generated files under Build/ should not be edited manually. diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 61dcc00..f14c0ba 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -2,9 +2,7 @@ "$schema": "../gen/schemas/desktop-schema.json", "identifier": "main-capability", "description": "Minimal permissions required by the KNeuron custom title bar and approved sidecars.", - "windows": [ - "main" - ], + "windows": ["main"], "permissions": [ "core:window:allow-close", "core:window:allow-minimize", diff --git a/src-tauri/gen/schemas/acl-manifests.json b/src-tauri/gen/schemas/acl-manifests.json index cb7b5b6..00e6fc6 100644 --- a/src-tauri/gen/schemas/acl-manifests.json +++ b/src-tauri/gen/schemas/acl-manifests.json @@ -1 +1,2009 @@ -{"core":{"default_permission":{"identifier":"default","description":"Default core plugins set.","permissions":["core:path:default","core:event:default","core:window:default","core:webview:default","core:app:default","core:image:default","core:resources:default","core:menu:default","core:tray:default"]},"permissions":{},"permission_sets":{},"global_scope_schema":null},"core:app":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-version","allow-name","allow-tauri-version","allow-identifier","allow-bundle-type","allow-register-listener","allow-remove-listener","allow-supports-multiple-windows"]},"permissions":{"allow-app-hide":{"identifier":"allow-app-hide","description":"Enables the app_hide command without any pre-configured scope.","commands":{"allow":["app_hide"],"deny":[]}},"allow-app-show":{"identifier":"allow-app-show","description":"Enables the app_show command without any pre-configured scope.","commands":{"allow":["app_show"],"deny":[]}},"allow-bundle-type":{"identifier":"allow-bundle-type","description":"Enables the bundle_type command without any pre-configured scope.","commands":{"allow":["bundle_type"],"deny":[]}},"allow-default-window-icon":{"identifier":"allow-default-window-icon","description":"Enables the default_window_icon command without any pre-configured scope.","commands":{"allow":["default_window_icon"],"deny":[]}},"allow-exit":{"identifier":"allow-exit","description":"Enables the exit command without any pre-configured scope.","commands":{"allow":["exit"],"deny":[]}},"allow-fetch-data-store-identifiers":{"identifier":"allow-fetch-data-store-identifiers","description":"Enables the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":["fetch_data_store_identifiers"],"deny":[]}},"allow-identifier":{"identifier":"allow-identifier","description":"Enables the identifier command without any pre-configured scope.","commands":{"allow":["identifier"],"deny":[]}},"allow-name":{"identifier":"allow-name","description":"Enables the name command without any pre-configured scope.","commands":{"allow":["name"],"deny":[]}},"allow-register-listener":{"identifier":"allow-register-listener","description":"Enables the register_listener command without any pre-configured scope.","commands":{"allow":["register_listener"],"deny":[]}},"allow-remove-data-store":{"identifier":"allow-remove-data-store","description":"Enables the remove_data_store command without any pre-configured scope.","commands":{"allow":["remove_data_store"],"deny":[]}},"allow-remove-listener":{"identifier":"allow-remove-listener","description":"Enables the remove_listener command without any pre-configured scope.","commands":{"allow":["remove_listener"],"deny":[]}},"allow-set-app-theme":{"identifier":"allow-set-app-theme","description":"Enables the set_app_theme command without any pre-configured scope.","commands":{"allow":["set_app_theme"],"deny":[]}},"allow-set-dock-visibility":{"identifier":"allow-set-dock-visibility","description":"Enables the set_dock_visibility command without any pre-configured scope.","commands":{"allow":["set_dock_visibility"],"deny":[]}},"allow-supports-multiple-windows":{"identifier":"allow-supports-multiple-windows","description":"Enables the supports_multiple_windows command without any pre-configured scope.","commands":{"allow":["supports_multiple_windows"],"deny":[]}},"allow-tauri-version":{"identifier":"allow-tauri-version","description":"Enables the tauri_version command without any pre-configured scope.","commands":{"allow":["tauri_version"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-app-hide":{"identifier":"deny-app-hide","description":"Denies the app_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["app_hide"]}},"deny-app-show":{"identifier":"deny-app-show","description":"Denies the app_show command without any pre-configured scope.","commands":{"allow":[],"deny":["app_show"]}},"deny-bundle-type":{"identifier":"deny-bundle-type","description":"Denies the bundle_type command without any pre-configured scope.","commands":{"allow":[],"deny":["bundle_type"]}},"deny-default-window-icon":{"identifier":"deny-default-window-icon","description":"Denies the default_window_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["default_window_icon"]}},"deny-exit":{"identifier":"deny-exit","description":"Denies the exit command without any pre-configured scope.","commands":{"allow":[],"deny":["exit"]}},"deny-fetch-data-store-identifiers":{"identifier":"deny-fetch-data-store-identifiers","description":"Denies the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":[],"deny":["fetch_data_store_identifiers"]}},"deny-identifier":{"identifier":"deny-identifier","description":"Denies the identifier command without any pre-configured scope.","commands":{"allow":[],"deny":["identifier"]}},"deny-name":{"identifier":"deny-name","description":"Denies the name command without any pre-configured scope.","commands":{"allow":[],"deny":["name"]}},"deny-register-listener":{"identifier":"deny-register-listener","description":"Denies the register_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["register_listener"]}},"deny-remove-data-store":{"identifier":"deny-remove-data-store","description":"Denies the remove_data_store command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_data_store"]}},"deny-remove-listener":{"identifier":"deny-remove-listener","description":"Denies the remove_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_listener"]}},"deny-set-app-theme":{"identifier":"deny-set-app-theme","description":"Denies the set_app_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_app_theme"]}},"deny-set-dock-visibility":{"identifier":"deny-set-dock-visibility","description":"Denies the set_dock_visibility command without any pre-configured scope.","commands":{"allow":[],"deny":["set_dock_visibility"]}},"deny-supports-multiple-windows":{"identifier":"deny-supports-multiple-windows","description":"Denies the supports_multiple_windows command without any pre-configured scope.","commands":{"allow":[],"deny":["supports_multiple_windows"]}},"deny-tauri-version":{"identifier":"deny-tauri-version","description":"Denies the tauri_version command without any pre-configured scope.","commands":{"allow":[],"deny":["tauri_version"]}},"deny-version":{"identifier":"deny-version","description":"Denies the version command without any pre-configured scope.","commands":{"allow":[],"deny":["version"]}}},"permission_sets":{},"global_scope_schema":null},"core:event":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-listen","allow-unlisten","allow-emit","allow-emit-to"]},"permissions":{"allow-emit":{"identifier":"allow-emit","description":"Enables the emit command without any pre-configured scope.","commands":{"allow":["emit"],"deny":[]}},"allow-emit-to":{"identifier":"allow-emit-to","description":"Enables the emit_to command without any pre-configured scope.","commands":{"allow":["emit_to"],"deny":[]}},"allow-listen":{"identifier":"allow-listen","description":"Enables the listen command without any pre-configured scope.","commands":{"allow":["listen"],"deny":[]}},"allow-unlisten":{"identifier":"allow-unlisten","description":"Enables the unlisten command without any pre-configured scope.","commands":{"allow":["unlisten"],"deny":[]}},"deny-emit":{"identifier":"deny-emit","description":"Denies the emit command without any pre-configured scope.","commands":{"allow":[],"deny":["emit"]}},"deny-emit-to":{"identifier":"deny-emit-to","description":"Denies the emit_to command without any pre-configured scope.","commands":{"allow":[],"deny":["emit_to"]}},"deny-listen":{"identifier":"deny-listen","description":"Denies the listen command without any pre-configured scope.","commands":{"allow":[],"deny":["listen"]}},"deny-unlisten":{"identifier":"deny-unlisten","description":"Denies the unlisten command without any pre-configured scope.","commands":{"allow":[],"deny":["unlisten"]}}},"permission_sets":{},"global_scope_schema":null},"core:image":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-from-bytes","allow-from-path","allow-rgba","allow-size"]},"permissions":{"allow-from-bytes":{"identifier":"allow-from-bytes","description":"Enables the from_bytes command without any pre-configured scope.","commands":{"allow":["from_bytes"],"deny":[]}},"allow-from-path":{"identifier":"allow-from-path","description":"Enables the from_path command without any pre-configured scope.","commands":{"allow":["from_path"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-rgba":{"identifier":"allow-rgba","description":"Enables the rgba command without any pre-configured scope.","commands":{"allow":["rgba"],"deny":[]}},"allow-size":{"identifier":"allow-size","description":"Enables the size command without any pre-configured scope.","commands":{"allow":["size"],"deny":[]}},"deny-from-bytes":{"identifier":"deny-from-bytes","description":"Denies the from_bytes command without any pre-configured scope.","commands":{"allow":[],"deny":["from_bytes"]}},"deny-from-path":{"identifier":"deny-from-path","description":"Denies the from_path command without any pre-configured scope.","commands":{"allow":[],"deny":["from_path"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-rgba":{"identifier":"deny-rgba","description":"Denies the rgba command without any pre-configured scope.","commands":{"allow":[],"deny":["rgba"]}},"deny-size":{"identifier":"deny-size","description":"Denies the size command without any pre-configured scope.","commands":{"allow":[],"deny":["size"]}}},"permission_sets":{},"global_scope_schema":null},"core:menu":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-append","allow-prepend","allow-insert","allow-remove","allow-remove-at","allow-items","allow-get","allow-popup","allow-create-default","allow-set-as-app-menu","allow-set-as-window-menu","allow-text","allow-set-text","allow-is-enabled","allow-set-enabled","allow-set-accelerator","allow-set-as-windows-menu-for-nsapp","allow-set-as-help-menu-for-nsapp","allow-is-checked","allow-set-checked","allow-set-icon"]},"permissions":{"allow-append":{"identifier":"allow-append","description":"Enables the append command without any pre-configured scope.","commands":{"allow":["append"],"deny":[]}},"allow-create-default":{"identifier":"allow-create-default","description":"Enables the create_default command without any pre-configured scope.","commands":{"allow":["create_default"],"deny":[]}},"allow-get":{"identifier":"allow-get","description":"Enables the get command without any pre-configured scope.","commands":{"allow":["get"],"deny":[]}},"allow-insert":{"identifier":"allow-insert","description":"Enables the insert command without any pre-configured scope.","commands":{"allow":["insert"],"deny":[]}},"allow-is-checked":{"identifier":"allow-is-checked","description":"Enables the is_checked command without any pre-configured scope.","commands":{"allow":["is_checked"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-items":{"identifier":"allow-items","description":"Enables the items command without any pre-configured scope.","commands":{"allow":["items"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-popup":{"identifier":"allow-popup","description":"Enables the popup command without any pre-configured scope.","commands":{"allow":["popup"],"deny":[]}},"allow-prepend":{"identifier":"allow-prepend","description":"Enables the prepend command without any pre-configured scope.","commands":{"allow":["prepend"],"deny":[]}},"allow-remove":{"identifier":"allow-remove","description":"Enables the remove command without any pre-configured scope.","commands":{"allow":["remove"],"deny":[]}},"allow-remove-at":{"identifier":"allow-remove-at","description":"Enables the remove_at command without any pre-configured scope.","commands":{"allow":["remove_at"],"deny":[]}},"allow-set-accelerator":{"identifier":"allow-set-accelerator","description":"Enables the set_accelerator command without any pre-configured scope.","commands":{"allow":["set_accelerator"],"deny":[]}},"allow-set-as-app-menu":{"identifier":"allow-set-as-app-menu","description":"Enables the set_as_app_menu command without any pre-configured scope.","commands":{"allow":["set_as_app_menu"],"deny":[]}},"allow-set-as-help-menu-for-nsapp":{"identifier":"allow-set-as-help-menu-for-nsapp","description":"Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_help_menu_for_nsapp"],"deny":[]}},"allow-set-as-window-menu":{"identifier":"allow-set-as-window-menu","description":"Enables the set_as_window_menu command without any pre-configured scope.","commands":{"allow":["set_as_window_menu"],"deny":[]}},"allow-set-as-windows-menu-for-nsapp":{"identifier":"allow-set-as-windows-menu-for-nsapp","description":"Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_windows_menu_for_nsapp"],"deny":[]}},"allow-set-checked":{"identifier":"allow-set-checked","description":"Enables the set_checked command without any pre-configured scope.","commands":{"allow":["set_checked"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-text":{"identifier":"allow-set-text","description":"Enables the set_text command without any pre-configured scope.","commands":{"allow":["set_text"],"deny":[]}},"allow-text":{"identifier":"allow-text","description":"Enables the text command without any pre-configured scope.","commands":{"allow":["text"],"deny":[]}},"deny-append":{"identifier":"deny-append","description":"Denies the append command without any pre-configured scope.","commands":{"allow":[],"deny":["append"]}},"deny-create-default":{"identifier":"deny-create-default","description":"Denies the create_default command without any pre-configured scope.","commands":{"allow":[],"deny":["create_default"]}},"deny-get":{"identifier":"deny-get","description":"Denies the get command without any pre-configured scope.","commands":{"allow":[],"deny":["get"]}},"deny-insert":{"identifier":"deny-insert","description":"Denies the insert command without any pre-configured scope.","commands":{"allow":[],"deny":["insert"]}},"deny-is-checked":{"identifier":"deny-is-checked","description":"Denies the is_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["is_checked"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-items":{"identifier":"deny-items","description":"Denies the items command without any pre-configured scope.","commands":{"allow":[],"deny":["items"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-popup":{"identifier":"deny-popup","description":"Denies the popup command without any pre-configured scope.","commands":{"allow":[],"deny":["popup"]}},"deny-prepend":{"identifier":"deny-prepend","description":"Denies the prepend command without any pre-configured scope.","commands":{"allow":[],"deny":["prepend"]}},"deny-remove":{"identifier":"deny-remove","description":"Denies the remove command without any pre-configured scope.","commands":{"allow":[],"deny":["remove"]}},"deny-remove-at":{"identifier":"deny-remove-at","description":"Denies the remove_at command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_at"]}},"deny-set-accelerator":{"identifier":"deny-set-accelerator","description":"Denies the set_accelerator command without any pre-configured scope.","commands":{"allow":[],"deny":["set_accelerator"]}},"deny-set-as-app-menu":{"identifier":"deny-set-as-app-menu","description":"Denies the set_as_app_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_app_menu"]}},"deny-set-as-help-menu-for-nsapp":{"identifier":"deny-set-as-help-menu-for-nsapp","description":"Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_help_menu_for_nsapp"]}},"deny-set-as-window-menu":{"identifier":"deny-set-as-window-menu","description":"Denies the set_as_window_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_window_menu"]}},"deny-set-as-windows-menu-for-nsapp":{"identifier":"deny-set-as-windows-menu-for-nsapp","description":"Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_windows_menu_for_nsapp"]}},"deny-set-checked":{"identifier":"deny-set-checked","description":"Denies the set_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["set_checked"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-text":{"identifier":"deny-set-text","description":"Denies the set_text command without any pre-configured scope.","commands":{"allow":[],"deny":["set_text"]}},"deny-text":{"identifier":"deny-text","description":"Denies the text command without any pre-configured scope.","commands":{"allow":[],"deny":["text"]}}},"permission_sets":{},"global_scope_schema":null},"core:path":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-resolve-directory","allow-resolve","allow-normalize","allow-join","allow-dirname","allow-extname","allow-basename","allow-is-absolute"]},"permissions":{"allow-basename":{"identifier":"allow-basename","description":"Enables the basename command without any pre-configured scope.","commands":{"allow":["basename"],"deny":[]}},"allow-dirname":{"identifier":"allow-dirname","description":"Enables the dirname command without any pre-configured scope.","commands":{"allow":["dirname"],"deny":[]}},"allow-extname":{"identifier":"allow-extname","description":"Enables the extname command without any pre-configured scope.","commands":{"allow":["extname"],"deny":[]}},"allow-is-absolute":{"identifier":"allow-is-absolute","description":"Enables the is_absolute command without any pre-configured scope.","commands":{"allow":["is_absolute"],"deny":[]}},"allow-join":{"identifier":"allow-join","description":"Enables the join command without any pre-configured scope.","commands":{"allow":["join"],"deny":[]}},"allow-normalize":{"identifier":"allow-normalize","description":"Enables the normalize command without any pre-configured scope.","commands":{"allow":["normalize"],"deny":[]}},"allow-resolve":{"identifier":"allow-resolve","description":"Enables the resolve command without any pre-configured scope.","commands":{"allow":["resolve"],"deny":[]}},"allow-resolve-directory":{"identifier":"allow-resolve-directory","description":"Enables the resolve_directory command without any pre-configured scope.","commands":{"allow":["resolve_directory"],"deny":[]}},"deny-basename":{"identifier":"deny-basename","description":"Denies the basename command without any pre-configured scope.","commands":{"allow":[],"deny":["basename"]}},"deny-dirname":{"identifier":"deny-dirname","description":"Denies the dirname command without any pre-configured scope.","commands":{"allow":[],"deny":["dirname"]}},"deny-extname":{"identifier":"deny-extname","description":"Denies the extname command without any pre-configured scope.","commands":{"allow":[],"deny":["extname"]}},"deny-is-absolute":{"identifier":"deny-is-absolute","description":"Denies the is_absolute command without any pre-configured scope.","commands":{"allow":[],"deny":["is_absolute"]}},"deny-join":{"identifier":"deny-join","description":"Denies the join command without any pre-configured scope.","commands":{"allow":[],"deny":["join"]}},"deny-normalize":{"identifier":"deny-normalize","description":"Denies the normalize command without any pre-configured scope.","commands":{"allow":[],"deny":["normalize"]}},"deny-resolve":{"identifier":"deny-resolve","description":"Denies the resolve command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve"]}},"deny-resolve-directory":{"identifier":"deny-resolve-directory","description":"Denies the resolve_directory command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve_directory"]}}},"permission_sets":{},"global_scope_schema":null},"core:resources":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-close"]},"permissions":{"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}}},"permission_sets":{},"global_scope_schema":null},"core:tray":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-get-by-id","allow-remove-by-id","allow-set-icon","allow-set-menu","allow-set-tooltip","allow-set-title","allow-set-visible","allow-set-temp-dir-path","allow-set-icon-as-template","allow-set-icon-with-as-template","allow-set-show-menu-on-left-click"]},"permissions":{"allow-get-by-id":{"identifier":"allow-get-by-id","description":"Enables the get_by_id command without any pre-configured scope.","commands":{"allow":["get_by_id"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-remove-by-id":{"identifier":"allow-remove-by-id","description":"Enables the remove_by_id command without any pre-configured scope.","commands":{"allow":["remove_by_id"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-icon-as-template":{"identifier":"allow-set-icon-as-template","description":"Enables the set_icon_as_template command without any pre-configured scope.","commands":{"allow":["set_icon_as_template"],"deny":[]}},"allow-set-icon-with-as-template":{"identifier":"allow-set-icon-with-as-template","description":"Enables the set_icon_with_as_template command without any pre-configured scope.","commands":{"allow":["set_icon_with_as_template"],"deny":[]}},"allow-set-menu":{"identifier":"allow-set-menu","description":"Enables the set_menu command without any pre-configured scope.","commands":{"allow":["set_menu"],"deny":[]}},"allow-set-show-menu-on-left-click":{"identifier":"allow-set-show-menu-on-left-click","description":"Enables the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":["set_show_menu_on_left_click"],"deny":[]}},"allow-set-temp-dir-path":{"identifier":"allow-set-temp-dir-path","description":"Enables the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":["set_temp_dir_path"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-tooltip":{"identifier":"allow-set-tooltip","description":"Enables the set_tooltip command without any pre-configured scope.","commands":{"allow":["set_tooltip"],"deny":[]}},"allow-set-visible":{"identifier":"allow-set-visible","description":"Enables the set_visible command without any pre-configured scope.","commands":{"allow":["set_visible"],"deny":[]}},"deny-get-by-id":{"identifier":"deny-get-by-id","description":"Denies the get_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["get_by_id"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-remove-by-id":{"identifier":"deny-remove-by-id","description":"Denies the remove_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_by_id"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-icon-as-template":{"identifier":"deny-set-icon-as-template","description":"Denies the set_icon_as_template command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon_as_template"]}},"deny-set-icon-with-as-template":{"identifier":"deny-set-icon-with-as-template","description":"Denies the set_icon_with_as_template command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon_with_as_template"]}},"deny-set-menu":{"identifier":"deny-set-menu","description":"Denies the set_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_menu"]}},"deny-set-show-menu-on-left-click":{"identifier":"deny-set-show-menu-on-left-click","description":"Denies the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":[],"deny":["set_show_menu_on_left_click"]}},"deny-set-temp-dir-path":{"identifier":"deny-set-temp-dir-path","description":"Denies the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":[],"deny":["set_temp_dir_path"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-tooltip":{"identifier":"deny-set-tooltip","description":"Denies the set_tooltip command without any pre-configured scope.","commands":{"allow":[],"deny":["set_tooltip"]}},"deny-set-visible":{"identifier":"deny-set-visible","description":"Denies the set_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible"]}}},"permission_sets":{},"global_scope_schema":null},"core:webview":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-webviews","allow-webview-position","allow-webview-size","allow-internal-toggle-devtools"]},"permissions":{"allow-clear-all-browsing-data":{"identifier":"allow-clear-all-browsing-data","description":"Enables the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":["clear_all_browsing_data"],"deny":[]}},"allow-create-webview":{"identifier":"allow-create-webview","description":"Enables the create_webview command without any pre-configured scope.","commands":{"allow":["create_webview"],"deny":[]}},"allow-create-webview-window":{"identifier":"allow-create-webview-window","description":"Enables the create_webview_window command without any pre-configured scope.","commands":{"allow":["create_webview_window"],"deny":[]}},"allow-get-all-webviews":{"identifier":"allow-get-all-webviews","description":"Enables the get_all_webviews command without any pre-configured scope.","commands":{"allow":["get_all_webviews"],"deny":[]}},"allow-internal-toggle-devtools":{"identifier":"allow-internal-toggle-devtools","description":"Enables the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":["internal_toggle_devtools"],"deny":[]}},"allow-print":{"identifier":"allow-print","description":"Enables the print command without any pre-configured scope.","commands":{"allow":["print"],"deny":[]}},"allow-reparent":{"identifier":"allow-reparent","description":"Enables the reparent command without any pre-configured scope.","commands":{"allow":["reparent"],"deny":[]}},"allow-set-webview-auto-resize":{"identifier":"allow-set-webview-auto-resize","description":"Enables the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":["set_webview_auto_resize"],"deny":[]}},"allow-set-webview-background-color":{"identifier":"allow-set-webview-background-color","description":"Enables the set_webview_background_color command without any pre-configured scope.","commands":{"allow":["set_webview_background_color"],"deny":[]}},"allow-set-webview-focus":{"identifier":"allow-set-webview-focus","description":"Enables the set_webview_focus command without any pre-configured scope.","commands":{"allow":["set_webview_focus"],"deny":[]}},"allow-set-webview-position":{"identifier":"allow-set-webview-position","description":"Enables the set_webview_position command without any pre-configured scope.","commands":{"allow":["set_webview_position"],"deny":[]}},"allow-set-webview-size":{"identifier":"allow-set-webview-size","description":"Enables the set_webview_size command without any pre-configured scope.","commands":{"allow":["set_webview_size"],"deny":[]}},"allow-set-webview-zoom":{"identifier":"allow-set-webview-zoom","description":"Enables the set_webview_zoom command without any pre-configured scope.","commands":{"allow":["set_webview_zoom"],"deny":[]}},"allow-webview-close":{"identifier":"allow-webview-close","description":"Enables the webview_close command without any pre-configured scope.","commands":{"allow":["webview_close"],"deny":[]}},"allow-webview-hide":{"identifier":"allow-webview-hide","description":"Enables the webview_hide command without any pre-configured scope.","commands":{"allow":["webview_hide"],"deny":[]}},"allow-webview-position":{"identifier":"allow-webview-position","description":"Enables the webview_position command without any pre-configured scope.","commands":{"allow":["webview_position"],"deny":[]}},"allow-webview-show":{"identifier":"allow-webview-show","description":"Enables the webview_show command without any pre-configured scope.","commands":{"allow":["webview_show"],"deny":[]}},"allow-webview-size":{"identifier":"allow-webview-size","description":"Enables the webview_size command without any pre-configured scope.","commands":{"allow":["webview_size"],"deny":[]}},"deny-clear-all-browsing-data":{"identifier":"deny-clear-all-browsing-data","description":"Denies the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":[],"deny":["clear_all_browsing_data"]}},"deny-create-webview":{"identifier":"deny-create-webview","description":"Denies the create_webview command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview"]}},"deny-create-webview-window":{"identifier":"deny-create-webview-window","description":"Denies the create_webview_window command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview_window"]}},"deny-get-all-webviews":{"identifier":"deny-get-all-webviews","description":"Denies the get_all_webviews command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_webviews"]}},"deny-internal-toggle-devtools":{"identifier":"deny-internal-toggle-devtools","description":"Denies the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_devtools"]}},"deny-print":{"identifier":"deny-print","description":"Denies the print command without any pre-configured scope.","commands":{"allow":[],"deny":["print"]}},"deny-reparent":{"identifier":"deny-reparent","description":"Denies the reparent command without any pre-configured scope.","commands":{"allow":[],"deny":["reparent"]}},"deny-set-webview-auto-resize":{"identifier":"deny-set-webview-auto-resize","description":"Denies the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_auto_resize"]}},"deny-set-webview-background-color":{"identifier":"deny-set-webview-background-color","description":"Denies the set_webview_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_background_color"]}},"deny-set-webview-focus":{"identifier":"deny-set-webview-focus","description":"Denies the set_webview_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_focus"]}},"deny-set-webview-position":{"identifier":"deny-set-webview-position","description":"Denies the set_webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_position"]}},"deny-set-webview-size":{"identifier":"deny-set-webview-size","description":"Denies the set_webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_size"]}},"deny-set-webview-zoom":{"identifier":"deny-set-webview-zoom","description":"Denies the set_webview_zoom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_zoom"]}},"deny-webview-close":{"identifier":"deny-webview-close","description":"Denies the webview_close command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_close"]}},"deny-webview-hide":{"identifier":"deny-webview-hide","description":"Denies the webview_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_hide"]}},"deny-webview-position":{"identifier":"deny-webview-position","description":"Denies the webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_position"]}},"deny-webview-show":{"identifier":"deny-webview-show","description":"Denies the webview_show command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_show"]}},"deny-webview-size":{"identifier":"deny-webview-size","description":"Denies the webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_size"]}}},"permission_sets":{},"global_scope_schema":null},"core:window":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-windows","allow-scale-factor","allow-inner-position","allow-outer-position","allow-inner-size","allow-outer-size","allow-is-fullscreen","allow-is-minimized","allow-is-maximized","allow-is-focused","allow-is-decorated","allow-is-resizable","allow-is-maximizable","allow-is-minimizable","allow-is-closable","allow-is-visible","allow-is-enabled","allow-title","allow-current-monitor","allow-primary-monitor","allow-monitor-from-point","allow-available-monitors","allow-cursor-position","allow-theme","allow-is-always-on-top","allow-activity-name","allow-scene-identifier","allow-internal-toggle-maximize"]},"permissions":{"allow-activity-name":{"identifier":"allow-activity-name","description":"Enables the activity_name command without any pre-configured scope.","commands":{"allow":["activity_name"],"deny":[]}},"allow-available-monitors":{"identifier":"allow-available-monitors","description":"Enables the available_monitors command without any pre-configured scope.","commands":{"allow":["available_monitors"],"deny":[]}},"allow-center":{"identifier":"allow-center","description":"Enables the center command without any pre-configured scope.","commands":{"allow":["center"],"deny":[]}},"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"allow-create":{"identifier":"allow-create","description":"Enables the create command without any pre-configured scope.","commands":{"allow":["create"],"deny":[]}},"allow-current-monitor":{"identifier":"allow-current-monitor","description":"Enables the current_monitor command without any pre-configured scope.","commands":{"allow":["current_monitor"],"deny":[]}},"allow-cursor-position":{"identifier":"allow-cursor-position","description":"Enables the cursor_position command without any pre-configured scope.","commands":{"allow":["cursor_position"],"deny":[]}},"allow-destroy":{"identifier":"allow-destroy","description":"Enables the destroy command without any pre-configured scope.","commands":{"allow":["destroy"],"deny":[]}},"allow-get-all-windows":{"identifier":"allow-get-all-windows","description":"Enables the get_all_windows command without any pre-configured scope.","commands":{"allow":["get_all_windows"],"deny":[]}},"allow-hide":{"identifier":"allow-hide","description":"Enables the hide command without any pre-configured scope.","commands":{"allow":["hide"],"deny":[]}},"allow-inner-position":{"identifier":"allow-inner-position","description":"Enables the inner_position command without any pre-configured scope.","commands":{"allow":["inner_position"],"deny":[]}},"allow-inner-size":{"identifier":"allow-inner-size","description":"Enables the inner_size command without any pre-configured scope.","commands":{"allow":["inner_size"],"deny":[]}},"allow-internal-toggle-maximize":{"identifier":"allow-internal-toggle-maximize","description":"Enables the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":["internal_toggle_maximize"],"deny":[]}},"allow-is-always-on-top":{"identifier":"allow-is-always-on-top","description":"Enables the is_always_on_top command without any pre-configured scope.","commands":{"allow":["is_always_on_top"],"deny":[]}},"allow-is-closable":{"identifier":"allow-is-closable","description":"Enables the is_closable command without any pre-configured scope.","commands":{"allow":["is_closable"],"deny":[]}},"allow-is-decorated":{"identifier":"allow-is-decorated","description":"Enables the is_decorated command without any pre-configured scope.","commands":{"allow":["is_decorated"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-is-focused":{"identifier":"allow-is-focused","description":"Enables the is_focused command without any pre-configured scope.","commands":{"allow":["is_focused"],"deny":[]}},"allow-is-fullscreen":{"identifier":"allow-is-fullscreen","description":"Enables the is_fullscreen command without any pre-configured scope.","commands":{"allow":["is_fullscreen"],"deny":[]}},"allow-is-maximizable":{"identifier":"allow-is-maximizable","description":"Enables the is_maximizable command without any pre-configured scope.","commands":{"allow":["is_maximizable"],"deny":[]}},"allow-is-maximized":{"identifier":"allow-is-maximized","description":"Enables the is_maximized command without any pre-configured scope.","commands":{"allow":["is_maximized"],"deny":[]}},"allow-is-minimizable":{"identifier":"allow-is-minimizable","description":"Enables the is_minimizable command without any pre-configured scope.","commands":{"allow":["is_minimizable"],"deny":[]}},"allow-is-minimized":{"identifier":"allow-is-minimized","description":"Enables the is_minimized command without any pre-configured scope.","commands":{"allow":["is_minimized"],"deny":[]}},"allow-is-resizable":{"identifier":"allow-is-resizable","description":"Enables the is_resizable command without any pre-configured scope.","commands":{"allow":["is_resizable"],"deny":[]}},"allow-is-visible":{"identifier":"allow-is-visible","description":"Enables the is_visible command without any pre-configured scope.","commands":{"allow":["is_visible"],"deny":[]}},"allow-maximize":{"identifier":"allow-maximize","description":"Enables the maximize command without any pre-configured scope.","commands":{"allow":["maximize"],"deny":[]}},"allow-minimize":{"identifier":"allow-minimize","description":"Enables the minimize command without any pre-configured scope.","commands":{"allow":["minimize"],"deny":[]}},"allow-monitor-from-point":{"identifier":"allow-monitor-from-point","description":"Enables the monitor_from_point command without any pre-configured scope.","commands":{"allow":["monitor_from_point"],"deny":[]}},"allow-outer-position":{"identifier":"allow-outer-position","description":"Enables the outer_position command without any pre-configured scope.","commands":{"allow":["outer_position"],"deny":[]}},"allow-outer-size":{"identifier":"allow-outer-size","description":"Enables the outer_size command without any pre-configured scope.","commands":{"allow":["outer_size"],"deny":[]}},"allow-primary-monitor":{"identifier":"allow-primary-monitor","description":"Enables the primary_monitor command without any pre-configured scope.","commands":{"allow":["primary_monitor"],"deny":[]}},"allow-request-user-attention":{"identifier":"allow-request-user-attention","description":"Enables the request_user_attention command without any pre-configured scope.","commands":{"allow":["request_user_attention"],"deny":[]}},"allow-scale-factor":{"identifier":"allow-scale-factor","description":"Enables the scale_factor command without any pre-configured scope.","commands":{"allow":["scale_factor"],"deny":[]}},"allow-scene-identifier":{"identifier":"allow-scene-identifier","description":"Enables the scene_identifier command without any pre-configured scope.","commands":{"allow":["scene_identifier"],"deny":[]}},"allow-set-always-on-bottom":{"identifier":"allow-set-always-on-bottom","description":"Enables the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":["set_always_on_bottom"],"deny":[]}},"allow-set-always-on-top":{"identifier":"allow-set-always-on-top","description":"Enables the set_always_on_top command without any pre-configured scope.","commands":{"allow":["set_always_on_top"],"deny":[]}},"allow-set-background-color":{"identifier":"allow-set-background-color","description":"Enables the set_background_color command without any pre-configured scope.","commands":{"allow":["set_background_color"],"deny":[]}},"allow-set-badge-count":{"identifier":"allow-set-badge-count","description":"Enables the set_badge_count command without any pre-configured scope.","commands":{"allow":["set_badge_count"],"deny":[]}},"allow-set-badge-label":{"identifier":"allow-set-badge-label","description":"Enables the set_badge_label command without any pre-configured scope.","commands":{"allow":["set_badge_label"],"deny":[]}},"allow-set-closable":{"identifier":"allow-set-closable","description":"Enables the set_closable command without any pre-configured scope.","commands":{"allow":["set_closable"],"deny":[]}},"allow-set-content-protected":{"identifier":"allow-set-content-protected","description":"Enables the set_content_protected command without any pre-configured scope.","commands":{"allow":["set_content_protected"],"deny":[]}},"allow-set-cursor-grab":{"identifier":"allow-set-cursor-grab","description":"Enables the set_cursor_grab command without any pre-configured scope.","commands":{"allow":["set_cursor_grab"],"deny":[]}},"allow-set-cursor-icon":{"identifier":"allow-set-cursor-icon","description":"Enables the set_cursor_icon command without any pre-configured scope.","commands":{"allow":["set_cursor_icon"],"deny":[]}},"allow-set-cursor-position":{"identifier":"allow-set-cursor-position","description":"Enables the set_cursor_position command without any pre-configured scope.","commands":{"allow":["set_cursor_position"],"deny":[]}},"allow-set-cursor-visible":{"identifier":"allow-set-cursor-visible","description":"Enables the set_cursor_visible command without any pre-configured scope.","commands":{"allow":["set_cursor_visible"],"deny":[]}},"allow-set-decorations":{"identifier":"allow-set-decorations","description":"Enables the set_decorations command without any pre-configured scope.","commands":{"allow":["set_decorations"],"deny":[]}},"allow-set-effects":{"identifier":"allow-set-effects","description":"Enables the set_effects command without any pre-configured scope.","commands":{"allow":["set_effects"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-focus":{"identifier":"allow-set-focus","description":"Enables the set_focus command without any pre-configured scope.","commands":{"allow":["set_focus"],"deny":[]}},"allow-set-focusable":{"identifier":"allow-set-focusable","description":"Enables the set_focusable command without any pre-configured scope.","commands":{"allow":["set_focusable"],"deny":[]}},"allow-set-fullscreen":{"identifier":"allow-set-fullscreen","description":"Enables the set_fullscreen command without any pre-configured scope.","commands":{"allow":["set_fullscreen"],"deny":[]}},"allow-set-fullscreen-on-monitor":{"identifier":"allow-set-fullscreen-on-monitor","description":"Enables the set_fullscreen_on_monitor command without any pre-configured scope.","commands":{"allow":["set_fullscreen_on_monitor"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-ignore-cursor-events":{"identifier":"allow-set-ignore-cursor-events","description":"Enables the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":["set_ignore_cursor_events"],"deny":[]}},"allow-set-max-size":{"identifier":"allow-set-max-size","description":"Enables the set_max_size command without any pre-configured scope.","commands":{"allow":["set_max_size"],"deny":[]}},"allow-set-maximizable":{"identifier":"allow-set-maximizable","description":"Enables the set_maximizable command without any pre-configured scope.","commands":{"allow":["set_maximizable"],"deny":[]}},"allow-set-min-size":{"identifier":"allow-set-min-size","description":"Enables the set_min_size command without any pre-configured scope.","commands":{"allow":["set_min_size"],"deny":[]}},"allow-set-minimizable":{"identifier":"allow-set-minimizable","description":"Enables the set_minimizable command without any pre-configured scope.","commands":{"allow":["set_minimizable"],"deny":[]}},"allow-set-overlay-icon":{"identifier":"allow-set-overlay-icon","description":"Enables the set_overlay_icon command without any pre-configured scope.","commands":{"allow":["set_overlay_icon"],"deny":[]}},"allow-set-position":{"identifier":"allow-set-position","description":"Enables the set_position command without any pre-configured scope.","commands":{"allow":["set_position"],"deny":[]}},"allow-set-progress-bar":{"identifier":"allow-set-progress-bar","description":"Enables the set_progress_bar command without any pre-configured scope.","commands":{"allow":["set_progress_bar"],"deny":[]}},"allow-set-resizable":{"identifier":"allow-set-resizable","description":"Enables the set_resizable command without any pre-configured scope.","commands":{"allow":["set_resizable"],"deny":[]}},"allow-set-shadow":{"identifier":"allow-set-shadow","description":"Enables the set_shadow command without any pre-configured scope.","commands":{"allow":["set_shadow"],"deny":[]}},"allow-set-simple-fullscreen":{"identifier":"allow-set-simple-fullscreen","description":"Enables the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":["set_simple_fullscreen"],"deny":[]}},"allow-set-size":{"identifier":"allow-set-size","description":"Enables the set_size command without any pre-configured scope.","commands":{"allow":["set_size"],"deny":[]}},"allow-set-size-constraints":{"identifier":"allow-set-size-constraints","description":"Enables the set_size_constraints command without any pre-configured scope.","commands":{"allow":["set_size_constraints"],"deny":[]}},"allow-set-skip-taskbar":{"identifier":"allow-set-skip-taskbar","description":"Enables the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":["set_skip_taskbar"],"deny":[]}},"allow-set-theme":{"identifier":"allow-set-theme","description":"Enables the set_theme command without any pre-configured scope.","commands":{"allow":["set_theme"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-title-bar-style":{"identifier":"allow-set-title-bar-style","description":"Enables the set_title_bar_style command without any pre-configured scope.","commands":{"allow":["set_title_bar_style"],"deny":[]}},"allow-set-visible-on-all-workspaces":{"identifier":"allow-set-visible-on-all-workspaces","description":"Enables the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":["set_visible_on_all_workspaces"],"deny":[]}},"allow-show":{"identifier":"allow-show","description":"Enables the show command without any pre-configured scope.","commands":{"allow":["show"],"deny":[]}},"allow-start-dragging":{"identifier":"allow-start-dragging","description":"Enables the start_dragging command without any pre-configured scope.","commands":{"allow":["start_dragging"],"deny":[]}},"allow-start-resize-dragging":{"identifier":"allow-start-resize-dragging","description":"Enables the start_resize_dragging command without any pre-configured scope.","commands":{"allow":["start_resize_dragging"],"deny":[]}},"allow-theme":{"identifier":"allow-theme","description":"Enables the theme command without any pre-configured scope.","commands":{"allow":["theme"],"deny":[]}},"allow-title":{"identifier":"allow-title","description":"Enables the title command without any pre-configured scope.","commands":{"allow":["title"],"deny":[]}},"allow-toggle-maximize":{"identifier":"allow-toggle-maximize","description":"Enables the toggle_maximize command without any pre-configured scope.","commands":{"allow":["toggle_maximize"],"deny":[]}},"allow-unmaximize":{"identifier":"allow-unmaximize","description":"Enables the unmaximize command without any pre-configured scope.","commands":{"allow":["unmaximize"],"deny":[]}},"allow-unminimize":{"identifier":"allow-unminimize","description":"Enables the unminimize command without any pre-configured scope.","commands":{"allow":["unminimize"],"deny":[]}},"deny-activity-name":{"identifier":"deny-activity-name","description":"Denies the activity_name command without any pre-configured scope.","commands":{"allow":[],"deny":["activity_name"]}},"deny-available-monitors":{"identifier":"deny-available-monitors","description":"Denies the available_monitors command without any pre-configured scope.","commands":{"allow":[],"deny":["available_monitors"]}},"deny-center":{"identifier":"deny-center","description":"Denies the center command without any pre-configured scope.","commands":{"allow":[],"deny":["center"]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}},"deny-create":{"identifier":"deny-create","description":"Denies the create command without any pre-configured scope.","commands":{"allow":[],"deny":["create"]}},"deny-current-monitor":{"identifier":"deny-current-monitor","description":"Denies the current_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["current_monitor"]}},"deny-cursor-position":{"identifier":"deny-cursor-position","description":"Denies the cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["cursor_position"]}},"deny-destroy":{"identifier":"deny-destroy","description":"Denies the destroy command without any pre-configured scope.","commands":{"allow":[],"deny":["destroy"]}},"deny-get-all-windows":{"identifier":"deny-get-all-windows","description":"Denies the get_all_windows command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_windows"]}},"deny-hide":{"identifier":"deny-hide","description":"Denies the hide command without any pre-configured scope.","commands":{"allow":[],"deny":["hide"]}},"deny-inner-position":{"identifier":"deny-inner-position","description":"Denies the inner_position command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_position"]}},"deny-inner-size":{"identifier":"deny-inner-size","description":"Denies the inner_size command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_size"]}},"deny-internal-toggle-maximize":{"identifier":"deny-internal-toggle-maximize","description":"Denies the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_maximize"]}},"deny-is-always-on-top":{"identifier":"deny-is-always-on-top","description":"Denies the is_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["is_always_on_top"]}},"deny-is-closable":{"identifier":"deny-is-closable","description":"Denies the is_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_closable"]}},"deny-is-decorated":{"identifier":"deny-is-decorated","description":"Denies the is_decorated command without any pre-configured scope.","commands":{"allow":[],"deny":["is_decorated"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-is-focused":{"identifier":"deny-is-focused","description":"Denies the is_focused command without any pre-configured scope.","commands":{"allow":[],"deny":["is_focused"]}},"deny-is-fullscreen":{"identifier":"deny-is-fullscreen","description":"Denies the is_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["is_fullscreen"]}},"deny-is-maximizable":{"identifier":"deny-is-maximizable","description":"Denies the is_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximizable"]}},"deny-is-maximized":{"identifier":"deny-is-maximized","description":"Denies the is_maximized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximized"]}},"deny-is-minimizable":{"identifier":"deny-is-minimizable","description":"Denies the is_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimizable"]}},"deny-is-minimized":{"identifier":"deny-is-minimized","description":"Denies the is_minimized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimized"]}},"deny-is-resizable":{"identifier":"deny-is-resizable","description":"Denies the is_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_resizable"]}},"deny-is-visible":{"identifier":"deny-is-visible","description":"Denies the is_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["is_visible"]}},"deny-maximize":{"identifier":"deny-maximize","description":"Denies the maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["maximize"]}},"deny-minimize":{"identifier":"deny-minimize","description":"Denies the minimize command without any pre-configured scope.","commands":{"allow":[],"deny":["minimize"]}},"deny-monitor-from-point":{"identifier":"deny-monitor-from-point","description":"Denies the monitor_from_point command without any pre-configured scope.","commands":{"allow":[],"deny":["monitor_from_point"]}},"deny-outer-position":{"identifier":"deny-outer-position","description":"Denies the outer_position command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_position"]}},"deny-outer-size":{"identifier":"deny-outer-size","description":"Denies the outer_size command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_size"]}},"deny-primary-monitor":{"identifier":"deny-primary-monitor","description":"Denies the primary_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["primary_monitor"]}},"deny-request-user-attention":{"identifier":"deny-request-user-attention","description":"Denies the request_user_attention command without any pre-configured scope.","commands":{"allow":[],"deny":["request_user_attention"]}},"deny-scale-factor":{"identifier":"deny-scale-factor","description":"Denies the scale_factor command without any pre-configured scope.","commands":{"allow":[],"deny":["scale_factor"]}},"deny-scene-identifier":{"identifier":"deny-scene-identifier","description":"Denies the scene_identifier command without any pre-configured scope.","commands":{"allow":[],"deny":["scene_identifier"]}},"deny-set-always-on-bottom":{"identifier":"deny-set-always-on-bottom","description":"Denies the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_bottom"]}},"deny-set-always-on-top":{"identifier":"deny-set-always-on-top","description":"Denies the set_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_top"]}},"deny-set-background-color":{"identifier":"deny-set-background-color","description":"Denies the set_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_background_color"]}},"deny-set-badge-count":{"identifier":"deny-set-badge-count","description":"Denies the set_badge_count command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_count"]}},"deny-set-badge-label":{"identifier":"deny-set-badge-label","description":"Denies the set_badge_label command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_label"]}},"deny-set-closable":{"identifier":"deny-set-closable","description":"Denies the set_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_closable"]}},"deny-set-content-protected":{"identifier":"deny-set-content-protected","description":"Denies the set_content_protected command without any pre-configured scope.","commands":{"allow":[],"deny":["set_content_protected"]}},"deny-set-cursor-grab":{"identifier":"deny-set-cursor-grab","description":"Denies the set_cursor_grab command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_grab"]}},"deny-set-cursor-icon":{"identifier":"deny-set-cursor-icon","description":"Denies the set_cursor_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_icon"]}},"deny-set-cursor-position":{"identifier":"deny-set-cursor-position","description":"Denies the set_cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_position"]}},"deny-set-cursor-visible":{"identifier":"deny-set-cursor-visible","description":"Denies the set_cursor_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_visible"]}},"deny-set-decorations":{"identifier":"deny-set-decorations","description":"Denies the set_decorations command without any pre-configured scope.","commands":{"allow":[],"deny":["set_decorations"]}},"deny-set-effects":{"identifier":"deny-set-effects","description":"Denies the set_effects command without any pre-configured scope.","commands":{"allow":[],"deny":["set_effects"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-focus":{"identifier":"deny-set-focus","description":"Denies the set_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focus"]}},"deny-set-focusable":{"identifier":"deny-set-focusable","description":"Denies the set_focusable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focusable"]}},"deny-set-fullscreen":{"identifier":"deny-set-fullscreen","description":"Denies the set_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_fullscreen"]}},"deny-set-fullscreen-on-monitor":{"identifier":"deny-set-fullscreen-on-monitor","description":"Denies the set_fullscreen_on_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["set_fullscreen_on_monitor"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-ignore-cursor-events":{"identifier":"deny-set-ignore-cursor-events","description":"Denies the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":[],"deny":["set_ignore_cursor_events"]}},"deny-set-max-size":{"identifier":"deny-set-max-size","description":"Denies the set_max_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_max_size"]}},"deny-set-maximizable":{"identifier":"deny-set-maximizable","description":"Denies the set_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_maximizable"]}},"deny-set-min-size":{"identifier":"deny-set-min-size","description":"Denies the set_min_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_min_size"]}},"deny-set-minimizable":{"identifier":"deny-set-minimizable","description":"Denies the set_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_minimizable"]}},"deny-set-overlay-icon":{"identifier":"deny-set-overlay-icon","description":"Denies the set_overlay_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_overlay_icon"]}},"deny-set-position":{"identifier":"deny-set-position","description":"Denies the set_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_position"]}},"deny-set-progress-bar":{"identifier":"deny-set-progress-bar","description":"Denies the set_progress_bar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_progress_bar"]}},"deny-set-resizable":{"identifier":"deny-set-resizable","description":"Denies the set_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_resizable"]}},"deny-set-shadow":{"identifier":"deny-set-shadow","description":"Denies the set_shadow command without any pre-configured scope.","commands":{"allow":[],"deny":["set_shadow"]}},"deny-set-simple-fullscreen":{"identifier":"deny-set-simple-fullscreen","description":"Denies the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_simple_fullscreen"]}},"deny-set-size":{"identifier":"deny-set-size","description":"Denies the set_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size"]}},"deny-set-size-constraints":{"identifier":"deny-set-size-constraints","description":"Denies the set_size_constraints command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size_constraints"]}},"deny-set-skip-taskbar":{"identifier":"deny-set-skip-taskbar","description":"Denies the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_skip_taskbar"]}},"deny-set-theme":{"identifier":"deny-set-theme","description":"Denies the set_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_theme"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-title-bar-style":{"identifier":"deny-set-title-bar-style","description":"Denies the set_title_bar_style command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title_bar_style"]}},"deny-set-visible-on-all-workspaces":{"identifier":"deny-set-visible-on-all-workspaces","description":"Denies the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible_on_all_workspaces"]}},"deny-show":{"identifier":"deny-show","description":"Denies the show command without any pre-configured scope.","commands":{"allow":[],"deny":["show"]}},"deny-start-dragging":{"identifier":"deny-start-dragging","description":"Denies the start_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_dragging"]}},"deny-start-resize-dragging":{"identifier":"deny-start-resize-dragging","description":"Denies the start_resize_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_resize_dragging"]}},"deny-theme":{"identifier":"deny-theme","description":"Denies the theme command without any pre-configured scope.","commands":{"allow":[],"deny":["theme"]}},"deny-title":{"identifier":"deny-title","description":"Denies the title command without any pre-configured scope.","commands":{"allow":[],"deny":["title"]}},"deny-toggle-maximize":{"identifier":"deny-toggle-maximize","description":"Denies the toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["toggle_maximize"]}},"deny-unmaximize":{"identifier":"deny-unmaximize","description":"Denies the unmaximize command without any pre-configured scope.","commands":{"allow":[],"deny":["unmaximize"]}},"deny-unminimize":{"identifier":"deny-unminimize","description":"Denies the unminimize command without any pre-configured scope.","commands":{"allow":[],"deny":["unminimize"]}}},"permission_sets":{},"global_scope_schema":null},"shell":{"default_permission":{"identifier":"default","description":"This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n","permissions":["allow-open"]},"permissions":{"allow-execute":{"identifier":"allow-execute","description":"Enables the execute command without any pre-configured scope.","commands":{"allow":["execute"],"deny":[]}},"allow-kill":{"identifier":"allow-kill","description":"Enables the kill command without any pre-configured scope.","commands":{"allow":["kill"],"deny":[]}},"allow-open":{"identifier":"allow-open","description":"Enables the open command without any pre-configured scope.","commands":{"allow":["open"],"deny":[]}},"allow-spawn":{"identifier":"allow-spawn","description":"Enables the spawn command without any pre-configured scope.","commands":{"allow":["spawn"],"deny":[]}},"allow-stdin-write":{"identifier":"allow-stdin-write","description":"Enables the stdin_write command without any pre-configured scope.","commands":{"allow":["stdin_write"],"deny":[]}},"deny-execute":{"identifier":"deny-execute","description":"Denies the execute command without any pre-configured scope.","commands":{"allow":[],"deny":["execute"]}},"deny-kill":{"identifier":"deny-kill","description":"Denies the kill command without any pre-configured scope.","commands":{"allow":[],"deny":["kill"]}},"deny-open":{"identifier":"deny-open","description":"Denies the open command without any pre-configured scope.","commands":{"allow":[],"deny":["open"]}},"deny-spawn":{"identifier":"deny-spawn","description":"Denies the spawn command without any pre-configured scope.","commands":{"allow":[],"deny":["spawn"]}},"deny-stdin-write":{"identifier":"deny-stdin-write","description":"Denies the stdin_write command without any pre-configured scope.","commands":{"allow":[],"deny":["stdin_write"]}}},"permission_sets":{},"global_scope_schema":{"$schema":"http://json-schema.org/draft-07/schema#","anyOf":[{"additionalProperties":false,"properties":{"args":{"allOf":[{"$ref":"#/definitions/ShellScopeEntryAllowedArgs"}],"description":"The allowed arguments for the command execution."},"cmd":{"description":"The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.","type":"string"},"name":{"description":"The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.","type":"string"}},"required":["cmd","name"],"type":"object"},{"additionalProperties":false,"properties":{"args":{"allOf":[{"$ref":"#/definitions/ShellScopeEntryAllowedArgs"}],"description":"The allowed arguments for the command execution."},"name":{"description":"The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.","type":"string"},"sidecar":{"description":"If this command is a sidecar command.","type":"boolean"}},"required":["name","sidecar"],"type":"object"}],"definitions":{"ShellScopeEntryAllowedArg":{"anyOf":[{"description":"A non-configurable argument that is passed to the command in the order it was specified.","type":"string"},{"additionalProperties":false,"description":"A variable that is set while calling the command from the webview API.","properties":{"raw":{"default":false,"description":"Marks the validator as a raw regex, meaning the plugin should not make any modification at runtime.\n\nThis means the regex will not match on the entire string by default, which might be exploited if your regex allow unexpected input to be considered valid. When using this option, make sure your regex is correct.","type":"boolean"},"validator":{"description":"[regex] validator to require passed values to conform to an expected input.\n\nThis will require the argument value passed to this variable to match the `validator` regex before it will be executed.\n\nThe regex string is by default surrounded by `^...$` to match the full string. For example the `https?://\\w+` regex would be registered as `^https?://\\w+$`.\n\n[regex]: ","type":"string"}},"required":["validator"],"type":"object"}],"description":"A command argument allowed to be executed by the webview API."},"ShellScopeEntryAllowedArgs":{"anyOf":[{"description":"Use a simple boolean to allow all or disable all arguments to this command configuration.","type":"boolean"},{"description":"A specific set of [`ShellScopeEntryAllowedArg`] that are valid to call for the command configuration.","items":{"$ref":"#/definitions/ShellScopeEntryAllowedArg"},"type":"array"}],"description":"A set of command arguments allowed to be executed by the webview API.\n\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellScopeEntryAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration."}},"description":"Shell scope entry.","title":"ShellScopeEntry"}}} \ No newline at end of file +{ + "core": { + "default_permission": { + "identifier": "default", + "description": "Default core plugins set.", + "permissions": [ + "core:path:default", + "core:event:default", + "core:window:default", + "core:webview:default", + "core:app:default", + "core:image:default", + "core:resources:default", + "core:menu:default", + "core:tray:default" + ] + }, + "permissions": {}, + "permission_sets": {}, + "global_scope_schema": null + }, + "core:app": { + "default_permission": { + "identifier": "default", + "description": "Default permissions for the plugin.", + "permissions": [ + "allow-version", + "allow-name", + "allow-tauri-version", + "allow-identifier", + "allow-bundle-type", + "allow-register-listener", + "allow-remove-listener", + "allow-supports-multiple-windows" + ] + }, + "permissions": { + "allow-app-hide": { + "identifier": "allow-app-hide", + "description": "Enables the app_hide command without any pre-configured scope.", + "commands": { "allow": ["app_hide"], "deny": [] } + }, + "allow-app-show": { + "identifier": "allow-app-show", + "description": "Enables the app_show command without any pre-configured scope.", + "commands": { "allow": ["app_show"], "deny": [] } + }, + "allow-bundle-type": { + "identifier": "allow-bundle-type", + "description": "Enables the bundle_type command without any pre-configured scope.", + "commands": { "allow": ["bundle_type"], "deny": [] } + }, + "allow-default-window-icon": { + "identifier": "allow-default-window-icon", + "description": "Enables the default_window_icon command without any pre-configured scope.", + "commands": { "allow": ["default_window_icon"], "deny": [] } + }, + "allow-exit": { + "identifier": "allow-exit", + "description": "Enables the exit command without any pre-configured scope.", + "commands": { "allow": ["exit"], "deny": [] } + }, + "allow-fetch-data-store-identifiers": { + "identifier": "allow-fetch-data-store-identifiers", + "description": "Enables the fetch_data_store_identifiers command without any pre-configured scope.", + "commands": { "allow": ["fetch_data_store_identifiers"], "deny": [] } + }, + "allow-identifier": { + "identifier": "allow-identifier", + "description": "Enables the identifier command without any pre-configured scope.", + "commands": { "allow": ["identifier"], "deny": [] } + }, + "allow-name": { + "identifier": "allow-name", + "description": "Enables the name command without any pre-configured scope.", + "commands": { "allow": ["name"], "deny": [] } + }, + "allow-register-listener": { + "identifier": "allow-register-listener", + "description": "Enables the register_listener command without any pre-configured scope.", + "commands": { "allow": ["register_listener"], "deny": [] } + }, + "allow-remove-data-store": { + "identifier": "allow-remove-data-store", + "description": "Enables the remove_data_store command without any pre-configured scope.", + "commands": { "allow": ["remove_data_store"], "deny": [] } + }, + "allow-remove-listener": { + "identifier": "allow-remove-listener", + "description": "Enables the remove_listener command without any pre-configured scope.", + "commands": { "allow": ["remove_listener"], "deny": [] } + }, + "allow-set-app-theme": { + "identifier": "allow-set-app-theme", + "description": "Enables the set_app_theme command without any pre-configured scope.", + "commands": { "allow": ["set_app_theme"], "deny": [] } + }, + "allow-set-dock-visibility": { + "identifier": "allow-set-dock-visibility", + "description": "Enables the set_dock_visibility command without any pre-configured scope.", + "commands": { "allow": ["set_dock_visibility"], "deny": [] } + }, + "allow-supports-multiple-windows": { + "identifier": "allow-supports-multiple-windows", + "description": "Enables the supports_multiple_windows command without any pre-configured scope.", + "commands": { "allow": ["supports_multiple_windows"], "deny": [] } + }, + "allow-tauri-version": { + "identifier": "allow-tauri-version", + "description": "Enables the tauri_version command without any pre-configured scope.", + "commands": { "allow": ["tauri_version"], "deny": [] } + }, + "allow-version": { + "identifier": "allow-version", + "description": "Enables the version command without any pre-configured scope.", + "commands": { "allow": ["version"], "deny": [] } + }, + "deny-app-hide": { + "identifier": "deny-app-hide", + "description": "Denies the app_hide command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["app_hide"] } + }, + "deny-app-show": { + "identifier": "deny-app-show", + "description": "Denies the app_show command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["app_show"] } + }, + "deny-bundle-type": { + "identifier": "deny-bundle-type", + "description": "Denies the bundle_type command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["bundle_type"] } + }, + "deny-default-window-icon": { + "identifier": "deny-default-window-icon", + "description": "Denies the default_window_icon command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["default_window_icon"] } + }, + "deny-exit": { + "identifier": "deny-exit", + "description": "Denies the exit command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["exit"] } + }, + "deny-fetch-data-store-identifiers": { + "identifier": "deny-fetch-data-store-identifiers", + "description": "Denies the fetch_data_store_identifiers command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["fetch_data_store_identifiers"] } + }, + "deny-identifier": { + "identifier": "deny-identifier", + "description": "Denies the identifier command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["identifier"] } + }, + "deny-name": { + "identifier": "deny-name", + "description": "Denies the name command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["name"] } + }, + "deny-register-listener": { + "identifier": "deny-register-listener", + "description": "Denies the register_listener command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["register_listener"] } + }, + "deny-remove-data-store": { + "identifier": "deny-remove-data-store", + "description": "Denies the remove_data_store command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["remove_data_store"] } + }, + "deny-remove-listener": { + "identifier": "deny-remove-listener", + "description": "Denies the remove_listener command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["remove_listener"] } + }, + "deny-set-app-theme": { + "identifier": "deny-set-app-theme", + "description": "Denies the set_app_theme command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["set_app_theme"] } + }, + "deny-set-dock-visibility": { + "identifier": "deny-set-dock-visibility", + "description": "Denies the set_dock_visibility command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["set_dock_visibility"] } + }, + "deny-supports-multiple-windows": { + "identifier": "deny-supports-multiple-windows", + "description": "Denies the supports_multiple_windows command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["supports_multiple_windows"] } + }, + "deny-tauri-version": { + "identifier": "deny-tauri-version", + "description": "Denies the tauri_version command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["tauri_version"] } + }, + "deny-version": { + "identifier": "deny-version", + "description": "Denies the version command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["version"] } + } + }, + "permission_sets": {}, + "global_scope_schema": null + }, + "core:event": { + "default_permission": { + "identifier": "default", + "description": "Default permissions for the plugin, which enables all commands.", + "permissions": ["allow-listen", "allow-unlisten", "allow-emit", "allow-emit-to"] + }, + "permissions": { + "allow-emit": { + "identifier": "allow-emit", + "description": "Enables the emit command without any pre-configured scope.", + "commands": { "allow": ["emit"], "deny": [] } + }, + "allow-emit-to": { + "identifier": "allow-emit-to", + "description": "Enables the emit_to command without any pre-configured scope.", + "commands": { "allow": ["emit_to"], "deny": [] } + }, + "allow-listen": { + "identifier": "allow-listen", + "description": "Enables the listen command without any pre-configured scope.", + "commands": { "allow": ["listen"], "deny": [] } + }, + "allow-unlisten": { + "identifier": "allow-unlisten", + "description": "Enables the unlisten command without any pre-configured scope.", + "commands": { "allow": ["unlisten"], "deny": [] } + }, + "deny-emit": { + "identifier": "deny-emit", + "description": "Denies the emit command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["emit"] } + }, + "deny-emit-to": { + "identifier": "deny-emit-to", + "description": "Denies the emit_to command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["emit_to"] } + }, + "deny-listen": { + "identifier": "deny-listen", + "description": "Denies the listen command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["listen"] } + }, + "deny-unlisten": { + "identifier": "deny-unlisten", + "description": "Denies the unlisten command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["unlisten"] } + } + }, + "permission_sets": {}, + "global_scope_schema": null + }, + "core:image": { + "default_permission": { + "identifier": "default", + "description": "Default permissions for the plugin, which enables all commands.", + "permissions": [ + "allow-new", + "allow-from-bytes", + "allow-from-path", + "allow-rgba", + "allow-size" + ] + }, + "permissions": { + "allow-from-bytes": { + "identifier": "allow-from-bytes", + "description": "Enables the from_bytes command without any pre-configured scope.", + "commands": { "allow": ["from_bytes"], "deny": [] } + }, + "allow-from-path": { + "identifier": "allow-from-path", + "description": "Enables the from_path command without any pre-configured scope.", + "commands": { "allow": ["from_path"], "deny": [] } + }, + "allow-new": { + "identifier": "allow-new", + "description": "Enables the new command without any pre-configured scope.", + "commands": { "allow": ["new"], "deny": [] } + }, + "allow-rgba": { + "identifier": "allow-rgba", + "description": "Enables the rgba command without any pre-configured scope.", + "commands": { "allow": ["rgba"], "deny": [] } + }, + "allow-size": { + "identifier": "allow-size", + "description": "Enables the size command without any pre-configured scope.", + "commands": { "allow": ["size"], "deny": [] } + }, + "deny-from-bytes": { + "identifier": "deny-from-bytes", + "description": "Denies the from_bytes command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["from_bytes"] } + }, + "deny-from-path": { + "identifier": "deny-from-path", + "description": "Denies the from_path command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["from_path"] } + }, + "deny-new": { + "identifier": "deny-new", + "description": "Denies the new command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["new"] } + }, + "deny-rgba": { + "identifier": "deny-rgba", + "description": "Denies the rgba command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["rgba"] } + }, + "deny-size": { + "identifier": "deny-size", + "description": "Denies the size command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["size"] } + } + }, + "permission_sets": {}, + "global_scope_schema": null + }, + "core:menu": { + "default_permission": { + "identifier": "default", + "description": "Default permissions for the plugin, which enables all commands.", + "permissions": [ + "allow-new", + "allow-append", + "allow-prepend", + "allow-insert", + "allow-remove", + "allow-remove-at", + "allow-items", + "allow-get", + "allow-popup", + "allow-create-default", + "allow-set-as-app-menu", + "allow-set-as-window-menu", + "allow-text", + "allow-set-text", + "allow-is-enabled", + "allow-set-enabled", + "allow-set-accelerator", + "allow-set-as-windows-menu-for-nsapp", + "allow-set-as-help-menu-for-nsapp", + "allow-is-checked", + "allow-set-checked", + "allow-set-icon" + ] + }, + "permissions": { + "allow-append": { + "identifier": "allow-append", + "description": "Enables the append command without any pre-configured scope.", + "commands": { "allow": ["append"], "deny": [] } + }, + "allow-create-default": { + "identifier": "allow-create-default", + "description": "Enables the create_default command without any pre-configured scope.", + "commands": { "allow": ["create_default"], "deny": [] } + }, + "allow-get": { + "identifier": "allow-get", + "description": "Enables the get command without any pre-configured scope.", + "commands": { "allow": ["get"], "deny": [] } + }, + "allow-insert": { + "identifier": "allow-insert", + "description": "Enables the insert command without any pre-configured scope.", + "commands": { "allow": ["insert"], "deny": [] } + }, + "allow-is-checked": { + "identifier": "allow-is-checked", + "description": "Enables the is_checked command without any pre-configured scope.", + "commands": { "allow": ["is_checked"], "deny": [] } + }, + "allow-is-enabled": { + "identifier": "allow-is-enabled", + "description": "Enables the is_enabled command without any pre-configured scope.", + "commands": { "allow": ["is_enabled"], "deny": [] } + }, + "allow-items": { + "identifier": "allow-items", + "description": "Enables the items command without any pre-configured scope.", + "commands": { "allow": ["items"], "deny": [] } + }, + "allow-new": { + "identifier": "allow-new", + "description": "Enables the new command without any pre-configured scope.", + "commands": { "allow": ["new"], "deny": [] } + }, + "allow-popup": { + "identifier": "allow-popup", + "description": "Enables the popup command without any pre-configured scope.", + "commands": { "allow": ["popup"], "deny": [] } + }, + "allow-prepend": { + "identifier": "allow-prepend", + "description": "Enables the prepend command without any pre-configured scope.", + "commands": { "allow": ["prepend"], "deny": [] } + }, + "allow-remove": { + "identifier": "allow-remove", + "description": "Enables the remove command without any pre-configured scope.", + "commands": { "allow": ["remove"], "deny": [] } + }, + "allow-remove-at": { + "identifier": "allow-remove-at", + "description": "Enables the remove_at command without any pre-configured scope.", + "commands": { "allow": ["remove_at"], "deny": [] } + }, + "allow-set-accelerator": { + "identifier": "allow-set-accelerator", + "description": "Enables the set_accelerator command without any pre-configured scope.", + "commands": { "allow": ["set_accelerator"], "deny": [] } + }, + "allow-set-as-app-menu": { + "identifier": "allow-set-as-app-menu", + "description": "Enables the set_as_app_menu command without any pre-configured scope.", + "commands": { "allow": ["set_as_app_menu"], "deny": [] } + }, + "allow-set-as-help-menu-for-nsapp": { + "identifier": "allow-set-as-help-menu-for-nsapp", + "description": "Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.", + "commands": { "allow": ["set_as_help_menu_for_nsapp"], "deny": [] } + }, + "allow-set-as-window-menu": { + "identifier": "allow-set-as-window-menu", + "description": "Enables the set_as_window_menu command without any pre-configured scope.", + "commands": { "allow": ["set_as_window_menu"], "deny": [] } + }, + "allow-set-as-windows-menu-for-nsapp": { + "identifier": "allow-set-as-windows-menu-for-nsapp", + "description": "Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.", + "commands": { "allow": ["set_as_windows_menu_for_nsapp"], "deny": [] } + }, + "allow-set-checked": { + "identifier": "allow-set-checked", + "description": "Enables the set_checked command without any pre-configured scope.", + "commands": { "allow": ["set_checked"], "deny": [] } + }, + "allow-set-enabled": { + "identifier": "allow-set-enabled", + "description": "Enables the set_enabled command without any pre-configured scope.", + "commands": { "allow": ["set_enabled"], "deny": [] } + }, + "allow-set-icon": { + "identifier": "allow-set-icon", + "description": "Enables the set_icon command without any pre-configured scope.", + "commands": { "allow": ["set_icon"], "deny": [] } + }, + "allow-set-text": { + "identifier": "allow-set-text", + "description": "Enables the set_text command without any pre-configured scope.", + "commands": { "allow": ["set_text"], "deny": [] } + }, + "allow-text": { + "identifier": "allow-text", + "description": "Enables the text command without any pre-configured scope.", + "commands": { "allow": ["text"], "deny": [] } + }, + "deny-append": { + "identifier": "deny-append", + "description": "Denies the append command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["append"] } + }, + "deny-create-default": { + "identifier": "deny-create-default", + "description": "Denies the create_default command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["create_default"] } + }, + "deny-get": { + "identifier": "deny-get", + "description": "Denies the get command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["get"] } + }, + "deny-insert": { + "identifier": "deny-insert", + "description": "Denies the insert command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["insert"] } + }, + "deny-is-checked": { + "identifier": "deny-is-checked", + "description": "Denies the is_checked command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["is_checked"] } + }, + "deny-is-enabled": { + "identifier": "deny-is-enabled", + "description": "Denies the is_enabled command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["is_enabled"] } + }, + "deny-items": { + "identifier": "deny-items", + "description": "Denies the items command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["items"] } + }, + "deny-new": { + "identifier": "deny-new", + "description": "Denies the new command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["new"] } + }, + "deny-popup": { + "identifier": "deny-popup", + "description": "Denies the popup command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["popup"] } + }, + "deny-prepend": { + "identifier": "deny-prepend", + "description": "Denies the prepend command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["prepend"] } + }, + "deny-remove": { + "identifier": "deny-remove", + "description": "Denies the remove command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["remove"] } + }, + "deny-remove-at": { + "identifier": "deny-remove-at", + "description": "Denies the remove_at command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["remove_at"] } + }, + "deny-set-accelerator": { + "identifier": "deny-set-accelerator", + "description": "Denies the set_accelerator command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["set_accelerator"] } + }, + "deny-set-as-app-menu": { + "identifier": "deny-set-as-app-menu", + "description": "Denies the set_as_app_menu command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["set_as_app_menu"] } + }, + "deny-set-as-help-menu-for-nsapp": { + "identifier": "deny-set-as-help-menu-for-nsapp", + "description": "Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["set_as_help_menu_for_nsapp"] } + }, + "deny-set-as-window-menu": { + "identifier": "deny-set-as-window-menu", + "description": "Denies the set_as_window_menu command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["set_as_window_menu"] } + }, + "deny-set-as-windows-menu-for-nsapp": { + "identifier": "deny-set-as-windows-menu-for-nsapp", + "description": "Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["set_as_windows_menu_for_nsapp"] } + }, + "deny-set-checked": { + "identifier": "deny-set-checked", + "description": "Denies the set_checked command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["set_checked"] } + }, + "deny-set-enabled": { + "identifier": "deny-set-enabled", + "description": "Denies the set_enabled command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["set_enabled"] } + }, + "deny-set-icon": { + "identifier": "deny-set-icon", + "description": "Denies the set_icon command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["set_icon"] } + }, + "deny-set-text": { + "identifier": "deny-set-text", + "description": "Denies the set_text command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["set_text"] } + }, + "deny-text": { + "identifier": "deny-text", + "description": "Denies the text command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["text"] } + } + }, + "permission_sets": {}, + "global_scope_schema": null + }, + "core:path": { + "default_permission": { + "identifier": "default", + "description": "Default permissions for the plugin, which enables all commands.", + "permissions": [ + "allow-resolve-directory", + "allow-resolve", + "allow-normalize", + "allow-join", + "allow-dirname", + "allow-extname", + "allow-basename", + "allow-is-absolute" + ] + }, + "permissions": { + "allow-basename": { + "identifier": "allow-basename", + "description": "Enables the basename command without any pre-configured scope.", + "commands": { "allow": ["basename"], "deny": [] } + }, + "allow-dirname": { + "identifier": "allow-dirname", + "description": "Enables the dirname command without any pre-configured scope.", + "commands": { "allow": ["dirname"], "deny": [] } + }, + "allow-extname": { + "identifier": "allow-extname", + "description": "Enables the extname command without any pre-configured scope.", + "commands": { "allow": ["extname"], "deny": [] } + }, + "allow-is-absolute": { + "identifier": "allow-is-absolute", + "description": "Enables the is_absolute command without any pre-configured scope.", + "commands": { "allow": ["is_absolute"], "deny": [] } + }, + "allow-join": { + "identifier": "allow-join", + "description": "Enables the join command without any pre-configured scope.", + "commands": { "allow": ["join"], "deny": [] } + }, + "allow-normalize": { + "identifier": "allow-normalize", + "description": "Enables the normalize command without any pre-configured scope.", + "commands": { "allow": ["normalize"], "deny": [] } + }, + "allow-resolve": { + "identifier": "allow-resolve", + "description": "Enables the resolve command without any pre-configured scope.", + "commands": { "allow": ["resolve"], "deny": [] } + }, + "allow-resolve-directory": { + "identifier": "allow-resolve-directory", + "description": "Enables the resolve_directory command without any pre-configured scope.", + "commands": { "allow": ["resolve_directory"], "deny": [] } + }, + "deny-basename": { + "identifier": "deny-basename", + "description": "Denies the basename command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["basename"] } + }, + "deny-dirname": { + "identifier": "deny-dirname", + "description": "Denies the dirname command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["dirname"] } + }, + "deny-extname": { + "identifier": "deny-extname", + "description": "Denies the extname command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["extname"] } + }, + "deny-is-absolute": { + "identifier": "deny-is-absolute", + "description": "Denies the is_absolute command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["is_absolute"] } + }, + "deny-join": { + "identifier": "deny-join", + "description": "Denies the join command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["join"] } + }, + "deny-normalize": { + "identifier": "deny-normalize", + "description": "Denies the normalize command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["normalize"] } + }, + "deny-resolve": { + "identifier": "deny-resolve", + "description": "Denies the resolve command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["resolve"] } + }, + "deny-resolve-directory": { + "identifier": "deny-resolve-directory", + "description": "Denies the resolve_directory command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["resolve_directory"] } + } + }, + "permission_sets": {}, + "global_scope_schema": null + }, + "core:resources": { + "default_permission": { + "identifier": "default", + "description": "Default permissions for the plugin, which enables all commands.", + "permissions": ["allow-close"] + }, + "permissions": { + "allow-close": { + "identifier": "allow-close", + "description": "Enables the close command without any pre-configured scope.", + "commands": { "allow": ["close"], "deny": [] } + }, + "deny-close": { + "identifier": "deny-close", + "description": "Denies the close command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["close"] } + } + }, + "permission_sets": {}, + "global_scope_schema": null + }, + "core:tray": { + "default_permission": { + "identifier": "default", + "description": "Default permissions for the plugin, which enables all commands.", + "permissions": [ + "allow-new", + "allow-get-by-id", + "allow-remove-by-id", + "allow-set-icon", + "allow-set-menu", + "allow-set-tooltip", + "allow-set-title", + "allow-set-visible", + "allow-set-temp-dir-path", + "allow-set-icon-as-template", + "allow-set-icon-with-as-template", + "allow-set-show-menu-on-left-click" + ] + }, + "permissions": { + "allow-get-by-id": { + "identifier": "allow-get-by-id", + "description": "Enables the get_by_id command without any pre-configured scope.", + "commands": { "allow": ["get_by_id"], "deny": [] } + }, + "allow-new": { + "identifier": "allow-new", + "description": "Enables the new command without any pre-configured scope.", + "commands": { "allow": ["new"], "deny": [] } + }, + "allow-remove-by-id": { + "identifier": "allow-remove-by-id", + "description": "Enables the remove_by_id command without any pre-configured scope.", + "commands": { "allow": ["remove_by_id"], "deny": [] } + }, + "allow-set-icon": { + "identifier": "allow-set-icon", + "description": "Enables the set_icon command without any pre-configured scope.", + "commands": { "allow": ["set_icon"], "deny": [] } + }, + "allow-set-icon-as-template": { + "identifier": "allow-set-icon-as-template", + "description": "Enables the set_icon_as_template command without any pre-configured scope.", + "commands": { "allow": ["set_icon_as_template"], "deny": [] } + }, + "allow-set-icon-with-as-template": { + "identifier": "allow-set-icon-with-as-template", + "description": "Enables the set_icon_with_as_template command without any pre-configured scope.", + "commands": { "allow": ["set_icon_with_as_template"], "deny": [] } + }, + "allow-set-menu": { + "identifier": "allow-set-menu", + "description": "Enables the set_menu command without any pre-configured scope.", + "commands": { "allow": ["set_menu"], "deny": [] } + }, + "allow-set-show-menu-on-left-click": { + "identifier": "allow-set-show-menu-on-left-click", + "description": "Enables the set_show_menu_on_left_click command without any pre-configured scope.", + "commands": { "allow": ["set_show_menu_on_left_click"], "deny": [] } + }, + "allow-set-temp-dir-path": { + "identifier": "allow-set-temp-dir-path", + "description": "Enables the set_temp_dir_path command without any pre-configured scope.", + "commands": { "allow": ["set_temp_dir_path"], "deny": [] } + }, + "allow-set-title": { + "identifier": "allow-set-title", + "description": "Enables the set_title command without any pre-configured scope.", + "commands": { "allow": ["set_title"], "deny": [] } + }, + "allow-set-tooltip": { + "identifier": "allow-set-tooltip", + "description": "Enables the set_tooltip command without any pre-configured scope.", + "commands": { "allow": ["set_tooltip"], "deny": [] } + }, + "allow-set-visible": { + "identifier": "allow-set-visible", + "description": "Enables the set_visible command without any pre-configured scope.", + "commands": { "allow": ["set_visible"], "deny": [] } + }, + "deny-get-by-id": { + "identifier": "deny-get-by-id", + "description": "Denies the get_by_id command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["get_by_id"] } + }, + "deny-new": { + "identifier": "deny-new", + "description": "Denies the new command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["new"] } + }, + "deny-remove-by-id": { + "identifier": "deny-remove-by-id", + "description": "Denies the remove_by_id command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["remove_by_id"] } + }, + "deny-set-icon": { + "identifier": "deny-set-icon", + "description": "Denies the set_icon command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["set_icon"] } + }, + "deny-set-icon-as-template": { + "identifier": "deny-set-icon-as-template", + "description": "Denies the set_icon_as_template command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["set_icon_as_template"] } + }, + "deny-set-icon-with-as-template": { + "identifier": "deny-set-icon-with-as-template", + "description": "Denies the set_icon_with_as_template command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["set_icon_with_as_template"] } + }, + "deny-set-menu": { + "identifier": "deny-set-menu", + "description": "Denies the set_menu command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["set_menu"] } + }, + "deny-set-show-menu-on-left-click": { + "identifier": "deny-set-show-menu-on-left-click", + "description": "Denies the set_show_menu_on_left_click command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["set_show_menu_on_left_click"] } + }, + "deny-set-temp-dir-path": { + "identifier": "deny-set-temp-dir-path", + "description": "Denies the set_temp_dir_path command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["set_temp_dir_path"] } + }, + "deny-set-title": { + "identifier": "deny-set-title", + "description": "Denies the set_title command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["set_title"] } + }, + "deny-set-tooltip": { + "identifier": "deny-set-tooltip", + "description": "Denies the set_tooltip command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["set_tooltip"] } + }, + "deny-set-visible": { + "identifier": "deny-set-visible", + "description": "Denies the set_visible command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["set_visible"] } + } + }, + "permission_sets": {}, + "global_scope_schema": null + }, + "core:webview": { + "default_permission": { + "identifier": "default", + "description": "Default permissions for the plugin.", + "permissions": [ + "allow-get-all-webviews", + "allow-webview-position", + "allow-webview-size", + "allow-internal-toggle-devtools" + ] + }, + "permissions": { + "allow-clear-all-browsing-data": { + "identifier": "allow-clear-all-browsing-data", + "description": "Enables the clear_all_browsing_data command without any pre-configured scope.", + "commands": { "allow": ["clear_all_browsing_data"], "deny": [] } + }, + "allow-create-webview": { + "identifier": "allow-create-webview", + "description": "Enables the create_webview command without any pre-configured scope.", + "commands": { "allow": ["create_webview"], "deny": [] } + }, + "allow-create-webview-window": { + "identifier": "allow-create-webview-window", + "description": "Enables the create_webview_window command without any pre-configured scope.", + "commands": { "allow": ["create_webview_window"], "deny": [] } + }, + "allow-get-all-webviews": { + "identifier": "allow-get-all-webviews", + "description": "Enables the get_all_webviews command without any pre-configured scope.", + "commands": { "allow": ["get_all_webviews"], "deny": [] } + }, + "allow-internal-toggle-devtools": { + "identifier": "allow-internal-toggle-devtools", + "description": "Enables the internal_toggle_devtools command without any pre-configured scope.", + "commands": { "allow": ["internal_toggle_devtools"], "deny": [] } + }, + "allow-print": { + "identifier": "allow-print", + "description": "Enables the print command without any pre-configured scope.", + "commands": { "allow": ["print"], "deny": [] } + }, + "allow-reparent": { + "identifier": "allow-reparent", + "description": "Enables the reparent command without any pre-configured scope.", + "commands": { "allow": ["reparent"], "deny": [] } + }, + "allow-set-webview-auto-resize": { + "identifier": "allow-set-webview-auto-resize", + "description": "Enables the set_webview_auto_resize command without any pre-configured scope.", + "commands": { "allow": ["set_webview_auto_resize"], "deny": [] } + }, + "allow-set-webview-background-color": { + "identifier": "allow-set-webview-background-color", + "description": "Enables the set_webview_background_color command without any pre-configured scope.", + "commands": { "allow": ["set_webview_background_color"], "deny": [] } + }, + "allow-set-webview-focus": { + "identifier": "allow-set-webview-focus", + "description": "Enables the set_webview_focus command without any pre-configured scope.", + "commands": { "allow": ["set_webview_focus"], "deny": [] } + }, + "allow-set-webview-position": { + "identifier": "allow-set-webview-position", + "description": "Enables the set_webview_position command without any pre-configured scope.", + "commands": { "allow": ["set_webview_position"], "deny": [] } + }, + "allow-set-webview-size": { + "identifier": "allow-set-webview-size", + "description": "Enables the set_webview_size command without any pre-configured scope.", + "commands": { "allow": ["set_webview_size"], "deny": [] } + }, + "allow-set-webview-zoom": { + "identifier": "allow-set-webview-zoom", + "description": "Enables the set_webview_zoom command without any pre-configured scope.", + "commands": { "allow": ["set_webview_zoom"], "deny": [] } + }, + "allow-webview-close": { + "identifier": "allow-webview-close", + "description": "Enables the webview_close command without any pre-configured scope.", + "commands": { "allow": ["webview_close"], "deny": [] } + }, + "allow-webview-hide": { + "identifier": "allow-webview-hide", + "description": "Enables the webview_hide command without any pre-configured scope.", + "commands": { "allow": ["webview_hide"], "deny": [] } + }, + "allow-webview-position": { + "identifier": "allow-webview-position", + "description": "Enables the webview_position command without any pre-configured scope.", + "commands": { "allow": ["webview_position"], "deny": [] } + }, + "allow-webview-show": { + "identifier": "allow-webview-show", + "description": "Enables the webview_show command without any pre-configured scope.", + "commands": { "allow": ["webview_show"], "deny": [] } + }, + "allow-webview-size": { + "identifier": "allow-webview-size", + "description": "Enables the webview_size command without any pre-configured scope.", + "commands": { "allow": ["webview_size"], "deny": [] } + }, + "deny-clear-all-browsing-data": { + "identifier": "deny-clear-all-browsing-data", + "description": "Denies the clear_all_browsing_data command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["clear_all_browsing_data"] } + }, + "deny-create-webview": { + "identifier": "deny-create-webview", + "description": "Denies the create_webview command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["create_webview"] } + }, + "deny-create-webview-window": { + "identifier": "deny-create-webview-window", + "description": "Denies the create_webview_window command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["create_webview_window"] } + }, + "deny-get-all-webviews": { + "identifier": "deny-get-all-webviews", + "description": "Denies the get_all_webviews command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["get_all_webviews"] } + }, + "deny-internal-toggle-devtools": { + "identifier": "deny-internal-toggle-devtools", + "description": "Denies the internal_toggle_devtools command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["internal_toggle_devtools"] } + }, + "deny-print": { + "identifier": "deny-print", + "description": "Denies the print command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["print"] } + }, + "deny-reparent": { + "identifier": "deny-reparent", + "description": "Denies the reparent command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["reparent"] } + }, + "deny-set-webview-auto-resize": { + "identifier": "deny-set-webview-auto-resize", + "description": "Denies the set_webview_auto_resize command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["set_webview_auto_resize"] } + }, + "deny-set-webview-background-color": { + "identifier": "deny-set-webview-background-color", + "description": "Denies the set_webview_background_color command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["set_webview_background_color"] } + }, + "deny-set-webview-focus": { + "identifier": "deny-set-webview-focus", + "description": "Denies the set_webview_focus command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["set_webview_focus"] } + }, + "deny-set-webview-position": { + "identifier": "deny-set-webview-position", + "description": "Denies the set_webview_position command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["set_webview_position"] } + }, + "deny-set-webview-size": { + "identifier": "deny-set-webview-size", + "description": "Denies the set_webview_size command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["set_webview_size"] } + }, + "deny-set-webview-zoom": { + "identifier": "deny-set-webview-zoom", + "description": "Denies the set_webview_zoom command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["set_webview_zoom"] } + }, + "deny-webview-close": { + "identifier": "deny-webview-close", + "description": "Denies the webview_close command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["webview_close"] } + }, + "deny-webview-hide": { + "identifier": "deny-webview-hide", + "description": "Denies the webview_hide command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["webview_hide"] } + }, + "deny-webview-position": { + "identifier": "deny-webview-position", + "description": "Denies the webview_position command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["webview_position"] } + }, + "deny-webview-show": { + "identifier": "deny-webview-show", + "description": "Denies the webview_show command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["webview_show"] } + }, + "deny-webview-size": { + "identifier": "deny-webview-size", + "description": "Denies the webview_size command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["webview_size"] } + } + }, + "permission_sets": {}, + "global_scope_schema": null + }, + "core:window": { + "default_permission": { + "identifier": "default", + "description": "Default permissions for the plugin.", + "permissions": [ + "allow-get-all-windows", + "allow-scale-factor", + "allow-inner-position", + "allow-outer-position", + "allow-inner-size", + "allow-outer-size", + "allow-is-fullscreen", + "allow-is-minimized", + "allow-is-maximized", + "allow-is-focused", + "allow-is-decorated", + "allow-is-resizable", + "allow-is-maximizable", + "allow-is-minimizable", + "allow-is-closable", + "allow-is-visible", + "allow-is-enabled", + "allow-title", + "allow-current-monitor", + "allow-primary-monitor", + "allow-monitor-from-point", + "allow-available-monitors", + "allow-cursor-position", + "allow-theme", + "allow-is-always-on-top", + "allow-activity-name", + "allow-scene-identifier", + "allow-internal-toggle-maximize" + ] + }, + "permissions": { + "allow-activity-name": { + "identifier": "allow-activity-name", + "description": "Enables the activity_name command without any pre-configured scope.", + "commands": { "allow": ["activity_name"], "deny": [] } + }, + "allow-available-monitors": { + "identifier": "allow-available-monitors", + "description": "Enables the available_monitors command without any pre-configured scope.", + "commands": { "allow": ["available_monitors"], "deny": [] } + }, + "allow-center": { + "identifier": "allow-center", + "description": "Enables the center command without any pre-configured scope.", + "commands": { "allow": ["center"], "deny": [] } + }, + "allow-close": { + "identifier": "allow-close", + "description": "Enables the close command without any pre-configured scope.", + "commands": { "allow": ["close"], "deny": [] } + }, + "allow-create": { + "identifier": "allow-create", + "description": "Enables the create command without any pre-configured scope.", + "commands": { "allow": ["create"], "deny": [] } + }, + "allow-current-monitor": { + "identifier": "allow-current-monitor", + "description": "Enables the current_monitor command without any pre-configured scope.", + "commands": { "allow": ["current_monitor"], "deny": [] } + }, + "allow-cursor-position": { + "identifier": "allow-cursor-position", + "description": "Enables the cursor_position command without any pre-configured scope.", + "commands": { "allow": ["cursor_position"], "deny": [] } + }, + "allow-destroy": { + "identifier": "allow-destroy", + "description": "Enables the destroy command without any pre-configured scope.", + "commands": { "allow": ["destroy"], "deny": [] } + }, + "allow-get-all-windows": { + "identifier": "allow-get-all-windows", + "description": "Enables the get_all_windows command without any pre-configured scope.", + "commands": { "allow": ["get_all_windows"], "deny": [] } + }, + "allow-hide": { + "identifier": "allow-hide", + "description": "Enables the hide command without any pre-configured scope.", + "commands": { "allow": ["hide"], "deny": [] } + }, + "allow-inner-position": { + "identifier": "allow-inner-position", + "description": "Enables the inner_position command without any pre-configured scope.", + "commands": { "allow": ["inner_position"], "deny": [] } + }, + "allow-inner-size": { + "identifier": "allow-inner-size", + "description": "Enables the inner_size command without any pre-configured scope.", + "commands": { "allow": ["inner_size"], "deny": [] } + }, + "allow-internal-toggle-maximize": { + "identifier": "allow-internal-toggle-maximize", + "description": "Enables the internal_toggle_maximize command without any pre-configured scope.", + "commands": { "allow": ["internal_toggle_maximize"], "deny": [] } + }, + "allow-is-always-on-top": { + "identifier": "allow-is-always-on-top", + "description": "Enables the is_always_on_top command without any pre-configured scope.", + "commands": { "allow": ["is_always_on_top"], "deny": [] } + }, + "allow-is-closable": { + "identifier": "allow-is-closable", + "description": "Enables the is_closable command without any pre-configured scope.", + "commands": { "allow": ["is_closable"], "deny": [] } + }, + "allow-is-decorated": { + "identifier": "allow-is-decorated", + "description": "Enables the is_decorated command without any pre-configured scope.", + "commands": { "allow": ["is_decorated"], "deny": [] } + }, + "allow-is-enabled": { + "identifier": "allow-is-enabled", + "description": "Enables the is_enabled command without any pre-configured scope.", + "commands": { "allow": ["is_enabled"], "deny": [] } + }, + "allow-is-focused": { + "identifier": "allow-is-focused", + "description": "Enables the is_focused command without any pre-configured scope.", + "commands": { "allow": ["is_focused"], "deny": [] } + }, + "allow-is-fullscreen": { + "identifier": "allow-is-fullscreen", + "description": "Enables the is_fullscreen command without any pre-configured scope.", + "commands": { "allow": ["is_fullscreen"], "deny": [] } + }, + "allow-is-maximizable": { + "identifier": "allow-is-maximizable", + "description": "Enables the is_maximizable command without any pre-configured scope.", + "commands": { "allow": ["is_maximizable"], "deny": [] } + }, + "allow-is-maximized": { + "identifier": "allow-is-maximized", + "description": "Enables the is_maximized command without any pre-configured scope.", + "commands": { "allow": ["is_maximized"], "deny": [] } + }, + "allow-is-minimizable": { + "identifier": "allow-is-minimizable", + "description": "Enables the is_minimizable command without any pre-configured scope.", + "commands": { "allow": ["is_minimizable"], "deny": [] } + }, + "allow-is-minimized": { + "identifier": "allow-is-minimized", + "description": "Enables the is_minimized command without any pre-configured scope.", + "commands": { "allow": ["is_minimized"], "deny": [] } + }, + "allow-is-resizable": { + "identifier": "allow-is-resizable", + "description": "Enables the is_resizable command without any pre-configured scope.", + "commands": { "allow": ["is_resizable"], "deny": [] } + }, + "allow-is-visible": { + "identifier": "allow-is-visible", + "description": "Enables the is_visible command without any pre-configured scope.", + "commands": { "allow": ["is_visible"], "deny": [] } + }, + "allow-maximize": { + "identifier": "allow-maximize", + "description": "Enables the maximize command without any pre-configured scope.", + "commands": { "allow": ["maximize"], "deny": [] } + }, + "allow-minimize": { + "identifier": "allow-minimize", + "description": "Enables the minimize command without any pre-configured scope.", + "commands": { "allow": ["minimize"], "deny": [] } + }, + "allow-monitor-from-point": { + "identifier": "allow-monitor-from-point", + "description": "Enables the monitor_from_point command without any pre-configured scope.", + "commands": { "allow": ["monitor_from_point"], "deny": [] } + }, + "allow-outer-position": { + "identifier": "allow-outer-position", + "description": "Enables the outer_position command without any pre-configured scope.", + "commands": { "allow": ["outer_position"], "deny": [] } + }, + "allow-outer-size": { + "identifier": "allow-outer-size", + "description": "Enables the outer_size command without any pre-configured scope.", + "commands": { "allow": ["outer_size"], "deny": [] } + }, + "allow-primary-monitor": { + "identifier": "allow-primary-monitor", + "description": "Enables the primary_monitor command without any pre-configured scope.", + "commands": { "allow": ["primary_monitor"], "deny": [] } + }, + "allow-request-user-attention": { + "identifier": "allow-request-user-attention", + "description": "Enables the request_user_attention command without any pre-configured scope.", + "commands": { "allow": ["request_user_attention"], "deny": [] } + }, + "allow-scale-factor": { + "identifier": "allow-scale-factor", + "description": "Enables the scale_factor command without any pre-configured scope.", + "commands": { "allow": ["scale_factor"], "deny": [] } + }, + "allow-scene-identifier": { + "identifier": "allow-scene-identifier", + "description": "Enables the scene_identifier command without any pre-configured scope.", + "commands": { "allow": ["scene_identifier"], "deny": [] } + }, + "allow-set-always-on-bottom": { + "identifier": "allow-set-always-on-bottom", + "description": "Enables the set_always_on_bottom command without any pre-configured scope.", + "commands": { "allow": ["set_always_on_bottom"], "deny": [] } + }, + "allow-set-always-on-top": { + "identifier": "allow-set-always-on-top", + "description": "Enables the set_always_on_top command without any pre-configured scope.", + "commands": { "allow": ["set_always_on_top"], "deny": [] } + }, + "allow-set-background-color": { + "identifier": "allow-set-background-color", + "description": "Enables the set_background_color command without any pre-configured scope.", + "commands": { "allow": ["set_background_color"], "deny": [] } + }, + "allow-set-badge-count": { + "identifier": "allow-set-badge-count", + "description": "Enables the set_badge_count command without any pre-configured scope.", + "commands": { "allow": ["set_badge_count"], "deny": [] } + }, + "allow-set-badge-label": { + "identifier": "allow-set-badge-label", + "description": "Enables the set_badge_label command without any pre-configured scope.", + "commands": { "allow": ["set_badge_label"], "deny": [] } + }, + "allow-set-closable": { + "identifier": "allow-set-closable", + "description": "Enables the set_closable command without any pre-configured scope.", + "commands": { "allow": ["set_closable"], "deny": [] } + }, + "allow-set-content-protected": { + "identifier": "allow-set-content-protected", + "description": "Enables the set_content_protected command without any pre-configured scope.", + "commands": { "allow": ["set_content_protected"], "deny": [] } + }, + "allow-set-cursor-grab": { + "identifier": "allow-set-cursor-grab", + "description": "Enables the set_cursor_grab command without any pre-configured scope.", + "commands": { "allow": ["set_cursor_grab"], "deny": [] } + }, + "allow-set-cursor-icon": { + "identifier": "allow-set-cursor-icon", + "description": "Enables the set_cursor_icon command without any pre-configured scope.", + "commands": { "allow": ["set_cursor_icon"], "deny": [] } + }, + "allow-set-cursor-position": { + "identifier": "allow-set-cursor-position", + "description": "Enables the set_cursor_position command without any pre-configured scope.", + "commands": { "allow": ["set_cursor_position"], "deny": [] } + }, + "allow-set-cursor-visible": { + "identifier": "allow-set-cursor-visible", + "description": "Enables the set_cursor_visible command without any pre-configured scope.", + "commands": { "allow": ["set_cursor_visible"], "deny": [] } + }, + "allow-set-decorations": { + "identifier": "allow-set-decorations", + "description": "Enables the set_decorations command without any pre-configured scope.", + "commands": { "allow": ["set_decorations"], "deny": [] } + }, + "allow-set-effects": { + "identifier": "allow-set-effects", + "description": "Enables the set_effects command without any pre-configured scope.", + "commands": { "allow": ["set_effects"], "deny": [] } + }, + "allow-set-enabled": { + "identifier": "allow-set-enabled", + "description": "Enables the set_enabled command without any pre-configured scope.", + "commands": { "allow": ["set_enabled"], "deny": [] } + }, + "allow-set-focus": { + "identifier": "allow-set-focus", + "description": "Enables the set_focus command without any pre-configured scope.", + "commands": { "allow": ["set_focus"], "deny": [] } + }, + "allow-set-focusable": { + "identifier": "allow-set-focusable", + "description": "Enables the set_focusable command without any pre-configured scope.", + "commands": { "allow": ["set_focusable"], "deny": [] } + }, + "allow-set-fullscreen": { + "identifier": "allow-set-fullscreen", + "description": "Enables the set_fullscreen command without any pre-configured scope.", + "commands": { "allow": ["set_fullscreen"], "deny": [] } + }, + "allow-set-fullscreen-on-monitor": { + "identifier": "allow-set-fullscreen-on-monitor", + "description": "Enables the set_fullscreen_on_monitor command without any pre-configured scope.", + "commands": { "allow": ["set_fullscreen_on_monitor"], "deny": [] } + }, + "allow-set-icon": { + "identifier": "allow-set-icon", + "description": "Enables the set_icon command without any pre-configured scope.", + "commands": { "allow": ["set_icon"], "deny": [] } + }, + "allow-set-ignore-cursor-events": { + "identifier": "allow-set-ignore-cursor-events", + "description": "Enables the set_ignore_cursor_events command without any pre-configured scope.", + "commands": { "allow": ["set_ignore_cursor_events"], "deny": [] } + }, + "allow-set-max-size": { + "identifier": "allow-set-max-size", + "description": "Enables the set_max_size command without any pre-configured scope.", + "commands": { "allow": ["set_max_size"], "deny": [] } + }, + "allow-set-maximizable": { + "identifier": "allow-set-maximizable", + "description": "Enables the set_maximizable command without any pre-configured scope.", + "commands": { "allow": ["set_maximizable"], "deny": [] } + }, + "allow-set-min-size": { + "identifier": "allow-set-min-size", + "description": "Enables the set_min_size command without any pre-configured scope.", + "commands": { "allow": ["set_min_size"], "deny": [] } + }, + "allow-set-minimizable": { + "identifier": "allow-set-minimizable", + "description": "Enables the set_minimizable command without any pre-configured scope.", + "commands": { "allow": ["set_minimizable"], "deny": [] } + }, + "allow-set-overlay-icon": { + "identifier": "allow-set-overlay-icon", + "description": "Enables the set_overlay_icon command without any pre-configured scope.", + "commands": { "allow": ["set_overlay_icon"], "deny": [] } + }, + "allow-set-position": { + "identifier": "allow-set-position", + "description": "Enables the set_position command without any pre-configured scope.", + "commands": { "allow": ["set_position"], "deny": [] } + }, + "allow-set-progress-bar": { + "identifier": "allow-set-progress-bar", + "description": "Enables the set_progress_bar command without any pre-configured scope.", + "commands": { "allow": ["set_progress_bar"], "deny": [] } + }, + "allow-set-resizable": { + "identifier": "allow-set-resizable", + "description": "Enables the set_resizable command without any pre-configured scope.", + "commands": { "allow": ["set_resizable"], "deny": [] } + }, + "allow-set-shadow": { + "identifier": "allow-set-shadow", + "description": "Enables the set_shadow command without any pre-configured scope.", + "commands": { "allow": ["set_shadow"], "deny": [] } + }, + "allow-set-simple-fullscreen": { + "identifier": "allow-set-simple-fullscreen", + "description": "Enables the set_simple_fullscreen command without any pre-configured scope.", + "commands": { "allow": ["set_simple_fullscreen"], "deny": [] } + }, + "allow-set-size": { + "identifier": "allow-set-size", + "description": "Enables the set_size command without any pre-configured scope.", + "commands": { "allow": ["set_size"], "deny": [] } + }, + "allow-set-size-constraints": { + "identifier": "allow-set-size-constraints", + "description": "Enables the set_size_constraints command without any pre-configured scope.", + "commands": { "allow": ["set_size_constraints"], "deny": [] } + }, + "allow-set-skip-taskbar": { + "identifier": "allow-set-skip-taskbar", + "description": "Enables the set_skip_taskbar command without any pre-configured scope.", + "commands": { "allow": ["set_skip_taskbar"], "deny": [] } + }, + "allow-set-theme": { + "identifier": "allow-set-theme", + "description": "Enables the set_theme command without any pre-configured scope.", + "commands": { "allow": ["set_theme"], "deny": [] } + }, + "allow-set-title": { + "identifier": "allow-set-title", + "description": "Enables the set_title command without any pre-configured scope.", + "commands": { "allow": ["set_title"], "deny": [] } + }, + "allow-set-title-bar-style": { + "identifier": "allow-set-title-bar-style", + "description": "Enables the set_title_bar_style command without any pre-configured scope.", + "commands": { "allow": ["set_title_bar_style"], "deny": [] } + }, + "allow-set-visible-on-all-workspaces": { + "identifier": "allow-set-visible-on-all-workspaces", + "description": "Enables the set_visible_on_all_workspaces command without any pre-configured scope.", + "commands": { "allow": ["set_visible_on_all_workspaces"], "deny": [] } + }, + "allow-show": { + "identifier": "allow-show", + "description": "Enables the show command without any pre-configured scope.", + "commands": { "allow": ["show"], "deny": [] } + }, + "allow-start-dragging": { + "identifier": "allow-start-dragging", + "description": "Enables the start_dragging command without any pre-configured scope.", + "commands": { "allow": ["start_dragging"], "deny": [] } + }, + "allow-start-resize-dragging": { + "identifier": "allow-start-resize-dragging", + "description": "Enables the start_resize_dragging command without any pre-configured scope.", + "commands": { "allow": ["start_resize_dragging"], "deny": [] } + }, + "allow-theme": { + "identifier": "allow-theme", + "description": "Enables the theme command without any pre-configured scope.", + "commands": { "allow": ["theme"], "deny": [] } + }, + "allow-title": { + "identifier": "allow-title", + "description": "Enables the title command without any pre-configured scope.", + "commands": { "allow": ["title"], "deny": [] } + }, + "allow-toggle-maximize": { + "identifier": "allow-toggle-maximize", + "description": "Enables the toggle_maximize command without any pre-configured scope.", + "commands": { "allow": ["toggle_maximize"], "deny": [] } + }, + "allow-unmaximize": { + "identifier": "allow-unmaximize", + "description": "Enables the unmaximize command without any pre-configured scope.", + "commands": { "allow": ["unmaximize"], "deny": [] } + }, + "allow-unminimize": { + "identifier": "allow-unminimize", + "description": "Enables the unminimize command without any pre-configured scope.", + "commands": { "allow": ["unminimize"], "deny": [] } + }, + "deny-activity-name": { + "identifier": "deny-activity-name", + "description": "Denies the activity_name command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["activity_name"] } + }, + "deny-available-monitors": { + "identifier": "deny-available-monitors", + "description": "Denies the available_monitors command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["available_monitors"] } + }, + "deny-center": { + "identifier": "deny-center", + "description": "Denies the center command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["center"] } + }, + "deny-close": { + "identifier": "deny-close", + "description": "Denies the close command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["close"] } + }, + "deny-create": { + "identifier": "deny-create", + "description": "Denies the create command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["create"] } + }, + "deny-current-monitor": { + "identifier": "deny-current-monitor", + "description": "Denies the current_monitor command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["current_monitor"] } + }, + "deny-cursor-position": { + "identifier": "deny-cursor-position", + "description": "Denies the cursor_position command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["cursor_position"] } + }, + "deny-destroy": { + "identifier": "deny-destroy", + "description": "Denies the destroy command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["destroy"] } + }, + "deny-get-all-windows": { + "identifier": "deny-get-all-windows", + "description": "Denies the get_all_windows command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["get_all_windows"] } + }, + "deny-hide": { + "identifier": "deny-hide", + "description": "Denies the hide command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["hide"] } + }, + "deny-inner-position": { + "identifier": "deny-inner-position", + "description": "Denies the inner_position command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["inner_position"] } + }, + "deny-inner-size": { + "identifier": "deny-inner-size", + "description": "Denies the inner_size command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["inner_size"] } + }, + "deny-internal-toggle-maximize": { + "identifier": "deny-internal-toggle-maximize", + "description": "Denies the internal_toggle_maximize command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["internal_toggle_maximize"] } + }, + "deny-is-always-on-top": { + "identifier": "deny-is-always-on-top", + "description": "Denies the is_always_on_top command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["is_always_on_top"] } + }, + "deny-is-closable": { + "identifier": "deny-is-closable", + "description": "Denies the is_closable command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["is_closable"] } + }, + "deny-is-decorated": { + "identifier": "deny-is-decorated", + "description": "Denies the is_decorated command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["is_decorated"] } + }, + "deny-is-enabled": { + "identifier": "deny-is-enabled", + "description": "Denies the is_enabled command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["is_enabled"] } + }, + "deny-is-focused": { + "identifier": "deny-is-focused", + "description": "Denies the is_focused command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["is_focused"] } + }, + "deny-is-fullscreen": { + "identifier": "deny-is-fullscreen", + "description": "Denies the is_fullscreen command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["is_fullscreen"] } + }, + "deny-is-maximizable": { + "identifier": "deny-is-maximizable", + "description": "Denies the is_maximizable command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["is_maximizable"] } + }, + "deny-is-maximized": { + "identifier": "deny-is-maximized", + "description": "Denies the is_maximized command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["is_maximized"] } + }, + "deny-is-minimizable": { + "identifier": "deny-is-minimizable", + "description": "Denies the is_minimizable command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["is_minimizable"] } + }, + "deny-is-minimized": { + "identifier": "deny-is-minimized", + "description": "Denies the is_minimized command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["is_minimized"] } + }, + "deny-is-resizable": { + "identifier": "deny-is-resizable", + "description": "Denies the is_resizable command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["is_resizable"] } + }, + "deny-is-visible": { + "identifier": "deny-is-visible", + "description": "Denies the is_visible command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["is_visible"] } + }, + "deny-maximize": { + "identifier": "deny-maximize", + "description": "Denies the maximize command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["maximize"] } + }, + "deny-minimize": { + "identifier": "deny-minimize", + "description": "Denies the minimize command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["minimize"] } + }, + "deny-monitor-from-point": { + "identifier": "deny-monitor-from-point", + "description": "Denies the monitor_from_point command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["monitor_from_point"] } + }, + "deny-outer-position": { + "identifier": "deny-outer-position", + "description": "Denies the outer_position command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["outer_position"] } + }, + "deny-outer-size": { + "identifier": "deny-outer-size", + "description": "Denies the outer_size command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["outer_size"] } + }, + "deny-primary-monitor": { + "identifier": "deny-primary-monitor", + "description": "Denies the primary_monitor command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["primary_monitor"] } + }, + "deny-request-user-attention": { + "identifier": "deny-request-user-attention", + "description": "Denies the request_user_attention command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["request_user_attention"] } + }, + "deny-scale-factor": { + "identifier": "deny-scale-factor", + "description": "Denies the scale_factor command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["scale_factor"] } + }, + "deny-scene-identifier": { + "identifier": "deny-scene-identifier", + "description": "Denies the scene_identifier command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["scene_identifier"] } + }, + "deny-set-always-on-bottom": { + "identifier": "deny-set-always-on-bottom", + "description": "Denies the set_always_on_bottom command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["set_always_on_bottom"] } + }, + "deny-set-always-on-top": { + "identifier": "deny-set-always-on-top", + "description": "Denies the set_always_on_top command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["set_always_on_top"] } + }, + "deny-set-background-color": { + "identifier": "deny-set-background-color", + "description": "Denies the set_background_color command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["set_background_color"] } + }, + "deny-set-badge-count": { + "identifier": "deny-set-badge-count", + "description": "Denies the set_badge_count command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["set_badge_count"] } + }, + "deny-set-badge-label": { + "identifier": "deny-set-badge-label", + "description": "Denies the set_badge_label command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["set_badge_label"] } + }, + "deny-set-closable": { + "identifier": "deny-set-closable", + "description": "Denies the set_closable command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["set_closable"] } + }, + "deny-set-content-protected": { + "identifier": "deny-set-content-protected", + "description": "Denies the set_content_protected command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["set_content_protected"] } + }, + "deny-set-cursor-grab": { + "identifier": "deny-set-cursor-grab", + "description": "Denies the set_cursor_grab command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["set_cursor_grab"] } + }, + "deny-set-cursor-icon": { + "identifier": "deny-set-cursor-icon", + "description": "Denies the set_cursor_icon command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["set_cursor_icon"] } + }, + "deny-set-cursor-position": { + "identifier": "deny-set-cursor-position", + "description": "Denies the set_cursor_position command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["set_cursor_position"] } + }, + "deny-set-cursor-visible": { + "identifier": "deny-set-cursor-visible", + "description": "Denies the set_cursor_visible command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["set_cursor_visible"] } + }, + "deny-set-decorations": { + "identifier": "deny-set-decorations", + "description": "Denies the set_decorations command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["set_decorations"] } + }, + "deny-set-effects": { + "identifier": "deny-set-effects", + "description": "Denies the set_effects command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["set_effects"] } + }, + "deny-set-enabled": { + "identifier": "deny-set-enabled", + "description": "Denies the set_enabled command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["set_enabled"] } + }, + "deny-set-focus": { + "identifier": "deny-set-focus", + "description": "Denies the set_focus command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["set_focus"] } + }, + "deny-set-focusable": { + "identifier": "deny-set-focusable", + "description": "Denies the set_focusable command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["set_focusable"] } + }, + "deny-set-fullscreen": { + "identifier": "deny-set-fullscreen", + "description": "Denies the set_fullscreen command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["set_fullscreen"] } + }, + "deny-set-fullscreen-on-monitor": { + "identifier": "deny-set-fullscreen-on-monitor", + "description": "Denies the set_fullscreen_on_monitor command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["set_fullscreen_on_monitor"] } + }, + "deny-set-icon": { + "identifier": "deny-set-icon", + "description": "Denies the set_icon command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["set_icon"] } + }, + "deny-set-ignore-cursor-events": { + "identifier": "deny-set-ignore-cursor-events", + "description": "Denies the set_ignore_cursor_events command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["set_ignore_cursor_events"] } + }, + "deny-set-max-size": { + "identifier": "deny-set-max-size", + "description": "Denies the set_max_size command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["set_max_size"] } + }, + "deny-set-maximizable": { + "identifier": "deny-set-maximizable", + "description": "Denies the set_maximizable command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["set_maximizable"] } + }, + "deny-set-min-size": { + "identifier": "deny-set-min-size", + "description": "Denies the set_min_size command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["set_min_size"] } + }, + "deny-set-minimizable": { + "identifier": "deny-set-minimizable", + "description": "Denies the set_minimizable command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["set_minimizable"] } + }, + "deny-set-overlay-icon": { + "identifier": "deny-set-overlay-icon", + "description": "Denies the set_overlay_icon command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["set_overlay_icon"] } + }, + "deny-set-position": { + "identifier": "deny-set-position", + "description": "Denies the set_position command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["set_position"] } + }, + "deny-set-progress-bar": { + "identifier": "deny-set-progress-bar", + "description": "Denies the set_progress_bar command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["set_progress_bar"] } + }, + "deny-set-resizable": { + "identifier": "deny-set-resizable", + "description": "Denies the set_resizable command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["set_resizable"] } + }, + "deny-set-shadow": { + "identifier": "deny-set-shadow", + "description": "Denies the set_shadow command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["set_shadow"] } + }, + "deny-set-simple-fullscreen": { + "identifier": "deny-set-simple-fullscreen", + "description": "Denies the set_simple_fullscreen command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["set_simple_fullscreen"] } + }, + "deny-set-size": { + "identifier": "deny-set-size", + "description": "Denies the set_size command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["set_size"] } + }, + "deny-set-size-constraints": { + "identifier": "deny-set-size-constraints", + "description": "Denies the set_size_constraints command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["set_size_constraints"] } + }, + "deny-set-skip-taskbar": { + "identifier": "deny-set-skip-taskbar", + "description": "Denies the set_skip_taskbar command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["set_skip_taskbar"] } + }, + "deny-set-theme": { + "identifier": "deny-set-theme", + "description": "Denies the set_theme command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["set_theme"] } + }, + "deny-set-title": { + "identifier": "deny-set-title", + "description": "Denies the set_title command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["set_title"] } + }, + "deny-set-title-bar-style": { + "identifier": "deny-set-title-bar-style", + "description": "Denies the set_title_bar_style command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["set_title_bar_style"] } + }, + "deny-set-visible-on-all-workspaces": { + "identifier": "deny-set-visible-on-all-workspaces", + "description": "Denies the set_visible_on_all_workspaces command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["set_visible_on_all_workspaces"] } + }, + "deny-show": { + "identifier": "deny-show", + "description": "Denies the show command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["show"] } + }, + "deny-start-dragging": { + "identifier": "deny-start-dragging", + "description": "Denies the start_dragging command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["start_dragging"] } + }, + "deny-start-resize-dragging": { + "identifier": "deny-start-resize-dragging", + "description": "Denies the start_resize_dragging command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["start_resize_dragging"] } + }, + "deny-theme": { + "identifier": "deny-theme", + "description": "Denies the theme command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["theme"] } + }, + "deny-title": { + "identifier": "deny-title", + "description": "Denies the title command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["title"] } + }, + "deny-toggle-maximize": { + "identifier": "deny-toggle-maximize", + "description": "Denies the toggle_maximize command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["toggle_maximize"] } + }, + "deny-unmaximize": { + "identifier": "deny-unmaximize", + "description": "Denies the unmaximize command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["unmaximize"] } + }, + "deny-unminimize": { + "identifier": "deny-unminimize", + "description": "Denies the unminimize command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["unminimize"] } + } + }, + "permission_sets": {}, + "global_scope_schema": null + }, + "shell": { + "default_permission": { + "identifier": "default", + "description": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n", + "permissions": ["allow-open"] + }, + "permissions": { + "allow-execute": { + "identifier": "allow-execute", + "description": "Enables the execute command without any pre-configured scope.", + "commands": { "allow": ["execute"], "deny": [] } + }, + "allow-kill": { + "identifier": "allow-kill", + "description": "Enables the kill command without any pre-configured scope.", + "commands": { "allow": ["kill"], "deny": [] } + }, + "allow-open": { + "identifier": "allow-open", + "description": "Enables the open command without any pre-configured scope.", + "commands": { "allow": ["open"], "deny": [] } + }, + "allow-spawn": { + "identifier": "allow-spawn", + "description": "Enables the spawn command without any pre-configured scope.", + "commands": { "allow": ["spawn"], "deny": [] } + }, + "allow-stdin-write": { + "identifier": "allow-stdin-write", + "description": "Enables the stdin_write command without any pre-configured scope.", + "commands": { "allow": ["stdin_write"], "deny": [] } + }, + "deny-execute": { + "identifier": "deny-execute", + "description": "Denies the execute command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["execute"] } + }, + "deny-kill": { + "identifier": "deny-kill", + "description": "Denies the kill command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["kill"] } + }, + "deny-open": { + "identifier": "deny-open", + "description": "Denies the open command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["open"] } + }, + "deny-spawn": { + "identifier": "deny-spawn", + "description": "Denies the spawn command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["spawn"] } + }, + "deny-stdin-write": { + "identifier": "deny-stdin-write", + "description": "Denies the stdin_write command without any pre-configured scope.", + "commands": { "allow": [], "deny": ["stdin_write"] } + } + }, + "permission_sets": {}, + "global_scope_schema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "args": { + "allOf": [{ "$ref": "#/definitions/ShellScopeEntryAllowedArgs" }], + "description": "The allowed arguments for the command execution." + }, + "cmd": { + "description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.", + "type": "string" + }, + "name": { + "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.", + "type": "string" + } + }, + "required": ["cmd", "name"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "args": { + "allOf": [{ "$ref": "#/definitions/ShellScopeEntryAllowedArgs" }], + "description": "The allowed arguments for the command execution." + }, + "name": { + "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.", + "type": "string" + }, + "sidecar": { "description": "If this command is a sidecar command.", "type": "boolean" } + }, + "required": ["name", "sidecar"], + "type": "object" + } + ], + "definitions": { + "ShellScopeEntryAllowedArg": { + "anyOf": [ + { + "description": "A non-configurable argument that is passed to the command in the order it was specified.", + "type": "string" + }, + { + "additionalProperties": false, + "description": "A variable that is set while calling the command from the webview API.", + "properties": { + "raw": { + "default": false, + "description": "Marks the validator as a raw regex, meaning the plugin should not make any modification at runtime.\n\nThis means the regex will not match on the entire string by default, which might be exploited if your regex allow unexpected input to be considered valid. When using this option, make sure your regex is correct.", + "type": "boolean" + }, + "validator": { + "description": "[regex] validator to require passed values to conform to an expected input.\n\nThis will require the argument value passed to this variable to match the `validator` regex before it will be executed.\n\nThe regex string is by default surrounded by `^...$` to match the full string. For example the `https?://\\w+` regex would be registered as `^https?://\\w+$`.\n\n[regex]: ", + "type": "string" + } + }, + "required": ["validator"], + "type": "object" + } + ], + "description": "A command argument allowed to be executed by the webview API." + }, + "ShellScopeEntryAllowedArgs": { + "anyOf": [ + { + "description": "Use a simple boolean to allow all or disable all arguments to this command configuration.", + "type": "boolean" + }, + { + "description": "A specific set of [`ShellScopeEntryAllowedArg`] that are valid to call for the command configuration.", + "items": { "$ref": "#/definitions/ShellScopeEntryAllowedArg" }, + "type": "array" + } + ], + "description": "A set of command arguments allowed to be executed by the webview API.\n\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellScopeEntryAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration." + } + }, + "description": "Shell scope entry.", + "title": "ShellScopeEntry" + } + } +} diff --git a/src-tauri/gen/schemas/capabilities.json b/src-tauri/gen/schemas/capabilities.json index 0a3500e..09e9c2d 100644 --- a/src-tauri/gen/schemas/capabilities.json +++ b/src-tauri/gen/schemas/capabilities.json @@ -1 +1,24 @@ -{"main-capability":{"identifier":"main-capability","description":"Minimal permissions required by the KNeuron custom title bar and approved sidecars.","local":true,"windows":["main"],"permissions":["core:window:allow-close","core:window:allow-minimize","core:window:allow-toggle-maximize","core:window:allow-start-dragging","shell:allow-stdin-write","shell:allow-kill",{"identifier":"shell:allow-spawn","allow":[{"name":"binaries/brainaccess-bridge","sidecar":true},{"name":"binaries/ssvep-classifier","sidecar":true},{"name":"binaries/brainlink-bridge","sidecar":true}]}]}} \ No newline at end of file +{ + "main-capability": { + "identifier": "main-capability", + "description": "Minimal permissions required by the KNeuron custom title bar and approved sidecars.", + "local": true, + "windows": ["main"], + "permissions": [ + "core:window:allow-close", + "core:window:allow-minimize", + "core:window:allow-toggle-maximize", + "core:window:allow-start-dragging", + "shell:allow-stdin-write", + "shell:allow-kill", + { + "identifier": "shell:allow-spawn", + "allow": [ + { "name": "binaries/brainaccess-bridge", "sidecar": true }, + { "name": "binaries/ssvep-classifier", "sidecar": true }, + { "name": "binaries/brainlink-bridge", "sidecar": true } + ] + } + ] + } +} diff --git a/src-tauri/gen/schemas/desktop-schema.json b/src-tauri/gen/schemas/desktop-schema.json index 5625606..ad4f1d7 100644 --- a/src-tauri/gen/schemas/desktop-schema.json +++ b/src-tauri/gen/schemas/desktop-schema.json @@ -21,9 +21,7 @@ { "description": "A list of capabilities.", "type": "object", - "required": [ - "capabilities" - ], + "required": ["capabilities"], "properties": { "capabilities": { "description": "The list of capabilities.", @@ -39,10 +37,7 @@ "Capability": { "description": "A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\n\nIt controls application windows' and webviews' fine grained access to the Tauri core, application, or plugin commands. If a webview or its window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create groups of windows, based on their required system access, which can reduce impact of frontend vulnerabilities in less privileged windows. Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`. A Window can have none, one, or multiple associated capabilities.\n\n## Example\n\n```json { \"identifier\": \"main-user-files-write\", \"description\": \"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.\", \"windows\": [ \"main\" ], \"permissions\": [ \"core:default\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] }, ], \"platforms\": [\"macOS\",\"windows\"] } ```", "type": "object", - "required": [ - "identifier", - "permissions" - ], + "required": ["identifier", "permissions"], "properties": { "identifier": { "description": "Identifier of the capability.\n\n## Example\n\n`main-user-files-write`", @@ -93,10 +88,7 @@ }, "platforms": { "description": "Limit which target platforms this capability applies to.\n\nBy default all platforms are targeted.\n\n## Example\n\n`[\"macOS\",\"windows\"]`", - "type": [ - "array", - "null" - ], + "type": ["array", "null"], "items": { "$ref": "#/definitions/Target" } @@ -106,9 +98,7 @@ "CapabilityRemote": { "description": "Configuration for remote URLs that are associated with the capability.", "type": "object", - "required": [ - "urls" - ], + "required": ["urls"], "properties": { "urls": { "description": "Remote domains this capability refers to using the [URLPattern standard](https://urlpattern.spec.whatwg.org/).\n\n## Examples\n\n- \"https://*.mydomain.dev\": allows subdomains of mydomain.dev - \"https://mydomain.dev/api/*\": allows any subpath of mydomain.dev/api", @@ -218,10 +208,7 @@ "anyOf": [ { "type": "object", - "required": [ - "cmd", - "name" - ], + "required": ["cmd", "name"], "properties": { "args": { "description": "The allowed arguments for the command execution.", @@ -244,10 +231,7 @@ }, { "type": "object", - "required": [ - "name", - "sidecar" - ], + "required": ["name", "sidecar"], "properties": { "args": { "description": "The allowed arguments for the command execution.", @@ -278,10 +262,7 @@ "anyOf": [ { "type": "object", - "required": [ - "cmd", - "name" - ], + "required": ["cmd", "name"], "properties": { "args": { "description": "The allowed arguments for the command execution.", @@ -304,10 +285,7 @@ }, { "type": "object", - "required": [ - "name", - "sidecar" - ], + "required": ["name", "sidecar"], "properties": { "args": { "description": "The allowed arguments for the command execution.", @@ -356,20 +334,14 @@ }, "allow": { "description": "Data that defines what is allowed by the scope.", - "type": [ - "array", - "null" - ], + "type": ["array", "null"], "items": { "$ref": "#/definitions/Value" } }, "deny": { "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", - "type": [ - "array", - "null" - ], + "type": ["array", "null"], "items": { "$ref": "#/definitions/Value" } @@ -377,9 +349,7 @@ } } ], - "required": [ - "identifier" - ] + "required": ["identifier"] } ] }, @@ -2554,37 +2524,27 @@ { "description": "MacOS.", "type": "string", - "enum": [ - "macOS" - ] + "enum": ["macOS"] }, { "description": "Windows.", "type": "string", - "enum": [ - "windows" - ] + "enum": ["windows"] }, { "description": "Linux.", "type": "string", - "enum": [ - "linux" - ] + "enum": ["linux"] }, { "description": "Android.", "type": "string", - "enum": [ - "android" - ] + "enum": ["android"] }, { "description": "iOS.", "type": "string", - "enum": [ - "iOS" - ] + "enum": ["iOS"] } ] }, @@ -2598,9 +2558,7 @@ { "description": "A variable that is set while calling the command from the webview API.", "type": "object", - "required": [ - "validator" - ], + "required": ["validator"], "properties": { "raw": { "description": "Marks the validator as a raw regex, meaning the plugin should not make any modification at runtime.\n\nThis means the regex will not match on the entire string by default, which might be exploited if your regex allow unexpected input to be considered valid. When using this option, make sure your regex is correct.", @@ -2633,4 +2591,4 @@ ] } } -} \ No newline at end of file +} diff --git a/src-tauri/gen/schemas/windows-schema.json b/src-tauri/gen/schemas/windows-schema.json index 5625606..ad4f1d7 100644 --- a/src-tauri/gen/schemas/windows-schema.json +++ b/src-tauri/gen/schemas/windows-schema.json @@ -21,9 +21,7 @@ { "description": "A list of capabilities.", "type": "object", - "required": [ - "capabilities" - ], + "required": ["capabilities"], "properties": { "capabilities": { "description": "The list of capabilities.", @@ -39,10 +37,7 @@ "Capability": { "description": "A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\n\nIt controls application windows' and webviews' fine grained access to the Tauri core, application, or plugin commands. If a webview or its window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create groups of windows, based on their required system access, which can reduce impact of frontend vulnerabilities in less privileged windows. Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`. A Window can have none, one, or multiple associated capabilities.\n\n## Example\n\n```json { \"identifier\": \"main-user-files-write\", \"description\": \"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.\", \"windows\": [ \"main\" ], \"permissions\": [ \"core:default\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] }, ], \"platforms\": [\"macOS\",\"windows\"] } ```", "type": "object", - "required": [ - "identifier", - "permissions" - ], + "required": ["identifier", "permissions"], "properties": { "identifier": { "description": "Identifier of the capability.\n\n## Example\n\n`main-user-files-write`", @@ -93,10 +88,7 @@ }, "platforms": { "description": "Limit which target platforms this capability applies to.\n\nBy default all platforms are targeted.\n\n## Example\n\n`[\"macOS\",\"windows\"]`", - "type": [ - "array", - "null" - ], + "type": ["array", "null"], "items": { "$ref": "#/definitions/Target" } @@ -106,9 +98,7 @@ "CapabilityRemote": { "description": "Configuration for remote URLs that are associated with the capability.", "type": "object", - "required": [ - "urls" - ], + "required": ["urls"], "properties": { "urls": { "description": "Remote domains this capability refers to using the [URLPattern standard](https://urlpattern.spec.whatwg.org/).\n\n## Examples\n\n- \"https://*.mydomain.dev\": allows subdomains of mydomain.dev - \"https://mydomain.dev/api/*\": allows any subpath of mydomain.dev/api", @@ -218,10 +208,7 @@ "anyOf": [ { "type": "object", - "required": [ - "cmd", - "name" - ], + "required": ["cmd", "name"], "properties": { "args": { "description": "The allowed arguments for the command execution.", @@ -244,10 +231,7 @@ }, { "type": "object", - "required": [ - "name", - "sidecar" - ], + "required": ["name", "sidecar"], "properties": { "args": { "description": "The allowed arguments for the command execution.", @@ -278,10 +262,7 @@ "anyOf": [ { "type": "object", - "required": [ - "cmd", - "name" - ], + "required": ["cmd", "name"], "properties": { "args": { "description": "The allowed arguments for the command execution.", @@ -304,10 +285,7 @@ }, { "type": "object", - "required": [ - "name", - "sidecar" - ], + "required": ["name", "sidecar"], "properties": { "args": { "description": "The allowed arguments for the command execution.", @@ -356,20 +334,14 @@ }, "allow": { "description": "Data that defines what is allowed by the scope.", - "type": [ - "array", - "null" - ], + "type": ["array", "null"], "items": { "$ref": "#/definitions/Value" } }, "deny": { "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", - "type": [ - "array", - "null" - ], + "type": ["array", "null"], "items": { "$ref": "#/definitions/Value" } @@ -377,9 +349,7 @@ } } ], - "required": [ - "identifier" - ] + "required": ["identifier"] } ] }, @@ -2554,37 +2524,27 @@ { "description": "MacOS.", "type": "string", - "enum": [ - "macOS" - ] + "enum": ["macOS"] }, { "description": "Windows.", "type": "string", - "enum": [ - "windows" - ] + "enum": ["windows"] }, { "description": "Linux.", "type": "string", - "enum": [ - "linux" - ] + "enum": ["linux"] }, { "description": "Android.", "type": "string", - "enum": [ - "android" - ] + "enum": ["android"] }, { "description": "iOS.", "type": "string", - "enum": [ - "iOS" - ] + "enum": ["iOS"] } ] }, @@ -2598,9 +2558,7 @@ { "description": "A variable that is set while calling the command from the webview API.", "type": "object", - "required": [ - "validator" - ], + "required": ["validator"], "properties": { "raw": { "description": "Marks the validator as a raw regex, meaning the plugin should not make any modification at runtime.\n\nThis means the regex will not match on the entire string by default, which might be exploited if your regex allow unexpected input to be considered valid. When using this option, make sure your regex is correct.", @@ -2633,4 +2591,4 @@ ] } } -} \ No newline at end of file +} diff --git a/src-tauri/icons/icon.ico b/src-tauri/icons/icon.ico index 95cc151205f476946df08d4808765038b1d1a495..78967bbe0459fc6761edb35f17457e2a85ffa31c 100644 GIT binary patch literal 37390 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 V@pm_VYS!T&1|}4a`QN|b{{rUiZbtwB diff --git a/src-tauri/icons/icon.png b/src-tauri/icons/icon.png index e862fd791487c476e9daad4958674c463823f505..af064e0e61a206f85530d9b72c0a400579654f72 100644 GIT binary patch literal 66679 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

xy)_goL! zJYDV#1*+^V48xLY_Be+oER>aWd0l{3!Zt8kI!u~-Xd_OHPY&7h38@iQ1H1LH4;USar5uHKK z2p_$F*PXI14Fi0ILM;IH? z0Q4C*hShm6?Oy1OB4X%6_b~K?%mXKl3g{iuc~d03W;JPGYvszlol=W`gpdPb<$Hi2UdHK0%VYLfu4TNj^p3<&$XgFkA!klOPuZ^h zV{XQRn~x8bhdz@hIgdG1twYk<^zac#1s{rjdcR&te{g!Q(@=~+o|cQJ#A2+Wv!#2z zu^`s_nHuINEK1&&Dj2Y&JZ2o>2BI~4?^^IPP6Q6KdIi{1A_MvMBr8&utxI>-C=tP`B~;DKNRKGLHg6afAB6pBfOr&(LZZS z_^Dq0utjh|qRk7fgFD3}oT3Z=9Qom3O>UO`y=;$n;_nQt`Xot{+K;RvkAP1^>R}5H zuo*j)t;k-&o54h!r7Pn!AI9LB<46u~aP~9->?4S0Tjz65?D~Ml2pfZpc+1fuQ9(M$ z(^m{@;mPG$(E{jr6)vy~isy2F4vP{Wcx5OSo?ysi6K>#YQ{h7NXdI)P$HwcwW& zy_~vD`WvCdRhlLupdzd4$H^7rb9)?q`bvL!H-da^3t%GJpsAR{@(2B4WD}C0&)qR6 zm`6Mqgd(HFo$%~}U7?LZ1Oc&$QZzcb9e+xLvGDxQ$pP?%#@xup4uIV*2@}rrKY(Qy z;1RhV>J@)X$M^_b{?i7eEU!^vMdIdR#BSsnT+BA2=?5)KtaB?W`|&$lP(Td5X6M-D z?6k&vHC&asAWDkjM;&${V zkI{Wi@ogow#N6pt%*8UFs|Bfv&%ot=vUB28m~e;6)9(ZO>6!s-!@#PhG7l} zk^U>|RytS!l#u-LW~viJZZoFQtb>=?iJj>Y!paM%k$%0>O;Q;c2e39cS|?_GGygpe zN%t4&KdI*qu6@ok5kS8a7=8ltbLZWC?>^%<{=h+vsSK)ENA@yw z8|>F5c+L1<$1V=*3`!kFFW@;l?p8uL+!{Nkp!*-Ko<5v%`7j}$eLiZ1`+g?vrCouo z_ri+T_j$1~+xT<_WHoZIDMLhLurh@GL<_|p!Vgtsz0v|C`h4qCp{gWo-#rrk4 z2@z2NBf=8q_f7|8fDll2DvaiCxF}&771H%nH6dCOP~{a*%TGuIRYCxpVENML?fT;V zczw`$8QPWO;*mnO5nE;cd&i+&Ac+tGr0A|YWr<8?JUid1Tvq5v#S z*$6oBc53gpE&KypI|H6A+!tKoBWFgW)2!AuIjx|LFrLo>Mx{cS_WGl+)7)&wPcaZ= z!-HXHuu%hSPF1|?41cq#Pco7J2~%jT_iPXQ2{Z0m+#}8~a~O(KVu5}gSi1+l)AiKe z)b$zL(b>AwRV!r{_;UEmR*pB_(bMF#mE0z=Fyp1R$UyOn=5f%mSMX687n#5Py7ayP z&JcLfTE^@#$O5-9 zACsPT+0A8)Cx6px*u{g96$mM|e^IGeXBSlg*gy@I!hwH(rJ*^SFA`8^zr^N_MaL8K zj25o?kFL4B_!Bq9G4{#gaHD~s@wh{uo#?!PB*8sbZh-5H=bmW4KddE#3_=*SZX=)3 zK@}70g)k}i1DvGG5AfBxKhyD84CH58s$Iy006V#b4DWqI6gJq`(y|%W;4DVXZO6_R z&%?34@EmcfpnhxKPvt7Ra#kuFQ%K1*Q(3kv4JOn(&QV1?#42JNBo78SG#TB|U>mA3GW7*sjxdmCpBi6*7szmuVX4+w}_<{-h zA4rpSZBPYbHoO}eEa~z8G()N`nVI*X27^$RVe$eFXh$qfGgsJ&e(Al;mA85BX1UpY z6Y9!t9BagJY9JgZ{oCX!v`%C@!1D@%wi4Fv5J+kRZhFViE+!V36n!oe$17>Fkzz{} zc**5(=Dqh$Dp?xY!BC8g{e_@M9vQaubnl=kKVDvg`&LEP^_un9QNtlHD6Ow+Xbd7z)STAnx9!Cd(2ya=9tnmtZ+sKo97c+%?o*MQu#~d0E^s>+XkKr> zDhHO0uau;sRL;~rq-E}8`0k03j{Bp6gKbD&tauzXS!k7}KL1hstZ&X&h&Kv!hNIy0 zt#bDj?eY{6vOUVc$MlFaA#@n#vMMh1J>wLUIIXK!afZ^qy2+2_t#E&4FJQJ7z+4-yv`kW14}VU-ot5Ui5VvTX3||E!*YF zH#V#KZ)G03(2?TEuAJi`T>=-a2<}Cju)Fu3V|0^jP{`o~n0JPYyBw4I!0do*z?ufo+TwXy#|&il9h@Kc`iN5oITX9nM&V1{03*Rs)$ z?tGh76jfYj?c#V+#H+ancMa^S)+KU)kd(=Uiu&NgFRpw0}ux8+YZ@N0FK;PtjWRASrcly|?3I??8-yg>F+C)53|{?{r^d-aq@EM=O8Y2n^69 zXU%z8iW2|O#!0l>$Qfr-!rM2=#zn>Rk0>hWP^Xv;WKhVDkBk(j9!I32ZyA$(b^2Lj z(3501vh-v%OB$B`*>N$SG8a;{J^aRWo?<^=9n1Uu#U1!Z_-qd`s80EM#_o$uEswT> z1CiH)Ij=Q{aA_$uGfm1vfVr@Zrr{^{pCc4V!yHC6wAQsNl&4?rwY&UAWS!OB>1>0C z!iZ&|mVO6{V0#exiL^-C--`4f%Z6k!BLXa82Hl%XmgK%H?8=EZxmN@BhbwJ-v}MKW znC$8qHM_s?ESgF>(|<=?<8rqm9Ko#&FiN@v~It$F*dh~xW& zN}u1oC0z+JD%)Y!VfelNQ9&HSC|-R7f?Z9R(2Fbk?mnyFl^o8OAeYo#+?`K2w3i<5 zc~}y9IkD`87)0{Nq_{DZNk&P&6H6F$Gj&%ko~_$oTaR~T8f^S|k7$I7tQtp^`y)H_ zRADmC3ZK{ zhg@Be?yUkI(eu9z-D;o((`O9_&x>xm;e#lD_OJXy3|@kFiAd__QQFO;z1Ym|R{Q#Q zS`PepWM#pf+!9uxwndlP3+IY1GbDexw0K9=K3JLe*Z#IV=5@n^8nz5qn@u zp;bSB+mwU<^{<4-L&%>$e~Ko=a(Ae?2J2I*??l4Rey6Ai#YwAV$%@1aMCe=aU9Nn6 z&B80pj{j}&eUM*!8Z3kke)-g5jp(rRrBvo8 z9?lTYGPSX-!QhN#@e=VuwS-kdORzSZp5k|YYAl{1hZ8#_E7xR!lr1#rbBPDIu(-oA z4H6-!FYsuRUBJ52KDdJ$#%$8rW_A5N{~2_Lsin8InB1Wrt8YqDE@(UL`#n;;52a-}%4Uz>!QHF^e0h$A!Ks6N_ZUA1tS3xp}HJiILTj0J6ywifG+L z=#+9K>2rl+qgve#R=$2N$m`1fvn2438*SRqul!j{LCsk{fz#Z0Xs5-cl# zX9|wiBH~z3dGV7XXpnX3XuSyg$8R8j?$rLw6(G7Lar62OP_?TIR{m9Ierz}&N8V5E zMqCe_NNgO^WBO$^gw6j|@Y1CZ2&SV({9ySioqNyEbGevTiVz7Guf#O$esD9ppnJ$L z^opg8-LDsL$GCbXKDF5Kh12-aY1Ga?o=k0S7SCQ+2t-10GK_hEL> z>yRd#)TMc>m?k`Xx)84Sna0(D>~o>l=!jTa^%&IG5FQjpyEJfGFBasdu=!g^%R?yi zZQ1KgoV^ZhXsEWUW}dO$TlT%+u=BySoYM2?-HO6sUYc<4?fmR)1X zv8cu1GBV{rbVVu!)2*cROew1z58g&AR}$Tuwb920-E>O2dVn@e8lHF$O z4JiF4ONUX$mjf$uKcVZ1`q zNXi>qLJ?8BTSCpsK9DR$=s9*KfYC!HwefeFme7!k!#enN$VW`oF_3J|0)A-c>u+b> zDdc)USuBD0i%ib=(BZX`caiNpUw4=eJ2NzMDc3O$jl6a|R97bho{*0G*r{I-w5yrX zxO2Z$h;|r}2})U=8N6VMf()sk`+0mZ+IhQ;-><0L$t7+v^(z#HU|H1$;-K~78R^Lv zbP>gurF*k8w|kOc3A3hp@Gq{)5AxK2le~75TyZ-q8P9h1Y~jptGQWZZNUsL)y;B)w zhw~EaRXUDb#p;_M+ng0{vthBE18<|OFB2w@oeafS^5d{dn#tUnwmnC|&NoC_xQqVe zlDha`-JlrQ|SjIcu4uYIaYpi^ZlR;-bzfk_&14O(SH@v)h!4;;>?77=>MKs=905x-$)Mtr@xnXKD{i#*fV$dr) z@#PvULbYO)b2UqQv1y=#p}SSE1zz$}vm`kOjc*)F{+$im7ytbE_a)Z5{}?yqJh=7EwqM^x&nPMOG`$UaG;(QH zZ}Cx`sfetv%+puRh_=IAqr|m>;+X1C)q=?eAle#v*8ei}1m@TKfb)h` z^d6ijW!_u@S>x4I*mkW*d)4W4xpLr8iHh~fF5KFr5UOn9!6S7SZtx;#?o2mlK~zEW zj3YKWZrI1Bo{&J5>dg`Ifmahc@|z;+^r7AfUxA*ku(bRGLQrJ#M{MPePu@$kvn@L8 zpGA1J*F((uBS?_Xs}8vLRcg+{f{XD@1KOq9nRb@jX-arW1*aAjYVwr>eDO61Vi|78 zlXUJJXETqgD-2g%d}IjWSXY`gC3o^0?iWV@8P%rWrt1Oej)9SEx#gm32}z1Y*)NYu zx@KFoUMo_NJ&S6lVz9jS-ylr3Zt+s@*-I=y=<83FJiKf}Lgte|5PT=R%EowMct6d^ z)*ju$?!`lfmcSFgl}r_D-Wq0K3m5;R(`qd_t+O+>1~)5Yq>_SI6@NO80>lS5&h&mo z#-HN~nhI;Ctz3HdNqlo+we@-K^f5#y|8P%}79Zcp6>QVqwFzYT93&8V7`)iO0|*%9 z|5S!+A|%2lzgccYAVa0|yrXYB;_#_-3pX&#+kNV&XXk6p&}|8hXY-{%3g$l15iH?T zUZj)|CuQmBUh9EP&W5a7ypH3Kr(~xl!_H4OIfw$MsV1o_t!Wc(;Bd3jvpY#o5S>vF zY3}G9T#X#s8Z%ql!ox`8H`3!XlTNA%fA*7&)VBtk+9}s@(E+#8K2nJ+BEdh?#TLwm z%Wdp2L_T@=6l<+#mrN~#>rO_}zhLgpOpdsp>geQJdFH0s(1Q5^p_{QbMsE|FS^j48 zC2)oMkYw{xsdY`6U~JU) z2tboDtaB?hILJ{&+0HAy3>A3{MLsBfIxT5BBaRG`=%HAIB!g34j)H{H^fBSS+#(VNtjk3wvr%C{M13;yyF8xnw ztEMepZwk7D8~LAdiH`A=(^>mVM z@l!hMb&uVQ_1NJWP7i0M(+Lb8-8f+kf$jJ@Omhs5xWhk-uTvdwzM&CU`^GF<1RK%A zlYG)GNaKm>rsmKQcM_Jc-FtDF{7J|5!LD^I%V9?X-c?6izfiR4R2E`z>*y4V?Yn0f zQv_N)zh3AYl1rBGODNS7P15oymyTDh%Ff^G6)%1lG#Mu;BAr;E&TD)+aus>3?0QLK z>)a)(%u7{#YQfPkFEZt(-AtprKFtf8c@=noypMS*QoQYJcjXz(YV zxavlq?jv)3;O6WGX-aM#4*n*HmBby*Kg=mQj9{K;Z1+;^4O$7Sw)PA@g{O0(ToZ}} zk|?!!RT32;ZPhMLTDW^zV;N%!SrhaSDMY}bsM176>>*OW zx|U;@F~OO64Hrd38d z6@*l7y)jR*e5=KlkgX&)L2X~uGgS4qO>V?gTz2cC)iarcah@?fFHa#KgU5y_a!(@d@$daoE+o3R@Y=y-)FwUmus0Kxd2_OKGf< z_{ww7(*o}DICNmY5^xG;aLu_E!JLD5CQ+ud^VON~SXiFE?|y3Dq7|Jp@nT-U z<5dTM|Dxvc@g3|uuimL1gwuIe!T`<)%z6KP`t=vqcVEAO)+}94`*`j-()A!rxvV)2 zCC;JEuJrBWIAxv1<9nO5X-&O(2Eizv$cv_y9PLryhQg;+cFm`n>bF4xhjT zFE+kGgk~I81i2pzB7Yru{umm_v(W$C{illfc5L55`~KKTJ2!5meLQ>M(BVVM0N7QE z-b0Bt2h~qZq55q28+T|&typiML(gNVDc8Xh*}*_0Sy7P{ZRF9Wh5ERyPHN0thm0nT zs6LmI;wr-_;d2W0y?uNwl3BHJGrh+4RF*`0<8~{N? z$^dysa%s!jwbYvHl(~pBFm)_MN`R%M`fRqEHLPPBNAJ%+`yxl<4Qa%%lh_DzOP#~b z#@b%0FpaKPCf7-$rW`#sW+Q^v7c!AAXV0JH=F$MJiMe>HVLl+3Wz?f|mT#`h6Xc38 zScXtb^@=XpD$_QI5`f3+)TA|RNRBjat2>kv^$RFSb4ea=^z5!=#sbHV-V7V_H5xW< zN||ibj~h0W4)5Pbe{9~y*Q1b*>1mN*V`d=IXL5W2ZIDFtBEtvZq^&D^lC8X9>-2m<|`RjJ0<>zqD zVUDEs)ToJytLM$7hwsWEoAVwTdHPwjtFS;h0g6m!w5IvP$=L$@o_hf}jqOblCvY26 zYRfua146c-N{cWu`O5ISRjS%n%IB145ry-B>)(`>AqTTm?bly_t(-+u#V*>=x@{Xu zN>BHevTR-_sM6Ufz82o{3OiW5Wj1WgnU-wpnQ^OBvPlkv4n&;In{Cla<~-F^e>nP_m-f~dwVazw+ zy$`UXwqoh8idUmmJOw0efP&)Qxf;cplS3cQ{)Cnj%=EsSBcFhUc>*vDFcV15GsAv=vg=gNgS!ibH)W*LR^AMZx^U* z;uKelxyr>#@fC0e2CFqO4@Iu7SYqYk(Zlyh z5<7-2F7wP?@-w~6HUG~(m`+~BzyuZzDy%Wf48*Qd`IrKn3SL?>qf3XFc}vK0QzpV>SKHQACNefoj>>7_>>qoqInM28CV*g1$(HA-|4csxhVsj0?SY_klC z&HAc0RW9Hy#Zqmsm2sUaa^1*^fD?FMn{#TtX~*`y^mjB3Rnd!jLUJM#02_K8OiS^t zGRGb0=WZ)qZL`K>+r1u_+T0LTvKPND_8{-&vCf~0`t9-Ry6xHZ2hDu@9eQZ;O+Mof zV_B1oKPgeE6lUSasUb{OWKr(*qP!}jxbASo5MA!xvX#D{J)8FJ+b0Wr6arb$sd5^k zs~f;$^~~mS&8&WAeQMdEJrBA`P@*&UpT$+&Mle)qLQ6E1z{*5*6M zZsR$yTV<~}TRf#R!O+b_xJC1Rpqa0{O0PWsEUjI!QhlD1n07jAT$fR|UcG7Xh*P;! zqY1AkE|np3m1Di|6g!tT-}kWUsKVo`iJqc#*eDQ8uhD};}rC`L*mwo zZR7bjsDS5Lk9RWarL5wGdf+jw+fY|-m)gDi+j5E2#2P?=vRQ<>IkP^Y`Sa(|mIaGw z%vD!XTON%H;#fRu1{A0PJT6~)-IrQ6zQ=h*o!MoOC~0{Z;ZJTOoHuJ0J@e#~w0`9(Wu&yBtf_tK^|(W#K6jL$;T0vZR5sRnJt%9v zup6zC9?N;PYxmcNxf>3Zkik8D;LpAEJ{$fQo_dlFA37upyJP?al^J(?5Q`K$0$cX% zqQ>poQN#L~lmJJ7e}_d6hTmDS=z3oSzjt^%SpXG$_{S-+mPZP=)|8%TRstG8w-;$^eO{N+>s z&We&PysUexCSMfb{P-WY|GA-Zo?DwQYW1k=mS9~omnElNhnF}!Cvn#GMCyO{;e#~y zo9}4q^*7Q>Z2Z9yDT-kctny|6;(7YtMOR-##||9GZIN68u^pX!Sf2F)PF#{NbK`Xo zQpvjxIjpjZhf9eoSVZeyR$F}Ksi(-r&zaQx#Q_jh0eQJcXgR0n=Y99B8m*bmS%7^7 zd6dfx<{gY`X+Jos)hpP<}#1rXa16_ z<{DJz0t;0)e10N3CKGP{D-CA>r9@`rL;0}4PUMbEhnuei9DR`Xfu)*z&GmZ84v+G~0K!Nrt@@(hQ!{xv+rv(-IY*$T>e8DX40k^ID2=*w5_5_gD2v3ZIPBa?1cBJ9*eD{ zus83@N~beq#CIJmmoGk&l zar)t%cj=moCU_SS*0x7_v?e^iXwjT{F=486r%u#~MVXFe;#4+gUQo^4 z@67W}3A9;k^GKW>r4FddKX#h-&&TP-2Om*NcyY{DcUD6RTYoNe^i!S-wO_Y*epU;hx58yw<4p$_SrZwNBfPsvhXcosxwR2cY5x!D{z% z8;gWpPjS`goU{G-^s|Y`B{9Aes{f52W6``%s_0{nW1yqi)J+ z*&Hq*#M_FCvuFc`w%3+U8N3B~C>f*@Fi|*9xucH~$-tf~S>+}DmMopVDVNCP-ZDC!s@F{ja_lb}k<_?)8i>I;phq?4A6Z;l()c@y(O>}^b z{$YcGj{Qbd>#3oUsm*HbLp490`9fnZy^?Nx;4ZSUz|mn(q53&ySCGZIumun#!)wqP zgJ}*pOP_u-hnCLynielwM4Q*FrURJ6;MX4Mum(P1jfm`;sMlq0w!sr3~YBhR8?0|!$NZg*v zcNeIMh~YN==!#I}Oy)%3tA_=Ae*JYREoGVH**raP8LL0~Ki6Id$yK9eXw6JzQ!Woq zoO}a~n=pYqsdbpkQsmP|2qZ1;73twJ80S^s78G&jaL<}`^!3M|&>Qc(MLQNREpblm zMt~IYC|enRhw49c8|yVnVp!Y2|G-LB=octehqEjWKumbeM}=ULl9QR#lu8~>e`nTD zr~1qgC#yFftbB|lEo1^-{=q{f>Y>$@ZnBvvX}ee4M&IP^cogb{tFEO!rw*rOcRbq={==UC(r~aNI?f7*ibIA7ie&minK13U%$+ zi5fI*Liy;^M^3;J=&^jeh<~`eY(2p}_(Tl9vzhjlxNX7GW>#?FyLey7F zQo#DzY|*g;U2*rFG;;iSL?&wNFUX=4Y(5}*_u1)(sZ8Q?wwvqw^I0tGo#&pTW#4^I zUOqvhcUA%-{Aw7N#Q_kg$yiM}g+5@!aB9)L7um9#P)1#z@6LVANz9T5b9V2p-8>d{Gj0EK zCv9Tp>$=6i(m|+PMcIv{jhE_e=ahaXa|MQ|Av1-GeT=@K{AC2M2-_FnQA-)qK%;PD=060{uq>VOh z+d^yS|3p8}{DkIw`YG+^bq$7Jae$)moW^tKNm`p`bkfMt)P)`KCM{c17LTD$gae2n zRLo&GxOX2NIGjU!e&0qbS@3DWS6}lq$;~7X2cVcYnbEEdojz#-ojT$)%I1eCBfFt* z0ZP(jBYvR3oON;ZzjxO?Cpl{c)an&VCVBI%kQ_nshN;IC%=SZP8L0 zsy)ntN34;8`SxLY$RN2|m#<-7-eG#@^*89*zu!YSJ9mZs$v(FK33U0rQ>hP+=hfx1 zaFjm4swl9uD(dk2CwtI-7Drz%{E0qz;YFJN;Y`}cBbw!YwEFy+uX4*%Ptcf)E>i1S zuGQTWoKVW0K$!i(-~G0B4Sn+5^E89kvICsujMhvaoIGSV8?|gjm)~A`G z^>lEslfcKM#y=~Dj#nEZ*9vov9OS8l(EfPyE&Ab;PwBw!-L?MSsg(|ZSqIAEHm@@; zzm(3HFrHd4*-J^r@UxeN z%}-u>S=xLI0UWYwkDYrQLzhpPOue{cz@1T#;uN347Pgi_P2fZXhx>5Z&kO0HTkfC@ z^S<{jmbmWf8dc&x)a4J-@4-0p{0r!sTW_WoeR@%WPp&A8WIvDh8BFB+FPBre@4oRm zz5DnRw3Ee~q;z@oxrSW$<@ZgcGe?c$2sl|0{sOgOs_CC7@$s6~Fg}I(D{lJmwO8rG zXP%|iOP58hWUuZxac~Rtc%JTg(Pa~ONH9Oyg&6~BLA)E&8+_NX`Y3 z0@c8>#}0c_^1FGW;u21wuHCkS+Vjw!W?7BZdDT`0m`O2vjDa$%0qVf+&GA95Q^8J|1WsW)=_5b?oV*1~M57Bh4_3z^vwU2jW zv(?i3&TITZy6oP!H(hr3U3Bv3b0|4JnH_dDF47uJRUAl*LmniB*Q}*0meFH(-owWK z8QQdVof?khyQiaQ&C*eS=VOnhOK-o8#$7s*nsEjohu7YCW&Ck2yu#c_{p<3Z#|q)R z$KoB`T?8$SYI zxp={32D6iyvW4XBk;Udl+L5;+?<`XFcX^f!W|2}IBQNQTxdEW!r=_M*J6;@HGU@J6 zejaUPj~X`~ zYTvgfHQ`p!j0RcE;fPb3_wKwrA9)MBkhr*bW$2Od!u7LT87q&~`G5sCpMi%YoPldKb5}aS8RxJW;oUwm;|MgTp|NYm$SFNqqdpND*b*MJi zBB~R|s*L>Iyr1ULs}Da$U%mSt<#Ai8Vgguw{Ixam*M_Q5>sEBt1NYMLXP?DRUJ~Un zdq2)vtJHN-8vWjTq>xFhiR>Ua*rA#`^Fw;}wma#^Pi9eGPEK*-7bOPJ)aU5IJe$A# z;{h5vdL-Got09+<)5dFWp&^5=pO-i*A3$miBj59+q|dSbv*QZKDTUYT&OLijW6u0; z<@QDl3G*1vx;OwevEI)YxO3;ubp1mQQokXC$i+X~@AS4W#bG*_5&7Dz1**_m;9S;% z>-J6H^FEA;ycppo+{f?ZWJA!L`|*$I*N1lI9H5Phyp!9I!ip9nscv#UZY=xgGL_`O>R;&>NJa5auN3t1jIom0YXsePZ`)V5bIYLK1H2Gd4qECl0bHN#w%KZdb) zTbuqj?)waO99nhkNZtGNp%LT9(P>N?J?V_osS7Jk@=Z*;xLicmpdWnFQM!y&ZJ{3iWAjG-ZoW71-`BHe(c?GYMk{`pr@An7#-%nJe;pBLF=6@IyYHsc z&O4t9czr2wJFV+rS5a_*Z&II!@;H5-nUG3{jvS(or%j`mA9#pXEndt-LASS0KFS1h zbQ>xvX~4LgV*jjPx$xopWDW2S$TS_DxwZ5$=G%p14r~k zH~*BT)d8*?2t-CGA2i`exlQk0w2f<(Ti2|Os`0Hla>YI+r}Cn}5x}B(KdR%a-?=?C zO3PGaM+e3a<6+UquA@=TcqFl~NXK41*q}IRJ@X&n1k~n~tiN*w$>3egHpbANhU^41 zfD_t|>rEV6@`7Dw~H`-+~s^KM%A%OYRO z-y{9oUgBwHCXK!MCc5z2zd-VXjh(kFX}64@b_~HoVQWft9Z9rh_jY>g$^Xza=J9Oj zdozxmcHP<+jl^ta{8_)`%10le&pgEA`C zIL@9m@7S60*-2W>>XG@J)wI_VC!ki408N36VdtMq_r3MHO6A+E2PV`fROx4f0LBuU zzb{)tPh58+E&BckzNy%&b-JQZGd$7PA3N;wNi_M+JE&Q!cIx{a7PtxI7{PHu=7U)O z`{VySOK<)AN!n>>tdkJ5)b)0;gVdvMU%LFE2dMY3;mq7GQchL4A8K$`Ad;eZD{x5c zYFMi(GoU$Bw(!&K23x$pEryNc5(#!00iQZOr3J5vAM1B;Tsibi6q^QqHWGLGgTp zZCtgA4lw)F;c!IVLWUHuJ{KEV*I@3apRx39e@blJh)V%Sz35_2gVjZm$_q&kmdDL( z){Hi;SfNsAf}>d$2j6>tY}>|;;Bx9WbPzSl%H}eFS8F|-*%B{<-&-qIPARM4Il1;( z#16<|YZ6>sN(#C8euOe)4*zT)|2~h?$hrJ%BwP0|Nl9`-S6a!K~T7 z^tvlbjhjyWr!arz)T^(cv$>@)A>Qtd>~%>p49^OlkNPQf=x|X1t>(2lm)lEg10k@+ zI{@n9vVn_dm9n9uN6|o@@>Y}>@AV)fkgiT+0U>*O9iBbEh<0!}z!9Z{Ig|X7!vkh^ zZdgYNO|z*%^X62K$Gd`XQ{-m^kUc@B!^tVemMn_Xh^2DZ@nQq<4S~_D$N&Gyie&pZ zLo)n=^La62)it*>a^(^$a)d#zg9R1~n>Okeq0FJO1YX5MyJbvVL}ZI(TO~3NJ9g01 zx81>_E%PnMMNWp?GO$$>`ta{X{Q3 z`~>|v_Z#Jq+M>9OqW29>-{3RPpbM_Jg3_8d^>IwXdM~=+tn-u&K5f7Au%oT(*4GN6 zslB$O|Fhb*ruKsdP#y^_{+6cQ_IKLw z&0N)JlaEI*z@UvHh;Fd=oWc*!z)>S<)IE1l0!wph2cgz;0^A1^zFXj{@?NTkDPokg6s~&uS&baCd zN~x2s278g;TB3Ls=v2Z;WGMTsSVhk~_9*={^8??#7R~2V6FghAZ%;$cJ&#&<>k+xx zBPv_srg~Wos5O6nqCr9w2cQP@X*bDkOiekOb!o#?N>6fG3sz~bFGg|#fe!U0{&%MV z11YtBLse^!1*7O@!?ljH7LVRIg+6}mHD(KYI|2M3NT$kgIRKmhW@yJH(}>aM(j8Af zsU%6Y2D{B(<8QNaIaWuQZ2A66|D)g5tRgTgeA@knuuqYnUO-v*n<6^}xSyTI&=(U0 z0rUt$Y0N9~JmL3vK8Q68Quwm9H#aS*4h*YxHVu&IFj{l}*c*(RVBxjh^geHhgZ5*35g%ZJk$VfP2&FC%R)d+X?+% z=Q5N6_3NFM!(qO^yOjIAA{zyGHp+T`HF=WTuu^!h*72N)_n-ccDnCVgq)HL#T1rsL zcfff!UQd&z-c5;_4LB25#B#SDZ(FXVRbscYC@y||v($RDaLF(9(6!go_bhkItkbB@ zI+~e|LZ0fF$(<>~&O4W~SBBf%<0ED^72rnm{r{N{nsiVYL%HtI}{kCAyj2V3e`aQ^15NAo_)ZMYCpGL{e zvbhHAovvlKNLSH%li`6ZZrVwRJs|(``?qn zN>gSw!5Ku3*AwJ^ugF$fv=!|Al@rcHHE2rwwtNNs^v$=5oLJkVf2;@E_@B&?@a6YU z<=Gt>bkLQjhWDVSzraR5!7{f}R%FA)>&bzwJLm=0P55c{=icZJ#^2?N?wKrB&p%3w zZ8&J?f97cvmx<%eSESUU3f_mFQl6L6?pby2RR+MJT4)P(i)0PI7i&dv*2O97^ggLI@BblwFoAENuhq0!vxSA6PmG9RegyC?Um(<0Q7@-g}j7%PO`ctGCzZ z|D8KCPfv;@OR{81esA)dpA<`a@6F7;_ndpqSIo@3gHg*cO*XxhHCEKd)3x~Lx4w^m zKK*AUrzm*Ldsc=P3K^RrJ&0O7*h@!>sn8tcCNg zT878|_Zygd+bv3ZJ;9X5zR)q)x8K*4ZiXaavPp}DwxumrC^W%v=;G3vq9(NXaofqQ zwsR+a&AZ-(ul@Q-+~b3hDi=As(^S7n3{S$yp}gvKcioBElxIz#VN5eK_7-yI!=_=( za6iP5<&{7E1v!+JoiKA2cl|~t?}1TX3!C6TO|b?7NTbr+gqhRf<=@F}FTT(lyPq@{ z<&@{nBl5U_QoBB(jdbbYcB$#v1eC?K zkWEuhw7)}WfL*LrQZi2=h_t@03P1bo=dt=nKc*~qV0g5u=vZ{v~A zeHNb4W2p9LB*Pjj9o9N>G25MPs%4(5I*V8T{Iv2oi3Lx9*bo8|uXft>&RMVkE=r#U zJE^b@1|_OH9(GVET8sFK8`D8KLFrmLV5 z0fM(KBPcMRcET+T^dWnD0J9M2a=m^o_Br%MNT8(a&fR;_dagR=*Lv|EFFSRT22As? zbiqP21cXLu?=k_nlu`mofMx!P&2Nb1|JR>=4qI2h7E=Z+-7OF<2F05{@P1tL$ot?T z*T0n&HUWv+qDFvx-$u2%WM>l2P}~0r_Wa-Y&F|ZL{ll7i|2|? zpqV^uPj)_|Q!vWNM~BtLmc@54c3^s^nI2#}pn+#17r|7jUd>=7AaCXjEO_7^?0xNZ zytVSLIAI!~$i&g5eKAZt1gr@m3-+$H8}Z6hPvgClr%*s67fr!d6~qi~KzmtJfL=bT z2~MJo@U?f`hLs2Rw<~|eV82&cL{p72;Pa!5%86d{e%f^{vVQ9kqaR(J#>mlB*?c4) z7W+k<+$+k8QF5#(wt;`<&Oid+6!yVy{m>)G%Pl~C*a-g0%OUES3s>FE;NRR-i&a1S z1y-}~ADK(T3_l?Z1UbcXK+fE0Q?P<&Q_F6<4bx`MCb&3>o-Abh(~8}}gjlLVHW>EM z`b8d-9t#v5gxeLu_&c@7`bi@j%=GgILI7!ge=!ugelC}YpLZe6hwmwLMPm7kb zRMM>CRUJ|~>@v~Q)3UK}ZZ;;d|2KEpGHm?gpRsk-%iJZFwkHB&lWKap5BWlF?l}JT zo8KahZH#+9{YmP%&{RkdsuEuNHR}cwLiZsxGZ(i!a4%l}^>0yARTa}{Z|l-)C=py$ zU4f+R9JMnJM2$X!vZqHO{dJ>#HNqXdl-w&<`O_z8nY?d*OeX-E2|NpvQZx0&YmiH= z|FDUXusQ5z)uerm(!%n)VeDA-3fBMrsYo(DYyf`(7B-np@|k3vNJ>t^HFw^L6^}fO zNpz%4PR&xg{bp7;f;K3$zm@2Q%nE7e#Ah(`FPkb?+EC8Qt+AxOojRot!Wr&%^ci5e07Z1fs3pObjERNgFnie& zdb&?RFv-IKSq>jLsn#MnTudsIQ47U8Y8vV&+c^^x#!W;U6M$$wdj$W$Duy`HC1+-! z>Qo7e$hB22TxL%&7PZ|KZXzR%m5~J)Q#gV5E}+I`=P}cHd=Azay%?vW&T=edQ3`oI zDW$LRRQ?z-u-JB~4WUGRG~~~pg9kqK?u}${{VVQ%>=T$ZX8|i;iDWgjDq||aH|ScF zCWSTD!@BtodWZQiSf`!u7hp?v`|GQ*_IFRAwXWVUZHs}HRBX5bih3RS>^6!3U3TR% zC92^k($pnoCo{Y5iX%NRv?#sD?OMcURisZj=_Lv91*!IxM1a_gIhcO)3S{Qem9M!K zUbZqsEh`c@hk-A-IFZ$}wUU~Wf`!cPb@usntqWX*;@%pcJF8$c);zsZB@cGHP3h&w zWCEZ#+#1FcpK=+&fX!i(5{UlOnuhyn8LuSRU0Cznvt$5lgHtGLF_-|@OlMGGh1~a< zPvOS9@8z20R0%+|dS8S-8J3os6L0+aPgwK2Kf*`gq!xv!A5uPlR23&RJzY_qZ~54x zc<<*vgDbDQ38CZ^I_>aYs!RYhaRfZ*2oerMP z$iSBWx2w}`epnDx;X4*aq}Gg3J?AO7G+@ulbfg7-fDIn2K1D%jmFV}M3U z%nn%{n`O!{s}nnE;IjAdL0+4yN6Sr(*FRdaDAUFH=y73pbNO@0Fih5zQ z7^NeMPtTwzs9*tzl?TZMrmXIYPkamoOE1Tpe|iS5|MEA=SZJsRss&CgJ#`A}N$Hxo z^y&`U_iv{{2G!3W{?E^$^xz?!IdM`YJ)u}bfSdOy(9{55P4$TH>-S~SXe{{$ybTD{ z)WodtkByZtX?xfywY}v2dof{>^!~XP=%H?sdRZMDI7mtEQ^m!RO3H9ZVkKe88b2QQ ze)vOJcEk0^o<0p4Sq#2VE1J2{N_4uFhDacKeUo)iv#GC=5;&-q>gDT{m!3xP`VD9- zDZ^NTh8NyS^;D^fDE^2(aGNy&;!2f)8?9Z>zlfK=^S|&)>3=xkDdGvhKn5zq0=TTG z7{B`b7x3woPhs--iAsi5dhj7rTU+Z#Ch0Z@u^fI;_v6L96 zPRFXB@;W357|KB_l?X|$0*y`B{f`$>{*gzKnV+vFHk%b3V1}ZX9!UVgXyrv_TrnRL z7tU7+fRuk@l@VaIf@RG3)Dqt{$j|yWZx3mLW{$;ZhD*Rw-s|*x@_w$UXVcy519;RNKWaHu|Dt*dWSgv#X6sF57$ezUx7C2kkIW@ZkuB)&KyCzJ|nl2k%$O z8yitqbh3ld*-%ONTDXn5cifKq9{V_POUvMKd6b4f5lyO(F8C`y+7%wdYR7NmH5on- z^0Pxn58&*!-6%d;jG|q8acuKeG|@jKp9XH%5G(Z2$G@mXYD>saMC>kE*37XQ>|)G! zxcB@FiJC**=|g*V;o0y1A5Ez0JAj+x3BaZ3$Yz4)4wGW{!w-KPpL*h_w4R*C0qawf zQK%pyK4C3*J?-$V#ZZ3N$G{B(dorgEJH!0lX60O{MS5Vepi_k(j)W9k`_V^{H*OrB z{PO>xm=1I5^-O8ap<-IJvMhOS_dblx8?D>{C7uT~U6s}3QwfDFQCV&<(Qzc=rU&jv z>CW9mgP%0i%VH?=jvhy0OB>$|pcnFbz7CugbSW^YM>zR9g1tmaqnDrWe4k$a-W63o zj>+IJ)Hnk764TSL;?YMicJX4)>ww7+>Ua;s5z{0a&el|5&s!UmOs$goy~yA{B?m5_ zo%2)>@|WLuJ?{9=&tUqTxugMSsR~9LXL}%uQ6=l1tcyOb13|838Cn8=?#JGP`*3)} zMijle7W=k~5@ZVk#6I~cie?4j(&I&V?wj96<=zAM?4N&+OnU+<{7ul;`>TMyc z{Kj`syltnUB*zJQu>n=O={Mfk)QDZ2%$MKx4wGLIY|b`{ zwG5gzRVnjFmB=k2Eh&4qsl=JMiQ_6%J4;C;C6p8GL#`fL^O ziRS{x!dh`+Pp{sHzxe(OOCGoni*C9RlO|6`GU*^K1gr=W-*2jO*`h%!Fn}vE zVUrvlmhv2^3Di?MawGP?v>Lnr`VW*GJ=UJ>I~;EVm1t3lT{5ZaYdNWTo-ICsBU`p2 zdmde}Xri-p`Eq1Toy_UO_hI=<@qx_}n2$msRebNcx8DXgT@F`$@5k8o@+%O>0|DEF z*_*#u1KL9Iw>30k-_D&l%hpF4ErV^QK~VIf<~Mom((_LrKNd@Fy$vP1_bXLA+c0-8 z8|b>fGD4w0ggqrK0zSIXg7KhVte)ZV18`HCS2Yep#n7(o?bU`s;D!``&{Yx7>hqR~nl5*@gP+ zVoO61{C+-L0kft#lGXfgXsSjdTcAhRti_twUd8DnMW{J>;%&)=t#FFa-+Y+tgOqp!T&lQB8%k z3MvFnz3FBwxb;R9=8oZTr2 zP!Ax`4K2&27HMM^wbs-jNX*YgO;&`{&vh(~9tA6C+VhS_*s>stN6PN4fhGhQydB(# z*f{rFsrcz+;FlIpL+u$Hr|kF9jhk?C^EMoywtj;N`jmCa(tc1@z}>8o7I0cs1N>g> z_{Tp{ck&cYfA|>YQ&H5NlZj+^hN_+!B3WToS=7`xBeQ^a&ZAlcT&pX~ao}VT_Pn$T z+kX2LN~x`)nUvdDmY{YZW|i1d$eo-m_R`8c2Z|qpy{rG1wpY6#^e&5KD z>Nht!k@3jNX*&O=iaY+jyZ2x>tC2fbt;RXFEm~VzjPb|chq*E8(;8{&o`^c%aY|>O ze*8aOesec-@vv1`cW*u`xayu zj-_&yffyEEZ4LU0W$zJJ&Rw;3h> zE&~Yvp_bI2(J8rD@!*3vx@9{Kl80-H5eZI<>Xk9302xyzsUE7qlIV=WZwKl{qx=yC zq`*^84%wlaMocDyoW(MF6_E zqz#}n)%q%!HWf3jyB6+*RGbYo64d5w2hF`rI$U%UjKlAy$d;vJ7R3q;>j=PGi-d{)|CwcA%a{R;ps zs=t?(nNUcXP;7bTZ|a;(GqRZ!ce9Nj4R=}+rst1Al5R&8s}1LBYp`eYW*p{P?Iqs8 zN^D^I6U2{LPXMA6?V^P($mK)HuI=hPU+EsE2*yAMEgA576d>lz@xyrTKR$)39oz7} zU;hkAZcn5^k*JI`1Q*~#VRAYqyzf!WXJ-7|&!51-f4+>G(`E2+cRg4OtQKWKYEMg5 z3mr^!ZrI$xbQNeqoQU~*%xw-O>rZ^VI)u5Ab(C-&$1?s$9um2EnIcS18UdbDuQZ)(=ow` z4rNZIqpGrk24>ITxyQfC;M{5iSqp1r25t-_o!h#XJc_s(le0H%R!gQhe#8@i9(zs$ z{ks>WvL(A|E)vt1T#nH*W+I2|fG~TgK2tu(;&L0=t)}EMBvT(VWeOhr?=N8=fkwNZ ze+BzqTt!wwRNreLu>ofJ)8gNo9X}z_Jr2UU3PD2g1Y&4PP5E!y}q}TGXdQyL;Xp*6e=N)D@B7+UvEYt-8T|cfwTY(Q88W&KoGmZG%isMzR{!GH5qv_} z1n@e!nnC;GweMFW0T`J6q-ba34W`fj+!fSD)P% zaoh|enGA@r<0jB8E*Y(@EtfRkQyemFoM+2FBp|vuipnc`AmLv8u}F(~nF$n-Z=X4K z9L^pqHqv#DH9@J_KA_dC#eI1HA2%pTM{) zu0Re0YZ4brDh`;6aelRg+9PFFi!2`j@_yGjOs8~yFCBTskY+=ZxRKzC@42uLl|gD8w! zf+4E_VLR*@NWMK?7$7d{Ffc!agkUwFZiFsf`;Wd!G3VUGD zB*~DFAl!Z$R7IG-qkP@@zv5wj<9~btxBSaT;9?m&sOhRT5$Yt)(fN!B(MV62*X5yh zK;G0zICZcX=S~!vpTl;^O>L>Tq*E^WrVn#BIBx*~s6oo3YLSXlPi}y?K!?Ed{q11B z7ox^y$TWdTryX)5eb^5XsCCIn0+@qbo@x$(fA@a<%S^%-m>UaSW$l_F_jUdlCu|zd zKh{SwLhmZ$DL|ktIY>NdYJ^J-tftP!@TQnWS}OJ-eCxkb@gmj zC4eGmDaoVYN=b(&DV^(8CNeW~kjECnge$MWj79S>p3ZZk0$`} z1Kml3T6zIj+;=xV{Qd7?!i-tW`rF90B~VB+e6f|%UsR9?pJ!24UIH4F#jKqZt}QG; zKuVA%q@i%yOiuC@$eTSKX%i+QYb@IfT#VsN;O_(#dq6jRy`xW=xJfMer;pCZsL2zt zaK*K_`4bf9c<)0kJ>r;gObblPC+*t2m1ytVb0q}?yvcHI2& zkK)<~-UScmmk7~1%$AQM%DbVbd#H5oSw+Znq;l>~qJv zLsSBk&4y@|_(}0~^A|GG@-Uuj*}|)qqBTe_mP3bGF>F!QMm-M5i?;v}^Du*UK-es* zYu*1pUqoKPXp{!)Y5(a|T-$a+P{$pAbXm1K|1hU1#VZCpnq680v>Xk!DU@#l1NG!Z z%aOZ?edy{+Y6p~|WK1j^_vC|V+tWKX(k z1+ISN{S^JlAuFX-;d8=0ys~BRr6|x*|7usZMx>AOHDyf@dURH17T)oh|KLW)Pe<0D zqPe-XgVsR&7+N{<_%T!^7_;v`iOzTreElo9?B<&|A(`7dY#01sv`)M5w;{-NBl!@7 zvByqSGE31dMDn_rboeT(s7_qX&k|I&ooW%`_Y_5dCx1LrQ$0v^C#c}n%pHJ~{N3C^ zG%1N9t>2&5#l=HSg@? zqn|`&MFlPebyo2Q2{PEGveom!Up$FaVwQQhnj5x zUu&<*Jv!&Hixq>KCh`iXvG1m_P9-JCo9i3Z*vy#Bq<-vZu^r)VKCG&ZfeToLY-&Cm z>zH^5q*6M)yy7%|_o>fg=gPmT7Go$7TLxh93PAkmuuD)oTL!oVH{F2gS1m&eCtfl$ zYX1OvU-z{8lU}`U*Uaidh)82`?eef<$^YZ-G_N4PgCUYE=pGZ5TuU%OH4}y;UW4d> z8^IvdZ&_DMR74h{YHtwv6K7+^eRtvSr%qE(ym`d$7Odc(L(tT<_uZqo|2~r`VH>pU zzUSS)75ude7+XC(Z!58gPC%l6TWn$5SYdLzl8JY@5r-do0I&Sy7u=zG4P{9^JmPo% zdCTTv34?!74@JPU7jnoPRjzhyo3t^Rknim-XRrTWK2=6q0<<(L=OG*Uv_wxb+ltLf zH{am*>t^uRE^z!=&pnw5QA^N=oYCX4_|7|VYU^&)P}|=!{fQs(1OTz;32qpJ%)H?m z%q8U}F)5QXv{{;mYGPmL;a3Konp%K`q3aEsd{`B<+ZSiMS*CZ{g_dVb-K6N!V<$AS zu4IU#OZZxsDs%mfDrp{RRkY$J>KIhl)Z^tJ{cJ>+hV9h74^i&clbC=Hee0W8uxvT% z7&;quFS*tBUi0`dfSUYPLTA5#gsr!KAN~HF5%0-P8At?%xl?fOH@%!0)_(bWc~JU+r(?>PIr6Ma@}R{2f|* zT4ud=_yWfUx?%hnNxmIp=1!*o)MO(8hyekpcmfbVy5AMD+&*sZETV5mD{io|O6|65 z-oIV7av5Ve(oG$45t~WoB&?^qpsXKUXW@@prnRWvyxzlNZhPC}uJy{erZ=HNK9^91 zQtYX?ib(m|lP9tBg;fZNe)kBI@U&Vyf@4_ro(D1Oq5DwhbP?X^RVCI?L?=NXBn-Sh zz*awcKZuImzo#05-09Qc6$!2s-1?<2AW&C}y|2BdY_LV#Dje2FRO#Q&;6LxS8*s;e zKaSkYZ1(M&6v(rG_go+Jo~_ywbn&r_9lSmuc2I`v9_`kp>Xn>=th{`~7Cg=dh_e7< zS!ZCvqzPEezW=y+bBWM(89mhjQbjYIu0pEM>TjsSzE!W_;JUYz`AlMJCWDYmnP!Al zU(!?vl-{dZ5zxDkBklbE=%U0J?g5zC7k5El(2MN60)mppp!C2|oGUvGCm9GM#VbJd zI8E%rBJ!&5r&*JoPLl!z3xzF4haS^AAj^(b(%v8uTXIV*x#0-7DSdnP8=&Ma9L=2R;zXzUsdp8SBN_lCrXFE{OSXi;Px6$#; z*{(y)IZZJz^oR^^kdrtR3adF73Y+SWg51{#>7oXCkDBKf zewsi;ex`Frj^YUCv)}8D2Z4A35W9zGbROp4`3|~nO+zz5GQyz`57dHcg6f-Q@Xw6!+uohFLtN!G1)K*t15gvD1I_&OrrATMzqM#{*Ax-rzt=)i11_;=Z&}%>l z_;Vd5t;yKYMlVT$sK6iY$7k0V{QX>1MKZN;`eXv<#-h5U7_}8=Bbo%3x504Q30hLl z4kjs=-+dQu|J-M2*Sr(~(l1pY)7l#*;dtQhkg&=T;Ikbwj!uC@q*s=dp}MSsRi7vm zm|>C#NLwdjCdSV+ZsA;f;#=Rs%oR6LKR-;@*><_#{@xF3E+Fz%S7KZ;7sF~7Zv=f~ zzSvX}B_{_O|DT8hVOdboFTpOs-a)30N6qgbll?$T1M2CTS9# zsC9MZ-8ND=Ol_LEJFR1hb^RaZT#0cD=3>@ecVG&4sN?C-7Rt?Miy#pmm-24oBH~a3 zC8Qb9A(tsZB-2@t!flHCD~W-Q7PC$%8-GpL+c5=N|N6}?BAlBkhLq-X(?#uF$e2D3 zfBnk0QLwo27*inA<#@P54GKR<)flV-A=(MW)$6KxmRfkzDJ zVo0d3_qAZbEq5T5m9dqd|1TU^vl>lR=OT#lAq9j+C3$kBQX69mt-~Mu!T(~y+{LJ& zAdbjeJDC9Ly54V?rFD7HegBXo=$b|Cw3&cC33SNsgOl9< zKy4koEzLMnRZjrkDfsIuxdEs{^})lc7r*rOTXEH+?+0Zn(b(1kPlCM%l4n0!03sqL z2|Mpi3&ntfKL5z)d&U!h_;Ep#f|R$~syeiIDZJrvs1n^k1_c8@MeVVcCbZSpnyZN6 z)2(bf`DP`%_Np_Hy~-KUSw1QL8AN$>8Az zD2WS_hdT?evUs(rl9JtK%G#KTOxOfAS_`(;2QZT?8g7i@OKlJnO z3dZ0p74RgOIxQ8zew*8HE@>7J&yow4c@%<^bHTw@iH|SeCffUrhJxbR%6hbU{YdmS z!&iG2l{K}L3vNX7xhgc4orRl1KxMQB@6JB=(Au});Wb?O?)MTb%%olqnT{hEK+J~& zja-Y)mYzmSeZ4^gj3)r`1Rz$AHkRNUiz#-~cqKfZF=*lfVjsW?fb`Ir`=h!1ELtmT zBJ2Y>qkXz%VQXc!u;zsq+W$C_3R2s;+~p*VO2+JW z+=&@V

W5U%!70!BowwmudSFbNI~NX8JZto@ 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 literal 962 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K5893O0R7}x|GzJFdPEQxdkcv5P?;7SYISMd1 zCUZ=1{-MeLaQTxIi%m}sOe*CpE|2}%baRR6!FAsX{_NUg*RbULOU4FECWb_sqUA3c z>*nP5zZ871cHZ~bUpUMdG=v#A@Sy0k(G5$l74%Eh-F{RpCe6^oNhOr~EnQ(+^snOH z`_sRjXQQ#V$qQ3pU|oAxQ|;4lkfxtre{cO|>P799I)dI)+4zh{wU&3+qS`1x&y9HSZG=xl#{{i&%fi@{-9NBsTA X36h?E-}s*b^Cp9*tDnm{r-UW|C8OSB diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index e3b8990..3cba5b6 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -21,14 +21,12 @@ "resizable": true, "decorations": false, "shadow": true, - "theme": "Dark", - "backgroundColor": "#030A12" + "theme": "Light", + "backgroundColor": "#D3D6D2" } ], "security": { - "capabilities": [ - "main-capability" - ], + "capabilities": ["main-capability"], "freezePrototype": true, "csp": { "default-src": "'self'", @@ -60,9 +58,7 @@ }, "bundle": { "active": true, - "targets": [ - "nsis" - ], + "targets": ["nsis"], "externalBin": [ "binaries/brainaccess-bridge", "binaries/ssvep-classifier", @@ -70,9 +66,6 @@ ], "category": "DeveloperTool", "shortDescription": "Modular brain-computer interface platform", - "icon": [ - "icons/icon.ico", - "icons/icon.png" - ] + "icon": ["icons/icon.ico", "icons/icon.png"] } } diff --git a/src/App.tsx b/src/App.tsx index 7d56a5b..8fdc9b2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; import { APP_CONFIG } from "./config/appConfig"; @@ -25,6 +25,7 @@ import { closeModule, launchModule } from "./features/modules/moduleLauncher"; import { NotificationCenter } from "./features/notifications/NotificationCenter"; import { DebugPanel } from "./features/debug/DebugPanel"; +import { WelcomeScreen } from "./features/welcome/WelcomeScreen"; import { logger } from "./lib/logger"; @@ -46,11 +47,23 @@ function getErrorMessage(error: unknown): string { export default function App() { const { settings } = useSettings(); + const [showWelcome, setShowWelcome] = useState(true); + const [route, setRoute] = useState({ kind: "shell", page: "dashboard", }); + useEffect(() => { + const timeout = window.setTimeout(() => { + setShowWelcome(false); + }, 4000); + + return () => { + window.clearTimeout(timeout); + }; + }, []); + async function handleOpenModule(moduleId: string): Promise { try { await launchModule(moduleId); @@ -171,21 +184,25 @@ export default function App() { } } - const activeSidebarPage: ShellPage = route.kind === "shell" ? route.page : "dashboard"; + const activeSidebarPage: ShellPage | null = route.kind === "shell" ? route.page : null; const isModuleOpen = route.kind === "module"; const shellClassName = isModuleOpen ? "app-shell app-shell--module-open" : "app-shell"; + if (showWelcome) { + return ; + } + return (

- - - + + +
{renderContent()}
- + handleShellNavigation("device")} /> diff --git a/src/components/DevicePanel.tsx b/src/components/DevicePanel.tsx index 6e6baea..50100e5 100644 --- a/src/components/DevicePanel.tsx +++ b/src/components/DevicePanel.tsx @@ -1,70 +1,131 @@ +import { useState } from "react"; + import { useDevice } from "../features/device/useDevice"; +import { deviceRegistry } from "../core/devices/deviceRegistry"; +import { isEEGDeviceAdapter } from "../core/devices/contracts/deviceGuards"; +import { isBrainMetricsDeviceAdapter } from "../core/devices/contracts/BrainMetricsDeviceAdapter"; +import type { BrainMetricsSnapshot } from "../core/brainMetrics"; +import type { DeviceConnectionState } from "../core/devices/models/device"; + +import { SignalScope, type ScopeSource } from "./SignalScope"; + +const STATE_LABEL: Record = { + disconnected: "Disconnected", + connecting: "Connecting", + connected: "Connected", + disconnecting: "Disconnecting", + error: "Error", +}; + +function formatMetric(value: number | null | undefined): string { + return value === null || value === undefined ? "–" : String(value); +} -export function DevicePanel() { +/** + * Right-hand readout: live view of whatever the active device produces. + */ +export function DevicePanel({ + paused, + onOpenDevices, +}: { + paused: boolean; + onOpenDevices: () => void; +}) { const { snapshot, streamInfoByDeviceId } = useDevice(); - const activeDevice = snapshot.activeDeviceInfo; + const [metrics, setMetrics] = useState | null>(null); - const activeStatus = snapshot.activeStatus; + const activeDevice = snapshot.activeDeviceInfo; + const state = snapshot.activeStatus?.state ?? "disconnected"; + const connected = activeDevice !== null && state === "connected"; const streamInfo = snapshot.activeDeviceId ? streamInfoByDeviceId[snapshot.activeDeviceId] : undefined; - return ( -
+ {!activeDevice && ( + )} ); diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 0742a0c..d3927af 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -1,48 +1,36 @@ -import { AppWindow, Cpu, FolderClock, LayoutDashboard, Settings } from "lucide-react"; import type { ShellPage } from "../types/navigation"; -import { APP_CONFIG } from "../config/appConfig"; -const items: Array<{ id: ShellPage; label: string; icon: typeof LayoutDashboard }> = [ - { id: "dashboard", label: "Dashboard", icon: LayoutDashboard }, - { id: "device", label: "Device", icon: Cpu }, - { id: "settings", label: "Settings", icon: Settings }, +const items: Array<{ id: ShellPage; label: string }> = [ + { id: "dashboard", label: "Modules" }, + { id: "device", label: "Devices" }, + { id: "settings", label: "Settings" }, ]; +/** + * Main navigation, rendered as a mode selector inside the title bar. + * + * `page` is null while a module is open, so no mode appears selected. + */ export function Sidebar({ page, onNavigate, }: { - page: ShellPage; + page: ShellPage | null; onNavigate: (page: ShellPage) => void; }) { return ( - + ); } diff --git a/src/components/TitleBar.tsx b/src/components/TitleBar.tsx index 56f8459..793e625 100644 --- a/src/components/TitleBar.tsx +++ b/src/components/TitleBar.tsx @@ -1,3 +1,4 @@ +import type { ReactNode } from "react"; import { Minus, Square, X } from "lucide-react"; import { getCurrentWindow } from "@tauri-apps/api/window"; import { BrainLogo } from "./BrainLogo"; @@ -5,7 +6,7 @@ import { APP_CONFIG } from "../config/appConfig"; const isTauri = () => "__TAURI_INTERNALS__" in window; -export function TitleBar() { +export function TitleBar({ children }: { children?: ReactNode }) { const run = async (action: "minimize" | "maximize" | "close") => { if (!isTauri()) return; const win = getCurrentWindow(); @@ -17,23 +18,23 @@ export function TitleBar() { return (
- - - -
- {APP_CONFIG.name} - Modular Brain-Computer Interface -
+ + {APP_CONFIG.name}
+ + {children} + +
+
diff --git a/src/config/appConfig.ts b/src/config/appConfig.ts index ed54e3f..a9cd4a0 100644 --- a/src/config/appConfig.ts +++ b/src/config/appConfig.ts @@ -21,6 +21,12 @@ export const APP_CONFIG = { */ enableDevelopmentModules: import.meta.env.DEV, + /** + * Simulated EEG / brain-metrics devices for working without hardware. + * Never registered in production builds. + */ + enableSimulationDevices: import.meta.env.DEV, + /** * Development-only diagnostics must never be exposed in production UI. */ diff --git a/src/core/brainMetrics/BrainMetricsService.ts b/src/core/brainMetrics/BrainMetricsService.ts index e209ebf..f67e83b 100644 --- a/src/core/brainMetrics/BrainMetricsService.ts +++ b/src/core/brainMetrics/BrainMetricsService.ts @@ -1,16 +1,8 @@ -import { - deviceManager, - type DeviceManager, -} from "../devices/deviceManager"; +import { deviceManager, type DeviceManager } from "../devices/deviceManager"; -import { - isBrainMetricsDeviceAdapter, -} from "../devices/contracts/BrainMetricsDeviceAdapter"; +import { isBrainMetricsDeviceAdapter } from "../devices/contracts/BrainMetricsDeviceAdapter"; -import type { - BrainMetricsListener, - BrainMetricsSnapshot, -} from "./models"; +import type { BrainMetricsListener, BrainMetricsSnapshot } from "./models"; export interface BrainMetricsHandle { readonly deviceId: string; @@ -24,13 +16,9 @@ export interface BrainMetricsHandle { * Modules depend on this service rather than importing BrainLinkAdapter. */ export class BrainMetricsService { - constructor( - private readonly manager: DeviceManager = deviceManager, - ) {} + constructor(private readonly manager: DeviceManager = deviceManager) {} - acquire( - listener: BrainMetricsListener, - ): BrainMetricsHandle { + acquire(listener: BrainMetricsListener): BrainMetricsHandle { const adapter = this.manager.getActiveAdapter(); if (!adapter) { diff --git a/src/core/brainMetrics/index.ts b/src/core/brainMetrics/index.ts index b4331d6..564ffd2 100644 --- a/src/core/brainMetrics/index.ts +++ b/src/core/brainMetrics/index.ts @@ -4,8 +4,4 @@ export { type BrainMetricsHandle, } from "./BrainMetricsService"; -export type { - BrainBandPowers, - BrainMetricsListener, - BrainMetricsSnapshot, -} from "./models"; +export type { BrainBandPowers, BrainMetricsListener, BrainMetricsSnapshot } from "./models"; diff --git a/src/core/brainMetrics/models.ts b/src/core/brainMetrics/models.ts index b12b22a..e8b5ab4 100644 --- a/src/core/brainMetrics/models.ts +++ b/src/core/brainMetrics/models.ts @@ -26,6 +26,4 @@ export interface BrainMetricsSnapshot { timestampMs: number; } -export type BrainMetricsListener = ( - snapshot: Readonly, -) => void; +export type BrainMetricsListener = (snapshot: Readonly) => void; diff --git a/src/core/devices/adapters/brainaccess/BrainAccessEEGAdapter.test.ts b/src/core/devices/adapters/brainaccess/BrainAccessEEGAdapter.test.ts index b795724..35386d7 100644 --- a/src/core/devices/adapters/brainaccess/BrainAccessEEGAdapter.test.ts +++ b/src/core/devices/adapters/brainaccess/BrainAccessEEGAdapter.test.ts @@ -111,50 +111,23 @@ describe("BrainAccessEEGAdapter", () => { type: "eeg", }); - expect( - streamInfo.channels.find((channel) => channel.label === "AF3") - ?.sourceIndex, - ).toBe(0); - - expect( - streamInfo.channels.find((channel) => channel.label === "AFz") - ?.sourceIndex, - ).toBe(1); - - expect( - streamInfo.channels.find((channel) => channel.label === "AF4") - ?.sourceIndex, - ).toBe(2); - - expect( - streamInfo.channels.find((channel) => channel.label === "PO3") - ?.sourceIndex, - ).toBe(26); - - expect( - streamInfo.channels.find((channel) => channel.label === "POz") - ?.sourceIndex, - ).toBe(27); - - expect( - streamInfo.channels.find((channel) => channel.label === "PO4") - ?.sourceIndex, - ).toBe(28); - - expect( - streamInfo.channels.find((channel) => channel.label === "O1") - ?.sourceIndex, - ).toBe(29); - - expect( - streamInfo.channels.find((channel) => channel.label === "Oz") - ?.sourceIndex, - ).toBe(30); - - expect( - streamInfo.channels.find((channel) => channel.label === "O2") - ?.sourceIndex, - ).toBe(31); + expect(streamInfo.channels.find((channel) => channel.label === "AF3")?.sourceIndex).toBe(0); + + expect(streamInfo.channels.find((channel) => channel.label === "AFz")?.sourceIndex).toBe(1); + + expect(streamInfo.channels.find((channel) => channel.label === "AF4")?.sourceIndex).toBe(2); + + expect(streamInfo.channels.find((channel) => channel.label === "PO3")?.sourceIndex).toBe(26); + + expect(streamInfo.channels.find((channel) => channel.label === "POz")?.sourceIndex).toBe(27); + + expect(streamInfo.channels.find((channel) => channel.label === "PO4")?.sourceIndex).toBe(28); + + expect(streamInfo.channels.find((channel) => channel.label === "O1")?.sourceIndex).toBe(29); + + expect(streamInfo.channels.find((channel) => channel.label === "Oz")?.sourceIndex).toBe(30); + + expect(streamInfo.channels.find((channel) => channel.label === "O2")?.sourceIndex).toBe(31); }); it("maps all 32 streamed EEG channels to unique electrode labels", async () => { @@ -345,4 +318,4 @@ describe("BrainAccessEEGAdapter", () => { expect(adapter.isStreaming()).toBe(false); }); -}); \ No newline at end of file +}); diff --git a/src/core/devices/adapters/brainaccess/BrainAccessEEGAdapter.ts b/src/core/devices/adapters/brainaccess/BrainAccessEEGAdapter.ts index 9cc1d87..a013631 100644 --- a/src/core/devices/adapters/brainaccess/BrainAccessEEGAdapter.ts +++ b/src/core/devices/adapters/brainaccess/BrainAccessEEGAdapter.ts @@ -1,24 +1,14 @@ import type { DeviceStatus } from "../../models/device"; -import type { - EEGChannelInfo, - EEGDeviceInfo, - EEGStreamInfo, -} from "../../models/eeg"; +import type { EEGChannelInfo, EEGDeviceInfo, EEGStreamInfo } from "../../models/eeg"; -import type { - DeviceStatusListener, - DeviceUnsubscribe, -} from "../../contracts/DeviceAdapter"; +import type { DeviceStatusListener, DeviceUnsubscribe } from "../../contracts/DeviceAdapter"; import type { EEGDeviceAdapter } from "../../contracts/EEGDeviceAdapter"; import type { EEGSampleBatchListener } from "../../../eeg/models"; -import { - BrainAccessBridge, - type BrainAccessBridgeLike, -} from "./BrainAccessBridge"; +import { BrainAccessBridge, type BrainAccessBridgeLike } from "./BrainAccessBridge"; const DEVICE_NAME = "BA MAXI 009"; @@ -108,8 +98,7 @@ const BRAINACCESS_MAXI_EEG_LABELS = [ function buildChannels(channelCount: number): EEGChannelInfo[] { return Array.from({ length: channelCount }, (_, sourceIndex) => { const label = - BRAINACCESS_MAXI_EEG_LABELS[sourceIndex] ?? - `Ch${String(sourceIndex + 1).padStart(2, "0")}`; + BRAINACCESS_MAXI_EEG_LABELS[sourceIndex] ?? `Ch${String(sourceIndex + 1).padStart(2, "0")}`; return { index: sourceIndex, @@ -159,9 +148,7 @@ export class BrainAccessEEGAdapter implements EEGDeviceAdapter { private readonly bridgeDisconnectUnsubscribe: () => void; - constructor( - private readonly bridge: BrainAccessBridgeLike = new BrainAccessBridge(), - ) { + constructor(private readonly bridge: BrainAccessBridgeLike = new BrainAccessBridge()) { this.bridgeSampleUnsubscribe = this.bridge.subscribeSamples((batch) => { if (!this.streaming) { return; @@ -172,18 +159,16 @@ export class BrainAccessEEGAdapter implements EEGDeviceAdapter { } }); - this.bridgeDisconnectUnsubscribe = this.bridge.subscribeDisconnected( - (reason) => { - this.streaming = false; - - this.setStatus({ - state: "error", - error: reason, - message: "BrainAccess Bluetooth connection was lost.", - updatedAt: Date.now(), - }); - }, - ); + this.bridgeDisconnectUnsubscribe = this.bridge.subscribeDisconnected((reason) => { + this.streaming = false; + + this.setStatus({ + state: "error", + error: reason, + message: "BrainAccess Bluetooth connection was lost.", + updatedAt: Date.now(), + }); + }); } getStatus(): Readonly { @@ -211,14 +196,9 @@ export class BrainAccessEEGAdapter implements EEGDeviceAdapter { const target = devices.find((device) => device.name === DEVICE_NAME); if (!target) { - const found = - devices.length > 0 - ? devices.map((device) => device.name).join(", ") - : "none"; - - throw new Error( - `BrainAccess device "${DEVICE_NAME}" was not found. Discovered: ${found}.`, - ); + const found = devices.length > 0 ? devices.map((device) => device.name).join(", ") : "none"; + + throw new Error(`BrainAccess device "${DEVICE_NAME}" was not found. Discovered: ${found}.`); } this.setStatus({ @@ -314,9 +294,7 @@ export class BrainAccessEEGAdapter implements EEGDeviceAdapter { async getStreamInfo(): Promise> { if (!this.streamInfo) { - throw new Error( - "BrainAccess stream metadata is available after the device connects.", - ); + throw new Error("BrainAccess stream metadata is available after the device connects."); } return { @@ -338,9 +316,7 @@ export class BrainAccessEEGAdapter implements EEGDeviceAdapter { } if (this.status.state !== "connected") { - throw new Error( - "BrainAccess MAXI 009 must be connected before streaming can start.", - ); + throw new Error("BrainAccess MAXI 009 must be connected before streaming can start."); } this.streaming = true; @@ -403,4 +379,4 @@ export class BrainAccessEEGAdapter implements EEGDeviceAdapter { listener(snapshot); } } -} \ No newline at end of file +} diff --git a/src/core/devices/adapters/brainlink/BrainLinkAdapter.ts b/src/core/devices/adapters/brainlink/BrainLinkAdapter.ts index dae8e00..a7f5f9a 100644 --- a/src/core/devices/adapters/brainlink/BrainLinkAdapter.ts +++ b/src/core/devices/adapters/brainlink/BrainLinkAdapter.ts @@ -1,10 +1,6 @@ -import type { - DeviceStatus, -} from "../../models/device"; +import type { DeviceStatus } from "../../models/device"; -import type { - EEGDeviceInfo, -} from "../../models/eeg"; +import type { EEGDeviceInfo } from "../../models/eeg"; import type { DeviceAdapter, @@ -12,19 +8,11 @@ import type { DeviceUnsubscribe, } from "../../contracts/DeviceAdapter"; -import type { - BrainMetricsDeviceAdapter, -} from "../../contracts/BrainMetricsDeviceAdapter"; +import type { BrainMetricsDeviceAdapter } from "../../contracts/BrainMetricsDeviceAdapter"; -import type { - BrainMetricsListener, - BrainMetricsSnapshot, -} from "../../../brainMetrics"; +import type { BrainMetricsListener, BrainMetricsSnapshot } from "../../../brainMetrics"; -import { - BrainLinkBridge, - type BrainLinkBridgeLike, -} from "./BrainLinkBridge"; +import { BrainLinkBridge, type BrainLinkBridgeLike } from "./BrainLinkBridge"; const DEVICE_INFO: EEGDeviceInfo = { id: "brainlink-lite", @@ -42,9 +30,7 @@ const DEVICE_INFO: EEGDeviceInfo = { * attention/meditation metrics instead of pretending that Neuorrun requires * FBCCA or BrainAccess raw EEG. */ -export class BrainLinkAdapter - implements DeviceAdapter, BrainMetricsDeviceAdapter -{ +export class BrainLinkAdapter implements DeviceAdapter, BrainMetricsDeviceAdapter { readonly info: Readonly = DEVICE_INFO; private status: DeviceStatus = { @@ -63,9 +49,7 @@ export class BrainLinkAdapter private readonly bridgeDisconnectUnsubscribe: () => void; - constructor( - private readonly bridge: BrainLinkBridgeLike = new BrainLinkBridge(), - ) { + constructor(private readonly bridge: BrainLinkBridgeLike = new BrainLinkBridge()) { this.bridgeMetricsUnsubscribe = this.bridge.subscribeMetrics((metrics) => { this.latestMetrics = { ...metrics }; @@ -74,18 +58,16 @@ export class BrainLinkAdapter } }); - this.bridgeDisconnectUnsubscribe = this.bridge.subscribeDisconnected( - (reason) => { - this.latestMetrics = null; - - this.setStatus({ - state: "error", - error: reason, - message: "BrainLink Lite Bluetooth connection was lost.", - updatedAt: Date.now(), - }); - }, - ); + this.bridgeDisconnectUnsubscribe = this.bridge.subscribeDisconnected((reason) => { + this.latestMetrics = null; + + this.setStatus({ + state: "error", + error: reason, + message: "BrainLink Lite Bluetooth connection was lost.", + updatedAt: Date.now(), + }); + }); } getStatus(): Readonly { @@ -188,9 +170,7 @@ export class BrainLinkAdapter return this.latestMetrics ? { ...this.latestMetrics } : null; } - subscribeBrainMetrics( - listener: BrainMetricsListener, - ): DeviceUnsubscribe { + subscribeBrainMetrics(listener: BrainMetricsListener): DeviceUnsubscribe { this.metricsListeners.add(listener); return () => { @@ -198,9 +178,7 @@ export class BrainLinkAdapter }; } - subscribeStatus( - listener: DeviceStatusListener, - ): DeviceUnsubscribe { + subscribeStatus(listener: DeviceStatusListener): DeviceUnsubscribe { this.statusListeners.add(listener); return () => { diff --git a/src/core/devices/adapters/brainlink/BrainLinkBridge.ts b/src/core/devices/adapters/brainlink/BrainLinkBridge.ts index 698aa59..f0edd10 100644 --- a/src/core/devices/adapters/brainlink/BrainLinkBridge.ts +++ b/src/core/devices/adapters/brainlink/BrainLinkBridge.ts @@ -1,15 +1,8 @@ -import { - Command, - type Child, -} from "@tauri-apps/plugin-shell"; +import { Command, type Child } from "@tauri-apps/plugin-shell"; -import type { - BrainMetricsSnapshot, -} from "../../../brainMetrics"; +import type { BrainMetricsSnapshot } from "../../../brainMetrics"; -import { - logger, -} from "../../../../lib/logger"; +import { logger } from "../../../../lib/logger"; export interface BrainLinkPortInfo { port: string; @@ -25,13 +18,9 @@ export interface BrainLinkConnectionInfo { baudRate: number; } -export type BrainLinkMetricsListener = ( - metrics: Readonly, -) => void; +export type BrainLinkMetricsListener = (metrics: Readonly) => void; -export type BrainLinkDisconnectListener = ( - reason: string, -) => void; +export type BrainLinkDisconnectListener = (reason: string) => void; export interface BrainLinkBridgeLike { start(): Promise; @@ -39,21 +28,15 @@ export interface BrainLinkBridgeLike { scan(): Promise; - connect( - port?: string, - ): Promise; + connect(port?: string): Promise; disconnect(): Promise; getMetrics(): Promise | null>; - subscribeMetrics( - listener: BrainLinkMetricsListener, - ): () => void; + subscribeMetrics(listener: BrainLinkMetricsListener): () => void; - subscribeDisconnected( - listener: BrainLinkDisconnectListener, - ): () => void; + subscribeDisconnected(listener: BrainLinkDisconnectListener): () => void; } interface BridgeResponse { @@ -149,10 +132,7 @@ export class BrainLinkBridge implements BrainLinkBridgeLike { try { await this.request("shutdown"); } catch (error) { - logger.warning( - "BrainLinkBridge", - `Graceful shutdown failed: ${this.getErrorMessage(error)}`, - ); + logger.warning("BrainLinkBridge", `Graceful shutdown failed: ${this.getErrorMessage(error)}`); } if (this.child) { @@ -171,10 +151,7 @@ export class BrainLinkBridge implements BrainLinkBridgeLike { } async connect(port?: string): Promise { - return this.request( - "connect", - port ? { port } : {}, - ); + return this.request("connect", port ? { port } : {}); } async disconnect(): Promise { @@ -185,9 +162,7 @@ export class BrainLinkBridge implements BrainLinkBridgeLike { return this.request("get_metrics"); } - subscribeMetrics( - listener: BrainLinkMetricsListener, - ): () => void { + subscribeMetrics(listener: BrainLinkMetricsListener): () => void { this.metricsListeners.add(listener); return () => { @@ -195,9 +170,7 @@ export class BrainLinkBridge implements BrainLinkBridgeLike { }; } - subscribeDisconnected( - listener: BrainLinkDisconnectListener, - ): () => void { + subscribeDisconnected(listener: BrainLinkDisconnectListener): () => void { this.disconnectListeners.add(listener); return () => { @@ -225,9 +198,7 @@ export class BrainLinkBridge implements BrainLinkBridgeLike { }); command.on("close", (event) => { - this.handleProcessClosed( - `BrainLink sidecar exited with code ${event.code ?? "unknown"}.`, - ); + this.handleProcessClosed(`BrainLink sidecar exited with code ${event.code ?? "unknown"}.`); }); this.child = await command.spawn(); @@ -294,10 +265,7 @@ export class BrainLinkBridge implements BrainLinkBridgeLike { try { this.handleMessage(JSON.parse(trimmed) as BridgeMessage); } catch (error) { - logger.error( - "BrainLinkBridge", - `Invalid sidecar JSON: ${this.getErrorMessage(error)}`, - ); + logger.error("BrainLinkBridge", `Invalid sidecar JSON: ${this.getErrorMessage(error)}`); } } } @@ -305,10 +273,7 @@ export class BrainLinkBridge implements BrainLinkBridgeLike { private handleMessage(message: BridgeMessage): void { switch (message.type) { case "ready": - logger.debug( - "BrainLinkBridge", - `Protocol ready: v${message.protocolVersion}`, - ); + logger.debug("BrainLinkBridge", `Protocol ready: v${message.protocolVersion}`); return; case "response": { @@ -323,9 +288,7 @@ export class BrainLinkBridge implements BrainLinkBridgeLike { if (message.ok) { pending.resolve(message.result); } else { - pending.reject( - new Error(message.error ?? "BrainLink bridge request failed."), - ); + pending.reject(new Error(message.error ?? "BrainLink bridge request failed.")); } return; @@ -358,10 +321,7 @@ export class BrainLinkBridge implements BrainLinkBridgeLike { } case "bridge-error": - logger.error( - "BrainLinkBridge", - message.message ?? "Unknown BrainLink bridge error.", - ); + logger.error("BrainLinkBridge", message.message ?? "Unknown BrainLink bridge error."); return; } } diff --git a/src/core/devices/adapters/brainlink/index.ts b/src/core/devices/adapters/brainlink/index.ts index f2b4ecc..169c623 100644 --- a/src/core/devices/adapters/brainlink/index.ts +++ b/src/core/devices/adapters/brainlink/index.ts @@ -1,6 +1,4 @@ -export { - BrainLinkAdapter, -} from "./BrainLinkAdapter"; +export { BrainLinkAdapter } from "./BrainLinkAdapter"; export { BrainLinkBridge, diff --git a/src/core/devices/adapters/brainlink/tests/BrainLinkAdapter.test.ts b/src/core/devices/adapters/brainlink/tests/BrainLinkAdapter.test.ts index ff1e0f4..5695613 100644 --- a/src/core/devices/adapters/brainlink/tests/BrainLinkAdapter.test.ts +++ b/src/core/devices/adapters/brainlink/tests/BrainLinkAdapter.test.ts @@ -1,16 +1,8 @@ -import { - describe, - expect, - it, -} from "vitest"; +import { describe, expect, it } from "vitest"; -import type { - BrainMetricsSnapshot, -} from "../../../../brainMetrics"; +import type { BrainMetricsSnapshot } from "../../../../brainMetrics"; -import { - BrainLinkAdapter, -} from "../BrainLinkAdapter"; +import { BrainLinkAdapter } from "../BrainLinkAdapter"; import type { BrainLinkBridgeLike, diff --git a/src/core/devices/contracts/BrainMetricsDeviceAdapter.ts b/src/core/devices/contracts/BrainMetricsDeviceAdapter.ts index 7c52a7f..8f8cbfa 100644 --- a/src/core/devices/contracts/BrainMetricsDeviceAdapter.ts +++ b/src/core/devices/contracts/BrainMetricsDeviceAdapter.ts @@ -1,12 +1,6 @@ -import type { - BrainMetricsListener, - BrainMetricsSnapshot, -} from "../../brainMetrics/models"; +import type { BrainMetricsListener, BrainMetricsSnapshot } from "../../brainMetrics/models"; -import type { - DeviceAdapter, - DeviceUnsubscribe, -} from "./DeviceAdapter"; +import type { DeviceAdapter, DeviceUnsubscribe } from "./DeviceAdapter"; /** * Optional capability implemented by devices that expose already-computed @@ -18,9 +12,7 @@ import type { export interface BrainMetricsDeviceAdapter extends DeviceAdapter { getLatestBrainMetrics(): Readonly | null; - subscribeBrainMetrics( - listener: BrainMetricsListener, - ): DeviceUnsubscribe; + subscribeBrainMetrics(listener: BrainMetricsListener): DeviceUnsubscribe; } export function isBrainMetricsDeviceAdapter( diff --git a/src/core/devices/registerBuiltInDevices.ts b/src/core/devices/registerBuiltInDevices.ts index cab4a07..a3fc1c6 100644 --- a/src/core/devices/registerBuiltInDevices.ts +++ b/src/core/devices/registerBuiltInDevices.ts @@ -1,29 +1,31 @@ -import { - BrainAccessEEGAdapter, -} from "./adapters/brainaccess/BrainAccessEEGAdapter"; +import { BrainAccessEEGAdapter } from "./adapters/brainaccess/BrainAccessEEGAdapter"; -import { - BrainLinkAdapter, -} from "./adapters/brainlink/BrainLinkAdapter"; +import { BrainLinkAdapter } from "./adapters/brainlink/BrainLinkAdapter"; -import type { - DeviceAdapter, -} from "./contracts/DeviceAdapter"; +import { SimulationBrainMetricsAdapter } from "./adapters/simulation/SimulationBrainMetricsAdapter"; -import { - deviceRegistry, -} from "./deviceRegistry"; +import { SimulationEEGAdapter } from "./adapters/simulation/SimulationEEGAdapter"; + +import type { DeviceAdapter } from "./contracts/DeviceAdapter"; + +import { deviceRegistry } from "./deviceRegistry"; + +import { APP_CONFIG } from "../../config/appConfig"; /** * Production hardware bundled with KNeuron. * - * Simulation EEG was intentionally removed from the production Device page. + * Simulation devices are registered only in development builds so the UI + * and modules can be worked on without physical hardware. */ function createBuiltInDevices(): DeviceAdapter[] { - return [ - new BrainAccessEEGAdapter(), - new BrainLinkAdapter(), - ]; + const devices: DeviceAdapter[] = [new BrainAccessEEGAdapter(), new BrainLinkAdapter()]; + + if (APP_CONFIG.enableSimulationDevices) { + devices.push(new SimulationEEGAdapter(), new SimulationBrainMetricsAdapter()); + } + + return devices; } export function registerBuiltInDevices(): void { diff --git a/src/core/eeg/EEGStreamService.test.ts b/src/core/eeg/EEGStreamService.test.ts index 1b5fc5b..846aaeb 100644 --- a/src/core/eeg/EEGStreamService.test.ts +++ b/src/core/eeg/EEGStreamService.test.ts @@ -56,7 +56,8 @@ describe("EEGStreamService", () => { await handle.release(); - expect(service.isStreaming()).toBe(false); + // The physical EEG stream remains active while the device is connected. + expect(service.isStreaming()).toBe(true); }); it("allows selecting six channels from a 32-channel stream", async () => { @@ -95,7 +96,8 @@ describe("EEGStreamService", () => { await second.release(); - expect(adapter.isStreaming()).toBe(false); + // Releasing the last consumer does not stop the physical device stream. + expect(adapter.isStreaming()).toBe(true); }); it("delivers only selected channels to a consumer", async () => { diff --git a/src/core/eeg/EEGStreamService.ts b/src/core/eeg/EEGStreamService.ts index 59a840d..7a11981 100644 --- a/src/core/eeg/EEGStreamService.ts +++ b/src/core/eeg/EEGStreamService.ts @@ -11,7 +11,6 @@ import type { EEGSampleBatch, EEGWindow, } from "./models"; - export interface EEGStreamRequest { /** * "all" means every channel exposed by the current EEG device. @@ -19,73 +18,36 @@ export interface EEGStreamRequest { * A string array requests only selected channel labels. */ channels: "all" | readonly string[]; - /** * Optional real-time callback. */ onBatch?: EEGConsumerBatchListener; } - export interface EEGStreamHandle { readonly id: number; - readonly channels: readonly Readonly[]; - readonly streamInfo: Readonly; - /** * Releases this consumer. * - * The physical stream is stopped only after the final consumer releases its - * handle and the short hand-off grace period expires. + * Releasing a module does not stop the physical EEG stream. + * The stream remains active while the EEG device stays connected, + * allowing another module to reuse it without restarting Bluetooth. */ release(): Promise; } - interface ConsumerRecord { channels: readonly Readonly[]; - channelIndices: readonly number[]; - listener?: EEGConsumerBatchListener; } - interface EEGStreamServiceOptions { /** * Number of seconds retained in the shared history buffer. */ bufferDurationSeconds?: number; - - /** - * How long the service keeps the physical EEG stream alive after the final - * consumer releases it. - * - * This prevents a rapid Cortex -> Miner transition from racing - * stopStream() against startStream(). - */ - stopGracePeriodMs?: number; } - -interface PendingStop { - timer: ReturnType | null; - - cancelled: boolean; - - promise: Promise; - - resolve: () => void; - - reject: (reason?: unknown) => void; -} - const DEFAULT_BUFFER_DURATION_SECONDS = 15; - -/** - * Short enough to be invisible to the user, but long enough for React to - * unmount one module and mount the next module without restarting Bluetooth. - */ -const DEFAULT_STOP_GRACE_PERIOD_MS = 400; - /** * Shared application-level EEG stream coordinator. * @@ -99,28 +61,22 @@ const DEFAULT_STOP_GRACE_PERIOD_MS = 400; * | * +--> SSVEP: O1/O2/Oz/PO3/PO4/POz * - * Important lifecycle rule: + * Lifecycle rule: * - * - startStream() and stopStream() are serialized; - * - after the final consumer releases the stream, shutdown is delayed briefly; - * - if another module acquires EEG during that delay, shutdown is cancelled. - * - * This keeps module transitions such as Cortex -> Miner from producing a - * stop/start race and partial SSVEP trials. + * - the first consumer starts the physical EEG stream; + * - additional consumers reuse the existing stream; + * - releasing consumers does not stop the physical stream; + * - the stream remains active while the EEG device is connected; + * - explicit disposal or device teardown stops the physical stream; + * - physical lifecycle operations are serialized to prevent start/stop races. */ export class EEGStreamService { private readonly consumers = new Map(); - private nextConsumerId = 1; - private activeAdapter: EEGDeviceAdapter | null = null; - private streamInfo: Readonly | null = null; - private ringBuffer: EEGRingBuffer | null = null; - private sampleUnsubscribe: (() => void) | null = null; - /** * All physical adapter lifecycle operations run through this queue. * @@ -132,59 +88,34 @@ export class EEGStreamService { * executing at the same time. */ private lifecycleQueue: Promise = Promise.resolve(); - - /** - * Delayed stop waiting for a possible next module. - */ - private pendingStop: PendingStop | null = null; - private readonly bufferDurationSeconds: number; - - private readonly stopGracePeriodMs: number; - constructor( private readonly manager: DeviceManager = deviceManager, options: EEGStreamServiceOptions = {}, ) { - this.bufferDurationSeconds = - options.bufferDurationSeconds ?? DEFAULT_BUFFER_DURATION_SECONDS; - - this.stopGracePeriodMs = - options.stopGracePeriodMs ?? DEFAULT_STOP_GRACE_PERIOD_MS; - + this.bufferDurationSeconds = options.bufferDurationSeconds ?? DEFAULT_BUFFER_DURATION_SECONDS; if (this.bufferDurationSeconds <= 0) { throw new Error("EEG stream buffer duration must be greater than zero."); } - - if (this.stopGracePeriodMs < 0) { - throw new Error("EEG stream stop grace period cannot be negative."); - } } - /** * Acquires access to the shared EEG stream. * * The first consumer starts the adapter stream. * Additional consumers reuse the same physical stream. - * - * A pending delayed shutdown is cancelled immediately. This allows the next - * module to inherit the already-running BrainAccess stream. + * A running stream is reused across module transitions. */ async acquire(request: EEGStreamRequest): Promise { const adapter = this.manager.getActiveAdapter(); - if (!adapter) { throw new Error("No active EEG device is connected."); } - if (!isEEGDeviceAdapter(adapter)) { throw new Error(`Active device "${adapter.info.id}" is not an EEG device.`); } - if (adapter.getStatus().state !== "connected") { throw new Error(`EEG device "${adapter.info.id}" is not connected.`); } - if ( this.activeAdapter && this.activeAdapter.info.id !== adapter.info.id && @@ -192,37 +123,18 @@ export class EEGStreamService { ) { throw new Error("Cannot switch EEG devices while stream consumers are active."); } - - /** - * If Cortex has just released EEG and Miner is opening, keep the physical - * stream alive rather than stopping and immediately restarting it. - */ - this.cancelPendingStop(); - await this.ensureStreaming(adapter); - if (!this.streamInfo) { throw new Error("EEG stream metadata is unavailable."); } - const selection = this.resolveSelection(this.streamInfo, request.channels); - const consumerId = this.nextConsumerId++; - this.consumers.set(consumerId, { channels: selection.channels, channelIndices: selection.indices, listener: request.onBatch, }); - - /** - * A final-consumer release can race the asynchronous acquire above. - * Cancelling again after registration closes that small window. - */ - this.cancelPendingStop(); - let released = false; - return { id: consumerId, channels: selection.channels, @@ -231,14 +143,11 @@ export class EEGStreamService { if (released) { return Promise.resolve(); } - released = true; - return this.releaseConsumer(consumerId); }, }; } - /** * Reads the latest history window from the shared ring buffer. * @@ -251,22 +160,12 @@ export class EEGStreamService { if (durationSeconds <= 0) { throw new Error("EEG window duration must be greater than zero."); } - if (!this.streamInfo || !this.ringBuffer) { throw new Error("EEG stream is not active."); } - const selection = this.resolveSelection(this.streamInfo, requestedChannels); - - const requestedSamples = Math.ceil( - durationSeconds * this.streamInfo.sampleRateHz, - ); - - const buffered = this.ringBuffer.readLatest( - requestedSamples, - selection.indices, - ); - + const requestedSamples = Math.ceil(durationSeconds * this.streamInfo.sampleRateHz); + const buffered = this.ringBuffer.readLatest(requestedSamples, selection.indices); return { sampleRateHz: this.streamInfo.sampleRateHz, sampleCount: buffered.sampleCount, @@ -277,31 +176,23 @@ export class EEGStreamService { values: buffered.values, }; } - getConsumerCount(): number { return this.consumers.size; } - isStreaming(): boolean { return this.activeAdapter?.isStreaming() ?? false; } - /** * Mainly intended for tests and application shutdown. * - * dispose() bypasses the hand-off grace period because the service is being - * explicitly torn down. + * dispose() explicitly stops the physical stream and clears all consumers. */ async dispose(): Promise { this.consumers.clear(); - - this.cancelPendingStop(); - await this.runLifecycle(async () => { await this.deactivateAdapterUnsafe(); }); } - /** * Makes sure the requested adapter is streaming. * @@ -318,7 +209,6 @@ export class EEGStreamService { ) { return; } - /** * Clean up any incomplete or different previous stream before starting. * Because this method runs inside lifecycleQueue, stop/start cannot @@ -327,35 +217,22 @@ export class EEGStreamService { if (this.activeAdapter) { await this.deactivateAdapterUnsafe(); } - await this.activateAdapterUnsafe(adapter); }); } - /** * Starts one adapter. * * Must only be called from inside lifecycleQueue. */ - private async activateAdapterUnsafe( - adapter: EEGDeviceAdapter, - ): Promise { + private async activateAdapterUnsafe(adapter: EEGDeviceAdapter): Promise { const streamInfo = await adapter.getStreamInfo(); - this.validateStreamInfo(streamInfo); - const capacitySamples = Math.max( 1, - Math.ceil( - streamInfo.sampleRateHz * this.bufferDurationSeconds, - ), - ); - - const ringBuffer = new EEGRingBuffer( - streamInfo.channels.length, - capacitySamples, + Math.ceil(streamInfo.sampleRateHz * this.bufferDurationSeconds), ); - + const ringBuffer = new EEGRingBuffer(streamInfo.channels.length, capacitySamples); /** * Publish the stream state before startStream(), because the adapter can * begin emitting samples immediately during startup. @@ -363,37 +240,29 @@ export class EEGStreamService { this.activeAdapter = adapter; this.streamInfo = streamInfo; this.ringBuffer = ringBuffer; - const unsubscribe = adapter.subscribeSamples((batch) => { this.handleBatch(batch); }); - this.sampleUnsubscribe = unsubscribe; - try { await adapter.startStream(); - logger.info( "EEGStreamService", `EEG stream started: ${adapter.info.id} (${streamInfo.sampleRateHz} Hz, ${streamInfo.channels.length} channels)`, ); } catch (error) { unsubscribe(); - if (this.sampleUnsubscribe === unsubscribe) { this.sampleUnsubscribe = null; } - if (this.activeAdapter === adapter) { this.activeAdapter = null; this.streamInfo = null; this.ringBuffer = null; } - throw error; } } - /** * Stops the current physical EEG stream. * @@ -402,46 +271,33 @@ export class EEGStreamService { private async deactivateAdapterUnsafe(): Promise { const adapter = this.activeAdapter; const unsubscribe = this.sampleUnsubscribe; - if (!adapter) { unsubscribe?.(); - this.sampleUnsubscribe = null; this.streamInfo = null; this.ringBuffer = null; - return; } - /** * Stop forwarding samples before shutting down the physical stream. * The adapter lifecycle itself remains serialized, so a new start cannot * begin until this stop completes. */ unsubscribe?.(); - if (this.sampleUnsubscribe === unsubscribe) { this.sampleUnsubscribe = null; } - try { if (adapter.isStreaming()) { await adapter.stopStream(); - - logger.info( - "EEGStreamService", - `EEG stream stopped: ${adapter.info.id}`, - ); + logger.info("EEGStreamService", `EEG stream stopped: ${adapter.info.id}`); } } catch (error) { - const message = - error instanceof Error ? error.message : String(error); - + const message = error instanceof Error ? error.message : String(error); logger.error( "EEGStreamService", `Failed to stop EEG stream "${adapter.info.id}": ${message}`, ); - throw error; } finally { /** @@ -455,20 +311,18 @@ export class EEGStreamService { } } } - /** * Removes one consumer. * - * The final consumer does not stop Bluetooth immediately. A short delayed - * stop is scheduled so another module can acquire the same stream. + * Releasing the final consumer does not stop the physical EEG stream. + * The connected device owns the physical stream lifecycle; modules only + * attach and detach consumers. */ private releaseConsumer(consumerId: number): Promise { const removed = this.consumers.delete(consumerId); - if (!removed) { return Promise.resolve(); } - /** * Do not stop the physical EEG stream when a module closes. * @@ -486,97 +340,6 @@ export class EEGStreamService { */ return Promise.resolve(); } - - /** - * Delays physical shutdown to support seamless module hand-off. - * - * If another consumer appears during the grace period, acquire() cancels - * this pending stop and the same BrainAccess stream continues. - */ - private scheduleDeactivateAdapter(): Promise { - if (this.pendingStop) { - return this.pendingStop.promise; - } - - let resolveStop: () => void = () => undefined; - let rejectStop: (reason?: unknown) => void = () => undefined; - - const promise = new Promise((resolve, reject) => { - resolveStop = resolve; - rejectStop = reject; - }); - - const pending: PendingStop = { - timer: null, - cancelled: false, - promise, - resolve: resolveStop, - reject: rejectStop, - }; - - pending.timer = setTimeout(() => { - pending.timer = null; - - void this.runLifecycle(async () => { - /** - * The timer may have fired just as the next module acquired EEG. - */ - if ( - pending.cancelled || - this.pendingStop !== pending || - this.consumers.size > 0 - ) { - return; - } - - await this.deactivateAdapterUnsafe(); - }) - .then(() => { - pending.resolve(); - }) - .catch((error: unknown) => { - pending.reject(error); - }) - .finally(() => { - if (this.pendingStop === pending) { - this.pendingStop = null; - } - }); - }, this.stopGracePeriodMs); - - this.pendingStop = pending; - - return pending.promise; - } - - /** - * Cancels a delayed shutdown when a new module wants EEG. - */ - private cancelPendingStop(): void { - const pending = this.pendingStop; - - if (!pending) { - return; - } - - pending.cancelled = true; - - if (pending.timer) { - clearTimeout(pending.timer); - pending.timer = null; - } - - /** - * release() may be awaiting the pending stop. Cancellation means the - * hand-off succeeded, so that release operation is complete. - */ - pending.resolve(); - - if (this.pendingStop === pending) { - this.pendingStop = null; - } - } - /** * Serializes adapter lifecycle operations. * @@ -585,46 +348,33 @@ export class EEGStreamService { */ private runLifecycle(operation: () => Promise): Promise { const result = this.lifecycleQueue.then(operation); - this.lifecycleQueue = result.then( () => undefined, () => undefined, ); - return result; } - private handleBatch(batch: Readonly): void { if (!this.streamInfo || !this.ringBuffer) { return; } - try { if (batch.sampleRateHz !== this.streamInfo.sampleRateHz) { throw new Error( `EEG sample rate changed from ${this.streamInfo.sampleRateHz} Hz to ${batch.sampleRateHz} Hz.`, ); } - if (batch.channelCount !== this.streamInfo.channels.length) { throw new Error( `EEG batch channel count changed from ${this.streamInfo.channels.length} to ${batch.channelCount}.`, ); } - this.ringBuffer.push(batch); } catch (error) { - const message = - error instanceof Error ? error.message : String(error); - - logger.error( - "EEGStreamService", - `Rejected EEG batch: ${message}`, - ); - + const message = error instanceof Error ? error.message : String(error); + logger.error("EEGStreamService", `Rejected EEG batch: ${message}`); return; } - /** * One module callback must not be able to prevent other consumers from * receiving the same EEG batch. @@ -633,33 +383,23 @@ export class EEGStreamService { if (!consumer.listener) { continue; } - const consumerBatch: EEGConsumerBatch = { sequenceStart: batch.sequenceStart, timestampStartMs: batch.timestampStartMs, sampleRateHz: batch.sampleRateHz, sampleCount: batch.sampleCount, channels: consumer.channels, - values: consumer.channelIndices.map((channelIndex) => [ - ...batch.values[channelIndex], - ]), + values: consumer.channelIndices.map((channelIndex) => [...batch.values[channelIndex]]), sourceSampleNumberStart: batch.sourceSampleNumberStart, }; - try { consumer.listener(consumerBatch); } catch (error) { - const message = - error instanceof Error ? error.message : String(error); - - logger.error( - "EEGStreamService", - `EEG consumer ${consumerId} rejected a batch: ${message}`, - ); + const message = error instanceof Error ? error.message : String(error); + logger.error("EEGStreamService", `EEG consumer ${consumerId} rejected a batch: ${message}`); } } } - private resolveSelection( streamInfo: Readonly, requested: "all" | readonly string[], @@ -675,15 +415,10 @@ export class EEGStreamService { indices: streamInfo.channels.map((_, index) => index), }; } - const resolution = resolveEEGChannels(streamInfo, requested); - if (!resolution.complete) { - throw new Error( - `Missing required EEG channels: ${resolution.missingLabels.join(", ")}.`, - ); + throw new Error(`Missing required EEG channels: ${resolution.missingLabels.join(", ")}.`); } - return { channels: resolution.resolved.map((entry) => ({ ...entry.channel, @@ -691,24 +426,14 @@ export class EEGStreamService { indices: resolution.resolved.map((entry) => entry.streamIndex), }; } - - private validateStreamInfo( - streamInfo: Readonly, - ): void { + private validateStreamInfo(streamInfo: Readonly): void { if (streamInfo.sampleRateHz <= 0) { - throw new Error( - "EEG stream sample rate must be greater than zero.", - ); + throw new Error("EEG stream sample rate must be greater than zero."); } - if (streamInfo.channels.length === 0) { - throw new Error( - "EEG stream must expose at least one channel.", - ); + throw new Error("EEG stream must expose at least one channel."); } - const labels = new Set(); - streamInfo.channels.forEach((channel, index) => { /** * Normalized channel index must correspond directly to the @@ -719,21 +444,14 @@ export class EEGStreamService { `EEG channel "${channel.label}" has index ${channel.index}, expected ${index}.`, ); } - - const normalizedLabel = - channel.label.trim().toUpperCase(); - + const normalizedLabel = channel.label.trim().toUpperCase(); if (labels.has(normalizedLabel)) { - throw new Error( - `Duplicate EEG channel label "${channel.label}".`, - ); + throw new Error(`Duplicate EEG channel label "${channel.label}".`); } - labels.add(normalizedLabel); }); } } - /** * Shared application-wide EEG stream service. */ diff --git a/src/core/ssvep/SsvepClassifierBridge.ts b/src/core/ssvep/SsvepClassifierBridge.ts index 46e55a8..d10e1d1 100644 --- a/src/core/ssvep/SsvepClassifierBridge.ts +++ b/src/core/ssvep/SsvepClassifierBridge.ts @@ -1,31 +1,16 @@ -import { - Command, - type Child, -} from "@tauri-apps/plugin-shell"; +import { Command, type Child } from "@tauri-apps/plugin-shell"; -import { - logger, -} from "../../lib/logger"; +import { logger } from "../../lib/logger"; export interface SsvepClassifierRequest { - eeg: - readonly ( - readonly number[] - )[]; + eeg: readonly (readonly number[])[]; sampleRateHz: number; - frequencies: - readonly number[]; + frequencies: readonly number[]; } export interface SsvepClassifierResponse { winnerHz: number; - scores: - Readonly< - Record< - string, - number - > - >; + scores: Readonly>; sampleRateHz: number; channelCount: number; sampleCount: number; @@ -44,19 +29,11 @@ interface BridgeReady { protocolVersion: number; } -type BridgeMessage = - | BridgeResponse - | BridgeReady; +type BridgeMessage = BridgeResponse | BridgeReady; interface PendingRequest { - resolve: - ( - value: unknown, - ) => void; - reject: - ( - error: Error, - ) => void; + resolve: (value: unknown) => void; + reject: (error: Error) => void; } /** @@ -66,33 +43,18 @@ interface PendingRequest { * stay aligned with the supplied working TaaLON Python implementation. */ export class SsvepClassifierBridge { - private child: - Child | null = null; + private child: Child | null = null; - private starting: - Promise | null = - null; + private starting: Promise | null = null; private requestSequence = 0; private stdoutBuffer = ""; - private readonly pending = - new Map< - string, - PendingRequest - >(); - - async classify( - request: - SsvepClassifierRequest, - ): Promise { - return this.request< - SsvepClassifierResponse - >( - "classify", - request, - ); + private readonly pending = new Map(); + + async classify(request: SsvepClassifierRequest): Promise { + return this.request("classify", request); } async start(): Promise { @@ -105,29 +67,24 @@ export class SsvepClassifierBridge { return; } - this.starting = - this.spawn(); + this.starting = this.spawn(); try { await this.starting; } finally { - this.starting = - null; + this.starting = null; } } async stop(): Promise { - const child = - this.child; + const child = this.child; if (!child) { return; } try { - await this.request( - "shutdown", - ); + await this.request("shutdown"); } catch (error) { logger.warning( "SsvepClassifierBridge", @@ -143,166 +100,93 @@ export class SsvepClassifierBridge { } } - this.close( - "SSVEP classifier sidecar stopped.", - ); + this.close("SSVEP classifier sidecar stopped."); } - private async spawn(): - Promise { - const command = - Command.sidecar( - "binaries/ssvep-classifier", - ); + private async spawn(): Promise { + const command = Command.sidecar("binaries/ssvep-classifier"); - command.stdout.on( - "data", - (data) => { - this.handleStdout( - String(data), - ); - }, - ); - - command.stderr.on( - "data", - (data) => { - const message = - String(data).trim(); - - if (message) { - logger.debug( - "SsvepClassifierBridge", - message, - ); - } - }, - ); - - command.on( - "error", - (error) => { - this.close( - `SSVEP classifier error: ${String(error)}`, - ); - }, - ); - - command.on( - "close", - (event) => { - this.close( - `SSVEP classifier exited with code ${event.code ?? "unknown"}.`, - ); - }, - ); + command.stdout.on("data", (data) => { + this.handleStdout(String(data)); + }); + + command.stderr.on("data", (data) => { + const message = String(data).trim(); + + if (message) { + logger.debug("SsvepClassifierBridge", message); + } + }); + + command.on("error", (error) => { + this.close(`SSVEP classifier error: ${String(error)}`); + }); + + command.on("close", (event) => { + this.close(`SSVEP classifier exited with code ${event.code ?? "unknown"}.`); + }); - this.child = - await command.spawn(); + this.child = await command.spawn(); - await this.request( - "ping", - ); + await this.request("ping"); - logger.info( - "SsvepClassifierBridge", - "SSVEP classifier sidecar started.", - ); + logger.info("SsvepClassifierBridge", "SSVEP classifier sidecar started."); } - private async request< - T = unknown, - >( - command: string, - payload?: - unknown, - ): Promise { + private async request(command: string, payload?: unknown): Promise { if (!this.child) { await this.start(); } - const child = - this.child; + const child = this.child; if (!child) { - throw new Error( - "SSVEP classifier sidecar is not running.", - ); + throw new Error("SSVEP classifier sidecar is not running."); } - const id = - `ssvep-${++this.requestSequence}`; - - const result = - new Promise( - ( - resolve, - reject, - ) => { - this.pending.set( - id, - { - resolve: - (value) => { - resolve( - value as T, - ); - }, - reject, - }, - ); + const id = `ssvep-${++this.requestSequence}`; + + const result = new Promise((resolve, reject) => { + this.pending.set(id, { + resolve: (value) => { + resolve(value as T); }, - ); + reject, + }); + }); try { await child.write( `${JSON.stringify({ id, command, - payload: - payload ?? {}, + payload: payload ?? {}, })}\n`, ); } catch (error) { - this.pending.delete( - id, - ); + this.pending.delete(id); throw error; } return result; } - private handleStdout( - data: string, - ): void { - this.stdoutBuffer += - data; + private handleStdout(data: string): void { + this.stdoutBuffer += data; - const lines = - this.stdoutBuffer.split( - /\r?\n/u, - ); + const lines = this.stdoutBuffer.split(/\r?\n/u); - this.stdoutBuffer = - lines.pop() ?? ""; + this.stdoutBuffer = lines.pop() ?? ""; - for ( - const line of lines - ) { - const trimmed = - line.trim(); + for (const line of lines) { + const trimmed = line.trim(); if (!trimmed) { continue; } try { - this.handleMessage( - JSON.parse( - trimmed, - ) as BridgeMessage, - ); + this.handleMessage(JSON.parse(trimmed) as BridgeMessage); } catch (error) { logger.error( "SsvepClassifierBridge", @@ -312,88 +196,48 @@ export class SsvepClassifierBridge { } } - private handleMessage( - message: - BridgeMessage, - ): void { - if ( - message.type === - "ready" - ) { - logger.debug( - "SsvepClassifierBridge", - `Protocol ready: v${message.protocolVersion}`, - ); + private handleMessage(message: BridgeMessage): void { + if (message.type === "ready") { + logger.debug("SsvepClassifierBridge", `Protocol ready: v${message.protocolVersion}`); return; } - const pending = - this.pending.get( - message.id, - ); + const pending = this.pending.get(message.id); if (!pending) { return; } - this.pending.delete( - message.id, - ); + this.pending.delete(message.id); if (message.ok) { - pending.resolve( - message.result, - ); + pending.resolve(message.result); return; } - pending.reject( - new Error( - message.error ?? - "SSVEP classification failed.", - ), - ); + pending.reject(new Error(message.error ?? "SSVEP classification failed.")); } - private close( - reason: string, - ): void { - if ( - !this.child && - this.pending.size === - 0 - ) { + private close(reason: string): void { + if (!this.child && this.pending.size === 0) { return; } this.child = null; this.stdoutBuffer = ""; - const error = - new Error(reason); + const error = new Error(reason); - for ( - const pending of - this.pending.values() - ) { - pending.reject( - error, - ); + for (const pending of this.pending.values()) { + pending.reject(error); } this.pending.clear(); - logger.warning( - "SsvepClassifierBridge", - reason, - ); + logger.warning("SsvepClassifierBridge", reason); } - private errorMessage( - error: unknown, - ): string { - return error instanceof Error - ? error.message - : String(error); + private errorMessage(error: unknown): string { + return error instanceof Error ? error.message : String(error); } } diff --git a/src/core/ssvep/SsvepClassifierService.ts b/src/core/ssvep/SsvepClassifierService.ts index 66cd750..3a9602a 100644 --- a/src/core/ssvep/SsvepClassifierService.ts +++ b/src/core/ssvep/SsvepClassifierService.ts @@ -1,91 +1,46 @@ -import { - TAALON_DEFAULT_TARGETS, - validateTaalonFrequencies, -} from "./config"; +import { TAALON_DEFAULT_TARGETS, validateTaalonFrequencies } from "./config"; -import { - SsvepClassifierBridge, -} from "./SsvepClassifierBridge"; +import { SsvepClassifierBridge } from "./SsvepClassifierBridge"; -import type { - SsvepClassification, - SsvepDirection, - SsvepTarget, -} from "./models"; +import type { SsvepClassification, SsvepDirection, SsvepTarget } from "./models"; -function directionForWinner( - winnerHz: number, - targets: - readonly SsvepTarget[], -): SsvepDirection { - const target = - targets.find( - (candidate) => - Math.abs( - candidate.frequencyHz - - winnerHz, - ) < 1e-6, - ); +function directionForWinner(winnerHz: number, targets: readonly SsvepTarget[]): SsvepDirection { + const target = targets.find((candidate) => Math.abs(candidate.frequencyHz - winnerHz) < 1e-6); if (!target) { - throw new Error( - `Classifier returned unknown frequency ${winnerHz}.`, - ); + throw new Error(`Classifier returned unknown frequency ${winnerHz}.`); } return target.direction; } export class SsvepClassifierService { - constructor( - private readonly bridge = - new SsvepClassifierBridge(), - ) {} + constructor(private readonly bridge = new SsvepClassifierBridge()) {} async classify( - eeg: - readonly ( - readonly number[] - )[], + eeg: readonly (readonly number[])[], sampleRateHz: number, - targets: - readonly SsvepTarget[] = - TAALON_DEFAULT_TARGETS, + targets: readonly SsvepTarget[] = TAALON_DEFAULT_TARGETS, ): Promise { - const frequencies = - targets.map( - (target) => - target.frequencyHz, - ); + const frequencies = targets.map((target) => target.frequencyHz); - validateTaalonFrequencies( - frequencies, - ); + validateTaalonFrequencies(frequencies); - const response = - await this.bridge.classify( - { - eeg, - sampleRateHz, - frequencies, - }, - ); + const response = await this.bridge.classify({ + eeg, + sampleRateHz, + frequencies, + }); return { ...response, - direction: - directionForWinner( - response.winnerHz, - targets, - ), + direction: directionForWinner(response.winnerHz, targets), }; } - async stop(): - Promise { + async stop(): Promise { await this.bridge.stop(); } } -export const ssvepClassifierService = - new SsvepClassifierService(); +export const ssvepClassifierService = new SsvepClassifierService(); diff --git a/src/core/ssvep/config.ts b/src/core/ssvep/config.ts index a9b4029..6af2cec 100644 --- a/src/core/ssvep/config.ts +++ b/src/core/ssvep/config.ts @@ -1,6 +1,4 @@ -import type { - SsvepTarget, -} from "./models"; +import type { SsvepTarget } from "./models"; /** * TaaLON defaults from the supplied working project. @@ -12,8 +10,7 @@ import type { * 14.25 -> RIGHT * 14.75 -> DOWN */ -export const TAALON_DEFAULT_TARGETS: - readonly SsvepTarget[] = [ +export const TAALON_DEFAULT_TARGETS: readonly SsvepTarget[] = [ { direction: "UP", frequencyHz: 10.25, @@ -39,76 +36,33 @@ export const TAALON_DEFAULT_TARGETS: * KNeuron resolves them by normalized labels instead, so the classifier stays * independent of BrainAccess hardware numbering. */ -export const TAALON_SSVEP_CHANNELS = - [ - "POz", - "PO3", - "PO4", - "Oz", - "O1", - "O2", - ] as const; +export const TAALON_SSVEP_CHANNELS = ["POz", "PO3", "PO4", "Oz", "O1", "O2"] as const; -export const TAALON_DEFAULT_TRIAL_SECONDS = - 4; +export const TAALON_DEFAULT_TRIAL_SECONDS = 4; -export const TAALON_MIN_TRIAL_SECONDS = - 4; +export const TAALON_MIN_TRIAL_SECONDS = 4; -export const TAALON_MAX_TRIAL_SECONDS = - 10; +export const TAALON_MAX_TRIAL_SECONDS = 10; -export const TAALON_TRIAL_STEP_SECONDS = - 0.5; +export const TAALON_TRIAL_STEP_SECONDS = 0.5; -export const TAALON_COUNTDOWN_SECONDS = - 2; +export const TAALON_COUNTDOWN_SECONDS = 2; -export const TAALON_SAMPLE_TIMEOUT_SECONDS = - 3; +export const TAALON_SAMPLE_TIMEOUT_SECONDS = 3; -export function validateTaalonFrequencies( - values: readonly number[], -): void { +export function validateTaalonFrequencies(values: readonly number[]): void { if (values.length !== 4) { - throw new Error( - "Exactly four SSVEP frequencies are required.", - ); + throw new Error("Exactly four SSVEP frequencies are required."); } - if ( - values.some( - (value) => - !Number.isFinite(value) || - value < 6 || - value > 18, - ) - ) { - throw new Error( - "Each SSVEP frequency must be between 6 and 18 Hz.", - ); + if (values.some((value) => !Number.isFinite(value) || value < 6 || value > 18)) { + throw new Error("Each SSVEP frequency must be between 6 and 18 Hz."); } - for ( - let index = 0; - index < values.length; - index += 1 - ) { - for ( - let other = - index + 1; - other < values.length; - other += 1 - ) { - if ( - Math.abs( - values[index] - - values[other], - ) < 0.5 - ) { - throw new Error( - "SSVEP frequencies must differ by at least 0.5 Hz.", - ); + for (let index = 0; index < values.length; index += 1) { + for (let other = index + 1; other < values.length; other += 1) { + if (Math.abs(values[index] - values[other]) < 0.5) { + throw new Error("SSVEP frequencies must differ by at least 0.5 Hz."); } } } diff --git a/src/core/ssvep/index.ts b/src/core/ssvep/index.ts index 69f72e4..b493f86 100644 --- a/src/core/ssvep/index.ts +++ b/src/core/ssvep/index.ts @@ -10,13 +10,6 @@ export { validateTaalonFrequencies, } from "./config"; -export { - SsvepClassifierService, - ssvepClassifierService, -} from "./SsvepClassifierService"; +export { SsvepClassifierService, ssvepClassifierService } from "./SsvepClassifierService"; -export type { - SsvepClassification, - SsvepDirection, - SsvepTarget, -} from "./models"; +export type { SsvepClassification, SsvepDirection, SsvepTarget } from "./models"; diff --git a/src/core/ssvep/models.ts b/src/core/ssvep/models.ts index ddebec7..88b830a 100644 --- a/src/core/ssvep/models.ts +++ b/src/core/ssvep/models.ts @@ -1,8 +1,4 @@ -export type SsvepDirection = - | "UP" - | "LEFT" - | "RIGHT" - | "DOWN"; +export type SsvepDirection = "UP" | "LEFT" | "RIGHT" | "DOWN"; export interface SsvepTarget { direction: SsvepDirection; diff --git a/src/features/dashboard/Dashboard.tsx b/src/features/dashboard/Dashboard.tsx index e3be2bd..2441c69 100644 --- a/src/features/dashboard/Dashboard.tsx +++ b/src/features/dashboard/Dashboard.tsx @@ -1,5 +1,7 @@ import { ModuleCard } from "../modules/ModuleCard"; +import { getModuleReadiness } from "../modules/moduleReadiness"; import { useRegisteredModules } from "../modules/useRegisteredModules"; +import { useDevice } from "../device/useDevice"; interface DashboardProps { onOpenModule: (moduleId: string) => Promise; @@ -14,28 +16,39 @@ interface DashboardProps { export function Dashboard({ onOpenModule }: DashboardProps) { const modules = useRegisteredModules(); - return ( -
-
-
-

Applications

+ const { snapshot, streamInfoByDeviceId } = useDevice(); -

Choose a module to start.

-
+ const streamInfo = snapshot.activeDeviceId + ? streamInfoByDeviceId[snapshot.activeDeviceId] + : undefined; + + return ( +
+
+

Modules

+

+ {snapshot.activeDeviceInfo + ? `Connected to ${snapshot.activeDeviceInfo.name}.` + : "Connect a device first, then open a module."} +

{modules.length === 0 ? ( -
-

No modules installed

- -

Installed KNeuron modules will appear here.

+
+ No modules installed +

Register a module in registerBuiltInModules.ts and it will appear here.

) : ( -
+
    {modules.map((module) => ( - + ))} -
+ )}
); diff --git a/src/features/device/DeviceCard.tsx b/src/features/device/DeviceCard.tsx index 6e3aed3..5fb1f4e 100644 --- a/src/features/device/DeviceCard.tsx +++ b/src/features/device/DeviceCard.tsx @@ -1,7 +1,13 @@ -import type { DeviceInfo, DeviceStatus } from "../../core/devices/models/device"; +import type { DeviceInfo, DeviceStatus, DeviceTransport } from "../../core/devices/models/device"; import type { EEGStreamInfo } from "../../core/devices/models/eeg"; +import { deviceRegistry } from "../../core/devices/deviceRegistry"; + +import { isEEGDeviceAdapter } from "../../core/devices/contracts/deviceGuards"; + +import { isBrainMetricsDeviceAdapter } from "../../core/devices/contracts/BrainMetricsDeviceAdapter"; + interface DeviceCardProps { device: Readonly; @@ -18,12 +24,33 @@ interface DeviceCardProps { onDisconnect: () => void; } -function formatDeviceKind(kind: string): string { - return kind.replace("-", " ").toUpperCase(); -} - -function formatTransport(transport: string): string { - return transport.replace("-", " ").toUpperCase(); +const TRANSPORT_LABEL: Record = { + simulation: "Simulated", + bluetooth: "Bluetooth", + usb: "USB", + serial: "Serial", + network: "Network", + lsl: "LSL", + other: "Other", +}; + +function describeSignal( + deviceId: string, + streamInfo: Readonly | undefined, +): string | null { + const adapter = deviceRegistry.get(deviceId); + + if (adapter && isEEGDeviceAdapter(adapter)) { + return streamInfo + ? `Raw EEG, ${streamInfo.channels.length} channels at ${streamInfo.sampleRateHz} Hz` + : "Raw EEG"; + } + + if (adapter && isBrainMetricsDeviceAdapter(adapter)) { + return "Attention and meditation metrics"; + } + + return null; } export function DeviceCard({ @@ -43,85 +70,49 @@ export function DeviceCard({ const buttonDisabled = busy || connecting || disconnecting; - return ( -
-
-
- {formatDeviceKind(device.kind)} - -

{device.name}

-
+ const state = active ? status.state : "disconnected"; - {status.state} -
+ const signal = describeSignal(device.id, streamInfo); -
- {device.manufacturer ?? "Unknown manufacturer"} - - {device.model && ( - <> - · - - {device.model} - - )} -
- -
-
- Type - - {formatDeviceKind(device.kind)} -
- -
- Transport + return ( +
  • + - {formatTransport(device.transport)} -
  • +
    +

    {device.name}

    - {streamInfo && ( - <> -
    - Sampling rate +

    + {[device.manufacturer, device.model, TRANSPORT_LABEL[device.transport]] + .filter(Boolean) + .join(", ")} +

    - {streamInfo.sampleRateHz} Hz -
    + {signal &&

    {signal}

    } -
    - Channels + {active && status.message &&

    {status.message}

    } - {streamInfo.channels.length} -
    - - )} + {active && status.error &&

    {status.error}

    }
    - {active && status.message &&

    {status.message}

    } - - {active && status.error &&

    {status.error}

    } - -
    - {connected ? ( - - ) : ( - - )} -
    -
    + {connected ? ( + + ) : ( + + )} + ); } diff --git a/src/features/device/DevicePage.tsx b/src/features/device/DevicePage.tsx index b193a76..1665424 100644 --- a/src/features/device/DevicePage.tsx +++ b/src/features/device/DevicePage.tsx @@ -12,64 +12,41 @@ export function DevicePage() { const { snapshot, streamInfoByDeviceId, connect, disconnect, getStatusForDevice } = useDevice(); return ( -
    -
    - KNEURON DEVICE LAYER - -

    Device

    - -

    Manage devices available to KNeuron.

    +
    +
    +

    Devices

    +

    One device can be connected at a time. Connecting another one disconnects the first.

    -
    -
    -
    -

    Available devices

    - -

    - {snapshot.devices.length} registered adapter - {snapshot.devices.length === 1 ? "" : "s"} -

    -
    - - {snapshot.activeDeviceInfo && ( - - Active · {snapshot.activeDeviceInfo.name} - - )} + {snapshot.devices.length === 0 ? ( +
    + No devices registered +

    Add an adapter in registerBuiltInDevices.ts and it will appear here.

    - - {snapshot.devices.length === 0 ? ( -
    - No devices available - - Install or register a KNeuron device adapter. -
    - ) : ( -
    - {snapshot.devices.map((device) => { - const active = snapshot.activeDeviceId === device.id; - - return ( - { - void connect(device.id); - }} - onDisconnect={() => { - void disconnect(); - }} - /> - ); - })} -
    - )} -
    + ) : ( +
      + {snapshot.devices.map((device) => { + const active = snapshot.activeDeviceId === device.id; + + return ( + { + void connect(device.id); + }} + onDisconnect={() => { + void disconnect(); + }} + /> + ); + })} +
    + )}
    ); } diff --git a/src/features/modules/ModuleCard.tsx b/src/features/modules/ModuleCard.tsx index 78c9e04..561a879 100644 --- a/src/features/modules/ModuleCard.tsx +++ b/src/features/modules/ModuleCard.tsx @@ -1,19 +1,22 @@ -import { ModuleThumbnail } from "../../components/ModuleThumbnail"; - import type { RegisteredModule } from "../../types/module"; +import type { ModuleReadiness } from "./moduleReadiness"; + interface ModuleCardProps { module: RegisteredModule; + readiness: ModuleReadiness; + onOpen: ((moduleId: string) => void) | ((moduleId: string) => Promise); } /** - * Presentation component for a single KNeuron module. + * Presentation component for a single KNeuron module, rendered as a row + * of the module list. * * ModuleCard contains no lifecycle implementation. */ -export function ModuleCard({ module, onOpen }: ModuleCardProps) { +export function ModuleCard({ module, readiness, onOpen }: ModuleCardProps) { const { manifest, status } = module; const isOpenDisabled = status === "disabled" || status === "starting" || status === "running"; @@ -21,7 +24,7 @@ export function ModuleCard({ module, onOpen }: ModuleCardProps) { let buttonLabel = "Open"; if (status === "starting") { - buttonLabel = "Starting..."; + buttonLabel = "Opening…"; } if (status === "running") { @@ -29,41 +32,42 @@ export function ModuleCard({ module, onOpen }: ModuleCardProps) { } if (status === "error") { - buttonLabel = "Retry"; + buttonLabel = "Open again"; } return ( -
    - - -
    -
    -

    {manifest.name}

    - - v{manifest.version} -
    - -

    {manifest.description}

    - -
    - {manifest.category} - - {manifest.capabilities.eeg?.required && EEG required} -
    - - {status === "error" && module.error &&

    {module.error}

    } - - +
  • +
    +

    {manifest.name}

    + +

    {manifest.description}

    + +

    + + + {readiness.requirement} + + {readiness.missing && {readiness.missing}} +

    + + {status === "error" && module.error &&

    {module.error}

    }
    -
  • + + + ); } diff --git a/src/features/modules/ModuleErrorBoundary.tsx b/src/features/modules/ModuleErrorBoundary.tsx index e4228b1..536c9f9 100644 --- a/src/features/modules/ModuleErrorBoundary.tsx +++ b/src/features/modules/ModuleErrorBoundary.tsx @@ -50,16 +50,14 @@ export class ModuleErrorBoundary extends Component< if (this.state.hasError) { return (
    - MODULE ERROR +

    This module stopped working

    -

    Module failed

    - -

    KNeuron isolated the module failure. The desktop shell is still running normally.

    +

    The rest of KNeuron is unaffected. Go back to the module list and open it again.

    {this.state.message && {this.state.message}}
    ); diff --git a/src/features/modules/ModuleHost.tsx b/src/features/modules/ModuleHost.tsx index e173d76..b59cb92 100644 --- a/src/features/modules/ModuleHost.tsx +++ b/src/features/modules/ModuleHost.tsx @@ -27,13 +27,11 @@ export function ModuleHost({ moduleId, onBack }: ModuleHostProps) {
    - KNEURON MODULE -

    Module unavailable

    The requested module is not registered.

    @@ -53,13 +51,11 @@ export function ModuleHost({ moduleId, onBack }: ModuleHostProps) {
    - KNEURON MODULE -

    {registeredModule.manifest.name}

    {registeredModule.manifest.description}

    @@ -81,13 +77,13 @@ export function ModuleHost({ moduleId, onBack }: ModuleHostProps) {
    {registeredModule.manifest.name} - v{registeredModule.manifest.version} + {registeredModule.manifest.version}
    diff --git a/src/features/modules/registerBuiltInModules.ts b/src/features/modules/registerBuiltInModules.ts index 9036d59..ff26563 100644 --- a/src/features/modules/registerBuiltInModules.ts +++ b/src/features/modules/registerBuiltInModules.ts @@ -1,37 +1,19 @@ -import { - cortexModuleDefinition, -} from "../../modules/cortex/cortexModuleDefinition"; +import { cortexModuleDefinition } from "../../modules/cortex/cortexModuleDefinition"; -import { - minerModuleDefinition, -} from "../../modules/miner/minerModuleDefinition"; +import { minerModuleDefinition } from "../../modules/miner/minerModuleDefinition"; -import { - neuorrunModuleDefinition, -} from "../../modules/neuorrun/neuorrunModuleDefinition"; +import { neuorrunModuleDefinition } from "../../modules/neuorrun/neuorrunModuleDefinition"; -import { - moduleRegistry, -} from "../../lib/moduleRegistry"; +import { moduleRegistry } from "../../lib/moduleRegistry"; -import { - moduleComponentRegistry, -} from "./moduleComponentRegistry"; +import { moduleComponentRegistry } from "./moduleComponentRegistry"; -import { - registerModuleDefinition, -} from "./registerModuleDefinition"; +import { registerModuleDefinition } from "./registerModuleDefinition"; -import type { - KNeuronModuleDefinition, -} from "./moduleDefinition"; +import type { KNeuronModuleDefinition } from "./moduleDefinition"; function createBuiltInModules(): KNeuronModuleDefinition[] { - return [ - cortexModuleDefinition, - minerModuleDefinition, - neuorrunModuleDefinition, - ]; + return [cortexModuleDefinition, minerModuleDefinition, neuorrunModuleDefinition]; } export function registerBuiltInModules(): void { @@ -47,9 +29,7 @@ export function registerBuiltInModules(): void { } if (manifestExists !== componentExists) { - throw new Error( - `Inconsistent module registration state for "${moduleId}".`, - ); + throw new Error(`Inconsistent module registration state for "${moduleId}".`); } registerModuleDefinition(definition); diff --git a/src/features/settings/SettingsPage.tsx b/src/features/settings/SettingsPage.tsx index 748b8cf..338b892 100644 --- a/src/features/settings/SettingsPage.tsx +++ b/src/features/settings/SettingsPage.tsx @@ -29,13 +29,11 @@ export function SettingsPage() { } return ( -
    -
    - KNEURON - +
    +

    Settings

    -

    Configure global KNeuron application behaviour.

    +

    Apply to the whole application. Each module keeps its own options.

    {/* GENERAL */} @@ -43,8 +41,6 @@ export function SettingsPage() {

    General

    - -

    Global behaviour of the KNeuron desktop application.

    @@ -75,7 +71,7 @@ export function SettingsPage() {

    Developer

    -

    Development and diagnostic information.

    +

    Shown only in development builds.

    @@ -105,8 +101,6 @@ export function SettingsPage() {

    About

    - -

    Application and licensing information.

    @@ -114,7 +108,7 @@ export function SettingsPage() {
    {APP_CONFIG.name} - Modular Brain-Computer Interface + Desktop shell version
    v{APP_CONFIG.version} @@ -124,7 +118,7 @@ export function SettingsPage() {
    Environment - Current application runtime environment. + Build this window is running.
    {APP_CONFIG.isDevelopment ? "Development" : "Production"} @@ -134,7 +128,7 @@ export function SettingsPage() {
    License - License governing this KNeuron build. + Distribution terms.
    {APP_CONFIG.license} diff --git a/src/main.tsx b/src/main.tsx index 8993946..8a80a29 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -7,6 +7,11 @@ import { registerBuiltInDevices } from "./core/devices/registerBuiltInDevices"; import { registerBuiltInModules } from "./features/modules/registerBuiltInModules"; +import "@fontsource/barlow/400.css"; +import "@fontsource/barlow/500.css"; +import "@fontsource/barlow/600.css"; +import "@fontsource/barlow-semi-condensed/600.css"; + import "./styles/app.css"; /** @@ -23,4 +28,4 @@ ReactDOM.createRoot(document.getElementById("root")!).render( , -); \ No newline at end of file +); diff --git a/src/modules/cortex/CortexModule.tsx b/src/modules/cortex/CortexModule.tsx index 5c6ab8e..8aaf653 100644 --- a/src/modules/cortex/CortexModule.tsx +++ b/src/modules/cortex/CortexModule.tsx @@ -82,7 +82,7 @@ function loadSavedElectrodes(): CortexElectrode[] { } function formatEEGState(state: string): string { - return state.toUpperCase(); + return state.charAt(0).toUpperCase() + state.slice(1); } function getCalibrationView( @@ -103,7 +103,7 @@ function getCalibrationView( if (eeg.phase === "warmup") { return { visible: true, - title: "FILTER WARM-UP", + title: "Warming up filters", hint: "Keep still; filter output is not entering the baseline yet.", progress: eeg.warmupProgress, elapsedSeconds: eeg.warmupProgress * CORTEX_WARMUP_SECONDS, @@ -114,7 +114,7 @@ function getCalibrationView( if (visualMode === "fast" && !eeg.fastReady) { return { visible: true, - title: "CALIBRATING FAST BASELINE", + title: "Calibrating the fast baseline", hint: "Keep still; the robust baseline freezes when calibration completes.", progress: eeg.fastCalibrationProgress, elapsedSeconds: eeg.fastCalibrationProgress * CORTEX_FAST_BASELINE_SECONDS, @@ -126,7 +126,7 @@ function getCalibrationView( if (eeg.bandWindowProgress < 1 && eeg.bandCalibrationProgress === 0) { return { visible: true, - title: "BUILDING BAND WINDOW", + title: "Collecting the band window", hint: "Collecting the 2 s filtered window required by Welch PSD.", progress: eeg.bandWindowProgress, elapsedSeconds: eeg.bandWindowProgress * CORTEX_BAND_WINDOW_SECONDS, @@ -136,7 +136,7 @@ function getCalibrationView( return { visible: true, - title: "BUILDING BAND BASELINE", + title: "Calibrating the band baseline", hint: "Keep still; the channel × band baseline freezes when complete.", progress: eeg.bandCalibrationProgress, elapsedSeconds: eeg.bandCalibrationProgress * CORTEX_BAND_BASELINE_SECONDS, @@ -159,22 +159,22 @@ function getProcessingStatus( visualMode: CortexVisualMode, ): string { if (!eeg.started) { - return "IDLE"; + return "Idle"; } if (eeg.phase === "warmup") { - return "WARM-UP"; + return "Warming up"; } if (visualMode === "fast" && !eeg.fastReady) { - return "CALIBRATING"; + return "Calibrating"; } if (visualMode === "bands" && !eeg.bandReady) { - return "CALIBRATING"; + return "Calibrating"; } - return "LIVE"; + return "Live"; } export function CortexModule(_props: KNeuronModuleProps) { @@ -186,6 +186,7 @@ export function CortexModule(_props: KNeuronModuleProps) { const [quality, setQuality] = useState("medium"); const [hotspotRadius, setHotspotRadius] = useState(0.38); const [deformation, setDeformation] = useState(0.2); + const [meshLinesVisible, setMeshLinesVisible] = useState(false); const [editMode, setEditMode] = useState(false); const [labelsVisible, setLabelsVisible] = useState(true); const [electrodes, setElectrodes] = useState(loadSavedElectrodes); @@ -285,6 +286,7 @@ export function CortexModule(_props: KNeuronModuleProps) { quality={quality} hotspotRadius={hotspotRadius} deformation={deformation} + meshLinesVisible={meshLinesVisible} editMode={editMode} labelsVisible={labelsVisible} onRendererReady={(renderer) => { @@ -304,17 +306,17 @@ export function CortexModule(_props: KNeuronModuleProps) {
    {formatEEGState(eeg.state)} - - {eeg.sampleRateHz ? `${eeg.sampleRateHz} Hz` : "—"} · {eeg.channelCount} ch - - {eeg.lastSequence !== null && sample {eeg.lastSequence}} + {eeg.sampleRateHz !== null && ( + + {eeg.sampleRateHz} Hz, {eeg.channelCount} channels + + )} {eeg.started && {processingStatus}}
    {calibration.visible && (
    - CORTEX CALIBRATION

    {calibration.title}

    {calibration.hint}

    @@ -337,14 +339,8 @@ export function CortexModule(_props: KNeuronModuleProps) {
    @@ -1215,47 +632,28 @@ export function MinerModule( {overlayPhase ? ( ) : null} - {trialPhase === - "error" ? ( + {trialPhase === "error" ? (
    - - SSVEP TRIAL ERROR - + Trial failed -

    - {trialError} -

    +

    {trialError}

    ) : null} @@ -1263,34 +661,16 @@ export function MinerModule( {game.completed ? (
    - - EXPEDITION COMPLETE - - -

    - All diamonds collected -

    +

    All diamonds collected

    - { - MINER_DIAMONDS.length - }{" "} - /{" "} - { - MINER_DIAMONDS.length - }{" "} - diamonds · {game.moves} moves + {MINER_DIAMONDS.length} / {MINER_DIAMONDS.length} diamonds in {game.moves} moves

    -
    diff --git a/src/modules/miner/components/DiamondIcon.tsx b/src/modules/miner/components/DiamondIcon.tsx index 424410f..ef39da9 100644 --- a/src/modules/miner/components/DiamondIcon.tsx +++ b/src/modules/miner/components/DiamondIcon.tsx @@ -1,22 +1,9 @@ export function DiamondIcon() { return ( -
    - BCI MOVE -

    - Focus on one direction -

    - - {countdownRemaining} - -

    - Four targets will start - simultaneously. -

    -
    ); } - if ( - phase === - "classifying" - ) { + if (phase === "classifying") { return (
    - FBCCA -

    - Classifying EEG -

    +

    Classifying EEG

    -

    - 5 subbands · 5 harmonics · - CCA -

    +

    Filter-bank CCA over 5 subbands and 5 harmonics.

    ); } - const up = - targetByDirection.get( - "UP", - ); + const up = targetByDirection.get("UP"); - const left = - targetByDirection.get( - "LEFT", - ); + const left = targetByDirection.get("LEFT"); - const right = - targetByDirection.get( - "RIGHT", - ); + const right = targetByDirection.get("RIGHT"); - const down = - targetByDirection.get( - "DOWN", - ); + const down = targetByDirection.get("DOWN"); - const ordered = [ - up, - left, - right, - down, - ].filter( - ( - target, - ): target is SsvepTarget => - Boolean(target), + const ordered = [up, left, right, down].filter((target): target is SsvepTarget => + Boolean(target), ); - const progress = - Math.min( - 1, - elapsedSeconds / - trialSeconds, - ); + const progress = Math.min(1, elapsedSeconds / trialSeconds); return (
    -
    - FOCUS ON ONE TARGET -
    +
    Look at one target
    - {ordered.map( - ( - target, - index, - ) => ( + {ordered.map((target, index) => ( +
    -
    { - targetRefs.current[ - index - ] = node; - }} - /> - - - { - target.direction - } - - - - {target.frequencyHz.toFixed( - 2, - )}{" "} - Hz - -
    - ), - )} + className="miner-ssvep-target__square" + ref={(node) => { + targetRefs.current[index] = node; + }} + /> + + {target.direction} + + {target.frequencyHz.toFixed(2)} Hz +
    + ))}
    - {Math.min( - elapsedSeconds, - trialSeconds, - ).toFixed(1)} + {Math.min(elapsedSeconds, trialSeconds).toFixed(1)} {" / "} - {trialSeconds.toFixed( - 1, - )}{" "} - s + {trialSeconds.toFixed(1)} s - {phase === - "waiting-samples" ? ( - - Waiting for remaining EEG - samples… - - ) : null} - -
    diff --git a/src/modules/miner/eeg/TrialSampleCollector.ts b/src/modules/miner/eeg/TrialSampleCollector.ts index 2d8c7b4..c8fe48c 100644 --- a/src/modules/miner/eeg/TrialSampleCollector.ts +++ b/src/modules/miner/eeg/TrialSampleCollector.ts @@ -7,20 +7,15 @@ export class TrialSampleCollector { private active = false; - private values: - number[][] = []; - - begin( - channelCount: number, - ): void { - this.values = - Array.from( - { - length: - channelCount, - }, - () => [], - ); + private values: number[][] = []; + + begin(channelCount: number): void { + this.values = Array.from( + { + length: channelCount, + }, + () => [], + ); this.active = true; } @@ -29,73 +24,37 @@ export class TrialSampleCollector { this.active = false; } - push( - batchValues: - readonly ( - readonly number[] - )[], - ): void { + push(batchValues: readonly (readonly number[])[]): void { if (!this.active) { return; } - if ( - batchValues.length !== - this.values.length - ) { + if (batchValues.length !== this.values.length) { throw new Error( `SSVEP trial expected ${this.values.length} channels but received ${batchValues.length}.`, ); } - for ( - let channel = 0; - channel < - batchValues.length; - channel += 1 - ) { - this.values[ - channel - ].push( - ...batchValues[ - channel - ], - ); + for (let channel = 0; channel < batchValues.length; channel += 1) { + this.values[channel].push(...batchValues[channel]); } } sampleCount(): number { - return ( - this.values[0] - ?.length ?? 0 - ); + return this.values[0]?.length ?? 0; } - latest( - sampleCount: number, - ): number[][] { - if ( - sampleCount <= 0 - ) { - throw new Error( - "sampleCount must be positive.", - ); + latest(sampleCount: number): number[][] { + if (sampleCount <= 0) { + throw new Error("sampleCount must be positive."); } - if ( - this.sampleCount() < - sampleCount - ) { + if (this.sampleCount() < sampleCount) { throw new Error( `Only ${this.sampleCount()} SSVEP samples are available; ${sampleCount} required.`, ); } - return this.values.map( - (channel) => - channel.slice( - -sampleCount, - ), - ); + return this.values.map((channel) => channel.slice(-sampleCount)); } } diff --git a/src/modules/miner/game/minerGame.ts b/src/modules/miner/game/minerGame.ts index 3a473a5..f71651e 100644 --- a/src/modules/miner/game/minerGame.ts +++ b/src/modules/miner/game/minerGame.ts @@ -1,27 +1,14 @@ -import type { - MinerGameState, - MinerMoveResult, - GridPosition, -} from "../models/miner"; - -import type { - SsvepDirection, -} from "../../../core/ssvep"; - -export const MINER_BOARD_WIDTH = - 7; - -export const MINER_BOARD_HEIGHT = - 5; - -export const MINER_START: - GridPosition = [ - 3, - 4, -]; - -export const MINER_WALLS: - readonly GridPosition[] = [ +import type { MinerGameState, MinerMoveResult, GridPosition } from "../models/miner"; + +import type { SsvepDirection } from "../../../core/ssvep"; + +export const MINER_BOARD_WIDTH = 7; + +export const MINER_BOARD_HEIGHT = 5; + +export const MINER_START: GridPosition = [3, 4]; + +export const MINER_WALLS: readonly GridPosition[] = [ [1, 1], [2, 1], [4, 1], @@ -37,178 +24,110 @@ export const MINER_WALLS: * completes only after every diamond has been collected. The old gold cell * (6, 0) remains one of the diamond locations. */ -export const MINER_DIAMONDS = - [ - { - id: "diamond-a", - position: [0, 0], - }, - { - id: "diamond-b", - position: [3, 0], - }, - { - id: "diamond-c", - position: [6, 0], - }, - { - id: "diamond-d", - position: [0, 2], - }, - { - id: "diamond-e", - position: [6, 2], - }, - { - id: "diamond-f", - position: [3, 3], - }, - ] as const; - -const DIRECTION_VECTOR: - Readonly< - Record< - SsvepDirection, - GridPosition - > - > = { +export const MINER_DIAMONDS = [ + { + id: "diamond-a", + position: [0, 0], + }, + { + id: "diamond-b", + position: [3, 0], + }, + { + id: "diamond-c", + position: [6, 0], + }, + { + id: "diamond-d", + position: [0, 2], + }, + { + id: "diamond-e", + position: [6, 2], + }, + { + id: "diamond-f", + position: [3, 3], + }, +] as const; + +const DIRECTION_VECTOR: Readonly> = { UP: [0, -1], LEFT: [-1, 0], RIGHT: [1, 0], DOWN: [0, 1], }; -function key( - position: - GridPosition, -): string { +function key(position: GridPosition): string { return `${position[0]}:${position[1]}`; } -const WALL_KEYS = - new Set( - MINER_WALLS.map( - key, - ), - ); +const WALL_KEYS = new Set(MINER_WALLS.map(key)); -export function createInitialMinerGame(): - MinerGameState { +export function createInitialMinerGame(): MinerGameState { return { - miner: - MINER_START, + miner: MINER_START, collected: [], moves: 0, completed: false, }; } -export function moveMiner( - state: - MinerGameState, - direction: - SsvepDirection, -): MinerMoveResult { +export function moveMiner(state: MinerGameState, direction: SsvepDirection): MinerMoveResult { if (state.completed) { return { state, moved: false, - collectedDiamond: - null, - message: - "All diamonds are already collected.", + collectedDiamond: null, + message: "All diamonds are already collected.", }; } - const vector = - DIRECTION_VECTOR[ - direction - ]; - - const next: - GridPosition = [ - state.miner[0] + - vector[0], - state.miner[1] + - vector[1], - ]; - - if ( - next[0] < 0 || - next[0] >= - MINER_BOARD_WIDTH || - next[1] < 0 || - next[1] >= - MINER_BOARD_HEIGHT - ) { + const vector = DIRECTION_VECTOR[direction]; + + const next: GridPosition = [state.miner[0] + vector[0], state.miner[1] + vector[1]]; + + if (next[0] < 0 || next[0] >= MINER_BOARD_WIDTH || next[1] < 0 || next[1] >= MINER_BOARD_HEIGHT) { return { state, moved: false, - collectedDiamond: - null, - message: - "Boundary blocked the move.", + collectedDiamond: null, + message: "Boundary blocked the move.", }; } - if ( - WALL_KEYS.has( - key(next), - ) - ) { + if (WALL_KEYS.has(key(next))) { return { state, moved: false, - collectedDiamond: - null, - message: - "Rock blocked the move.", + collectedDiamond: null, + message: "Rock blocked the move.", }; } - const diamond = - MINER_DIAMONDS.find( - (candidate) => - key( - candidate.position, - ) === key(next) && - !state.collected.includes( - candidate.id, - ), - ); - - const collected = - diamond - ? [ - ...state.collected, - diamond.id, - ] - : state.collected; - - const completed = - collected.length === - MINER_DIAMONDS.length; - - const nextState: - MinerGameState = { + const diamond = MINER_DIAMONDS.find( + (candidate) => key(candidate.position) === key(next) && !state.collected.includes(candidate.id), + ); + + const collected = diamond ? [...state.collected, diamond.id] : state.collected; + + const completed = collected.length === MINER_DIAMONDS.length; + + const nextState: MinerGameState = { miner: next, collected, - moves: - state.moves + 1, + moves: state.moves + 1, completed, }; return { - state: - nextState, + state: nextState, moved: true, - collectedDiamond: - diamond?.id ?? null, - message: - completed - ? "All diamonds collected." - : diamond - ? "Diamond collected." - : "Miner moved one cell.", + collectedDiamond: diamond?.id ?? null, + message: completed + ? "All diamonds collected." + : diamond + ? "Diamond collected." + : "Miner moved one cell.", }; } diff --git a/src/modules/miner/hooks/useMinerEEG.ts b/src/modules/miner/hooks/useMinerEEG.ts index 56cc23c..4a6eaa6 100644 --- a/src/modules/miner/hooks/useMinerEEG.ts +++ b/src/modules/miner/hooks/useMinerEEG.ts @@ -1,195 +1,87 @@ -import { - useCallback, - useEffect, - useRef, - useState, -} from "react"; - -import { - eegStreamService, - type EEGStreamHandle, -} from "../../../core/eeg"; - -import { - TAALON_SSVEP_CHANNELS, -} from "../../../core/ssvep"; - -import { - TrialSampleCollector, -} from "../eeg/TrialSampleCollector"; - -export type MinerEEGState = - | "starting" - | "streaming" - | "error"; +import { useCallback, useEffect, useRef, useState } from "react"; + +import { eegStreamService, type EEGStreamHandle } from "../../../core/eeg"; + +import { TAALON_SSVEP_CHANNELS } from "../../../core/ssvep"; + +import { TrialSampleCollector } from "../eeg/TrialSampleCollector"; + +export type MinerEEGState = "starting" | "streaming" | "error"; export interface MinerEEGSnapshot { - state: - MinerEEGState; - error: - string | null; - sampleRateHz: - number | null; - channelCount: - number; + state: MinerEEGState; + error: string | null; + sampleRateHz: number | null; + channelCount: number; retry: () => void; - beginCapture: - () => void; - stopCapture: - () => void; - availableCaptureSamples: - () => number; - takeLatestCapture: - ( - sampleCount: - number, - ) => number[][]; + beginCapture: () => void; + stopCapture: () => void; + availableCaptureSamples: () => number; + takeLatestCapture: (sampleCount: number) => number[][]; } -function errorMessage( - error: unknown, -): string { - return error instanceof Error - ? error.message - : String(error); +function errorMessage(error: unknown): string { + return error instanceof Error ? error.message : String(error); } -export function useMinerEEG(): - MinerEEGSnapshot { - const [ - retryGeneration, - setRetryGeneration, - ] = useState(0); +export function useMinerEEG(): MinerEEGSnapshot { + const [retryGeneration, setRetryGeneration] = useState(0); - const [ - state, - setState, - ] = - useState( - "starting", - ); + const [state, setState] = useState("starting"); - const [ - error, - setError, - ] = - useState< - string | null - >(null); + const [error, setError] = useState(null); - const [ - sampleRateHz, - setSampleRateHz, - ] = - useState< - number | null - >(null); + const [sampleRateHz, setSampleRateHz] = useState(null); - const [ - channelCount, - setChannelCount, - ] = useState(0); - - const collectorRef = - useRef( - new TrialSampleCollector(), - ); - - const retry = - useCallback(() => { - setState( - "starting", - ); - setError(null); - setSampleRateHz( - null, - ); - setChannelCount(0); - - setRetryGeneration( - (value) => - value + 1, - ); - }, []); - - const beginCapture = - useCallback(() => { - collectorRef.current.begin( - channelCount, - ); - }, [channelCount]); - - const stopCapture = - useCallback(() => { - collectorRef.current.stop(); - }, []); - - const availableCaptureSamples = - useCallback( - () => - collectorRef.current.sampleCount(), - [], - ); - - const takeLatestCapture = - useCallback( - ( - count: - number, - ) => - collectorRef.current.latest( - count, - ), - [], - ); + const [channelCount, setChannelCount] = useState(0); + + const collectorRef = useRef(new TrialSampleCollector()); + + const retry = useCallback(() => { + setState("starting"); + setError(null); + setSampleRateHz(null); + setChannelCount(0); + + setRetryGeneration((value) => value + 1); + }, []); + + const beginCapture = useCallback(() => { + collectorRef.current.begin(channelCount); + }, [channelCount]); + + const stopCapture = useCallback(() => { + collectorRef.current.stop(); + }, []); + + const availableCaptureSamples = useCallback(() => collectorRef.current.sampleCount(), []); + + const takeLatestCapture = useCallback((count: number) => collectorRef.current.latest(count), []); useEffect(() => { let cancelled = false; - let handle: - EEGStreamHandle | null = - null; + let handle: EEGStreamHandle | null = null; - async function start(): - Promise { + async function start(): Promise { try { - const acquired = - await eegStreamService.acquire( - { - channels: - [ - ...TAALON_SSVEP_CHANNELS, - ], - - onBatch: - (batch) => { - if ( - cancelled - ) { - return; - } - - try { - collectorRef.current.push( - batch.values, - ); - - } catch ( - batchError - ) { - setState( - "error", - ); - - setError( - errorMessage( - batchError, - ), - ); - } - }, - }, - ); + const acquired = await eegStreamService.acquire({ + channels: [...TAALON_SSVEP_CHANNELS], + + onBatch: (batch) => { + if (cancelled) { + return; + } + + try { + collectorRef.current.push(batch.values); + } catch (batchError) { + setState("error"); + + setError(errorMessage(batchError)); + } + }, + }); if (cancelled) { await acquired.release(); @@ -198,45 +90,32 @@ export function useMinerEEG(): handle = acquired; - setSampleRateHz( - acquired.streamInfo.sampleRateHz, - ); + setSampleRateHz(acquired.streamInfo.sampleRateHz); - setChannelCount( - acquired.channels.length, - ); + setChannelCount(acquired.channels.length); - setState( - "streaming", - ); - } catch ( - startError - ) { + setState("streaming"); + } catch (startError) { if (cancelled) { return; } - setState( - "error", - ); + setState("error"); - setError( - errorMessage( - startError, - ), - ); + setError(errorMessage(startError)); } } void start(); + const collector = collectorRef.current; + return () => { cancelled = true; - collectorRef.current.stop(); + collector.stop(); - const currentHandle = - handle; + const currentHandle = handle; handle = null; diff --git a/src/modules/miner/minerManifest.ts b/src/modules/miner/minerManifest.ts index c7d99c4..d3ddf23 100644 --- a/src/modules/miner/minerManifest.ts +++ b/src/modules/miner/minerManifest.ts @@ -1,30 +1,18 @@ -import type { - KNeuronModuleManifest, -} from "../../types/module"; +import type { KNeuronModuleManifest } from "../../types/module"; -export const minerManifest: - KNeuronModuleManifest = { +export const minerManifest: KNeuronModuleManifest = { schemaVersion: 1, id: "taalon-miner", name: "TaaLON Miner", version: "1.0.0", - description: - "SSVEP-controlled mining game using the TaaLON FBCCA pipeline.", + description: "SSVEP-controlled mining game using the TaaLON FBCCA pipeline.", category: "GAME", appearance: {}, - entryPoint: - "/modules/taalon-miner", + entryPoint: "/modules/taalon-miner", capabilities: { eeg: { required: true, - requiredChannels: [ - "POz", - "PO3", - "PO4", - "Oz", - "O1", - "O2", - ], + requiredChannels: ["POz", "PO3", "PO4", "Oz", "O1", "O2"], }, }, }; diff --git a/src/modules/miner/minerModuleDefinition.ts b/src/modules/miner/minerModuleDefinition.ts index 5b13172..ca3acfb 100644 --- a/src/modules/miner/minerModuleDefinition.ts +++ b/src/modules/miner/minerModuleDefinition.ts @@ -1,19 +1,31 @@ +import { createElement, lazy, Suspense } from "react"; + import type { KNeuronModuleDefinition, + KNeuronModuleProps, } from "../../features/modules/moduleDefinition"; -import { - MinerModule, -} from "./MinerModule"; +import { minerManifest } from "./minerManifest"; + +const LazyMinerModule = lazy(async () => { + const module = await import("./MinerModule"); + + return { + default: module.MinerModule, + }; +}); -import { - minerManifest, -} from "./minerManifest"; +function MinerModuleLoader(props: KNeuronModuleProps) { + return createElement( + Suspense, + { + fallback: createElement("div", null, "Loading TaaLON Miner..."), + }, + createElement(LazyMinerModule, props), + ); +} -export const minerModuleDefinition: - KNeuronModuleDefinition = { - manifest: - minerManifest, - component: - MinerModule, +export const minerModuleDefinition: KNeuronModuleDefinition = { + manifest: minerManifest, + component: MinerModuleLoader, }; diff --git a/src/modules/miner/models/miner.ts b/src/modules/miner/models/miner.ts index a72b2cc..7ea65b0 100644 --- a/src/modules/miner/models/miner.ts +++ b/src/modules/miner/models/miner.ts @@ -1,43 +1,25 @@ -import type { - SsvepClassification, - SsvepDirection, -} from "../../../core/ssvep"; +import type { SsvepClassification, SsvepDirection } from "../../../core/ssvep"; -export type GridPosition = - readonly [ - number, - number, - ]; +export type GridPosition = readonly [number, number]; export interface MinerGameState { - miner: - GridPosition; - collected: - readonly string[]; + miner: GridPosition; + collected: readonly string[]; moves: number; completed: boolean; } export interface MinerMoveResult { - state: - MinerGameState; + state: MinerGameState; message: string; moved: boolean; - collectedDiamond: - string | null; + collectedDiamond: string | null; } export type MinerTrialPhase = - | "idle" - | "countdown" - | "stimulating" - | "waiting-samples" - | "classifying" - | "error"; + "idle" | "countdown" | "stimulating" | "waiting-samples" | "classifying" | "error"; export interface MinerLastDecision { - direction: - SsvepDirection; - classification: - SsvepClassification; + direction: SsvepDirection; + classification: SsvepClassification; } diff --git a/src/modules/miner/styles/miner.css b/src/modules/miner/styles/miner.css index e45659a..0b69990 100644 --- a/src/modules/miner/styles/miner.css +++ b/src/modules/miner/styles/miner.css @@ -1,3 +1,10 @@ +/* TaaLON Miner: game board on a dark screen, BCI controls on the light + instrument face. Uses the shell tokens from src/styles/app.css. + + The SSVEP stimulus section at the end is deliberately plain: target + colour, contrast and size affect classification, so change it only + together with the FBCCA setup. */ + .miner-game { position: relative; @@ -8,2064 +15,747 @@ height: 100%; min-height: 0; - padding: 18px 20px; - - color: #edf5ff; - - background: - radial-gradient( - circle at 30% 15%, - rgba(15, 79, 135, 0.18), - transparent 30% - ), - linear-gradient( - 180deg, - #030a12 0%, - #02070c 100% - ); + color: var(--ink); overflow: hidden; } - - .miner-game button, - .miner-game input { - font: inherit; - } +.miner-game h2 { + margin: 0 0 6px; + font-family: var(--font-display); + font-size: 18px; + font-weight: 600; +} -.miner-game__header { - display: flex; - flex: 0 0 auto; +.miner-game__layout { + display: grid; + grid-template-columns: minmax(0, 1fr) 320px; - align-items: flex-start; - justify-content: space-between; + flex: 1; + min-height: 0; - gap: 24px; + gap: 12px; - margin-bottom: 14px; + overflow: hidden; } +/* --------------------------------------------------------- + Board screen + --------------------------------------------------------- */ +.miner-game__board-panel { + display: grid; + grid-template-rows: auto minmax(0, 1fr) auto; -.miner-game__eyebrow, + min-width: 0; + min-height: 0; -.miner-game__section-label { + color: #e3ece8; + background: var(--screen); + border: 1px solid var(--screen-edge); + border-radius: 4px; + box-shadow: inset 0 2px 6px rgb(0 0 0 / 55%); - color: #168cff; + overflow: hidden; +} +.miner-game__mission-bar { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px 28px; + padding: 12px 18px; - font-size: 10px; + border-bottom: 1px solid var(--screen-grid); +} - font-weight: 700; +.miner-game__mission-bar > div { + display: flex; + flex-direction: column; +} +.miner-game__mission-bar small { + color: var(--screen-label); + font-size: 13px; +} +.miner-game__mission-bar strong { + color: var(--trace); + font-family: var(--font-display); + font-size: 24px; + font-weight: 600; + line-height: 1.1; +} - letter-spacing: 0.16em; +.miner-game__mission-message { + flex: 1 1 240px; + color: #c9d6d2; + font-size: 14px; } +.miner-game__legend { + display: flex; + justify-content: center; + gap: 22px; + padding: 10px 18px 12px; -.miner-game__header h1 { - margin: 6px 0 5px; + color: var(--screen-label); + font-size: 13px; +} - font-size: 26px; - font-weight: 650; +.miner-game__legend span { + display: flex; + align-items: center; - letter-spacing: -0.02em; + gap: 7px; } +.miner-game__legend i { + display: inline-block; + width: 10px; + height: 10px; -.miner-game__header p { + border-radius: 2px; +} - margin: 0; +.miner-game__legend-miner { + background: #f76707; +} +.miner-game__legend-diamond { + background: #22bfff; +} +.miner-game__legend-rock { + background: #3a4a52; +} - color: #91a7bb; +/* --------------------------------------------------------- + Control face + --------------------------------------------------------- */ +.miner-game__control-panel { + display: flex; + flex-direction: column; + min-height: 0; - font-size: 13px; + padding: 4px 18px 18px; - line-height: 1.5; + background: var(--face); + border: 1px solid var(--groove); + border-radius: var(--radius-panel); + overflow-y: auto; } +.miner-game__section { + padding: 16px 0; + border-bottom: 1px solid var(--groove); +} -.miner-game__stream { - - display: grid; - - - - grid-template-columns: - - 12px +.miner-game__section-heading { + display: flex; + align-items: baseline; + justify-content: space-between; +} - auto +.miner-game__section-heading button, +.miner-game__error button, +.miner-game__secondary, +.miner-game__trial-error-banner button { + height: 32px; - auto + padding: 0 12px; - auto; + color: var(--ink); + background: var(--face-raised); + border: 1px solid var(--groove); + border-radius: var(--radius-control); + box-shadow: 0 1px 0 rgb(255 255 255 / 80%) inset; + font-weight: 600; + cursor: pointer; +} +.miner-game__stream { + display: flex; align-items: center; + gap: 9px; + margin-bottom: 14px; + color: var(--ink-2); + font-size: 14px; +} - gap: 12px 22px; - - +.miner-game__status-dot { + flex: none; - min-width: 390px; + width: 9px; + height: 9px; + background: var(--led-busy); + border-radius: 50%; +} +.miner-game__status-dot--streaming { + background: var(--led-on); + box-shadow: 0 0 0 3px rgb(47 158 68 / 18%); +} - padding: 13px 16px; +.miner-game__status-dot--error { + background: var(--led-error); +} +.miner-game__primary { + width: 100%; + height: 42px; + margin-top: 12px; - border: 1px solid #18334c; + color: var(--face-raised); + background: var(--ink); + border: 1px solid var(--ink); + border-radius: var(--radius-control); + font-size: 16px; + font-weight: 600; + cursor: pointer; +} - background: #06111d; +.miner-game__primary:hover:not(:disabled) { + background: #2c353a; +} +.miner-game__primary:disabled, +.miner-game__duration button:disabled { + opacity: 0.45; + cursor: default; } +.miner-game__hint, +.miner-game__dev-note { + margin-top: 6px; + color: var(--ink-3); + font-size: 13px; +} -.miner-game__stream > div { +.miner-game__dev-note { + margin-top: 12px; +} - display: flex; +.miner-game__frequency-list { + display: grid; - flex-direction: column; + gap: 6px; + margin-top: 4px; +} +.miner-game__frequency-list label { + display: grid; + grid-template-columns: 64px minmax(0, 1fr) auto; + align-items: center; + gap: 8px; - gap: 3px; + font-size: 14px; +} +.miner-game__frequency-list label > span { + color: var(--ink-2); + text-transform: capitalize; } +.miner-game__frequency-list input { + height: 32px; + padding: 0 9px; -.miner-game__stream small { + color: var(--ink); + background: var(--face-raised); + border: 1px solid var(--groove); + border-radius: var(--radius-control); - color: #6f879d; + font-weight: 600; + text-align: right; +} +.miner-game__frequency-list small { + color: var(--ink-3); +} +.miner-game__duration { + display: grid; + grid-template-columns: 36px minmax(0, 1fr) 36px; + align-items: center; - font-size: 9px; + margin-top: 4px; +} +.miner-game__duration button { + height: 34px; + color: var(--ink); + background: var(--face-raised); + border: 1px solid var(--groove); + border-radius: var(--radius-control); - letter-spacing: 0.14em; + font-size: 18px; + cursor: pointer; } +.miner-game__duration strong { + font-family: var(--font-display); + font-size: 24px; + font-weight: 600; + text-align: center; +} +.miner-game__winner { + display: flex; + align-items: baseline; + justify-content: space-between; -.miner-game__stream strong { - - font-size: 12px; + margin-top: 2px; +} +.miner-game__winner span { + font-family: var(--font-display); + font-size: 24px; font-weight: 600; + text-transform: capitalize; +} +.miner-game__winner strong { + color: var(--ink-2); + font-weight: 600; } +.miner-game__scores { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 4px 16px; + margin-top: 10px; +} -.miner-game__status-dot { +.miner-game__scores > div { + display: flex; + justify-content: space-between; - width: 8px; + font-size: 13px; +} - height: 8px; +.miner-game__scores span { + color: var(--ink-2); + text-transform: capitalize; +} +.miner-game__secondary { + align-self: flex-start; + margin-top: 16px; +} - border-radius: 50%; +.miner-game__error { + margin-top: 12px; + padding: 10px 12px; + background: var(--face-raised); + border: 1px solid var(--groove); + border-left: 3px solid var(--led-error); + border-radius: var(--radius-control); +} +.miner-game__error span { + font-weight: 600; +} - background: #65788a; +.miner-game__error p { + margin: 2px 0 8px; + color: var(--ink-2); + font-size: 14px; } +/* --------------------------------------------------------- + Messages over the game + --------------------------------------------------------- */ +.miner-game__trial-error-banner { + position: fixed; + z-index: 120; + right: 24px; + bottom: 24px; -.miner-game__status-dot--streaming { + display: grid; + grid-template-columns: minmax(200px, 360px) auto; + align-items: center; - background: #28d79b; + gap: 2px 16px; + padding: 12px 12px 12px 16px; + color: var(--ink); + background: var(--face-raised); + border: 1px solid var(--groove); + border-left: 3px solid var(--led-error); + border-radius: var(--radius-control); + box-shadow: 0 8px 24px rgb(28 34 38 / 25%); +} +.miner-game__trial-error-banner p { + grid-column: 1; + margin: 0; - box-shadow: + color: var(--ink-2); + font-size: 14px; +} - 0 0 10px +.miner-game__trial-error-banner button { + grid-column: 2; + grid-row: 1 / span 2; +} - rgba(40, 215, 155, 0.6); +.miner-ssvep-overlay, +.miner-game__complete { + position: absolute; + z-index: 100; + inset: 0; + background: var(--screen); } +.miner-ssvep-overlay__countdown, +.miner-ssvep-overlay__analyzing, +.miner-game__complete-card { + position: absolute; + top: 50%; + left: 50%; + width: min(460px, calc(100% - 48px)); -.miner-game__status-dot--error { + padding: 32px; - background: #ff5264; + color: #e3ece8; + text-align: center; + transform: translate(-50%, -50%); +} +.miner-ssvep-overlay__countdown h2, +.miner-ssvep-overlay__analyzing h2, +.miner-game__complete-card h2 { + margin: 8px 0; - box-shadow: + font-size: 28px; +} - 0 0 10px +.miner-ssvep-overlay__countdown strong { + display: block; - rgba(255, 82, 100, 0.55); + margin: 18px 0; + color: var(--trace); + font-family: var(--font-display); + font-size: 96px; + font-weight: 600; + line-height: 1; } +.miner-ssvep-overlay__countdown p, +.miner-ssvep-overlay__analyzing p, +.miner-game__complete-card p { + color: var(--screen-label); + font-size: 14px; +} +.miner-ssvep-overlay__countdown button, +.miner-ssvep-overlay__footer button { + margin-top: 18px; + padding: 6px 14px; -.miner-game__layout { - display: grid; - - grid-template-columns: - minmax(0, 1fr) - 330px; - - gap: 18px; + color: #c9d6d2; + background: none; + border: 1px solid var(--screen-grid); + border-radius: var(--radius-control); - flex: 1; + font-size: 14px; - min-height: 0; - overflow: hidden; + cursor: pointer; } +.miner-game__complete-card .miner-game__diamond-svg { + width: 84px; + height: 84px; +} +.miner-game__complete-card button { + height: 40px; -.miner-game__board-panel { - display: grid; - - grid-template-rows: - auto - minmax(0, 1fr) - auto; - - min-width: 0; - min-height: 0; + margin-top: 20px; + padding: 0 20px; - border: 1px solid #17324a; + color: var(--ink); + background: var(--face-raised); + border: 0; + border-radius: var(--radius-control); - background: #06111c; + font-weight: 600; - overflow: hidden; + cursor: pointer; } -.miner-game__control-panel { - display: flex; - flex-direction: column; - - min-height: 0; - - border: 1px solid #17324a; - - background: #06111c; - - overflow-y: auto; -} - - - -.miner-game__mission-bar { - display: grid; - - grid-template-columns: - 90px - 90px - 90px - minmax(0, 1fr); - - min-height: 58px; - - border-bottom: 1px solid #17324a; -} - - - -.miner-game__mission-bar > div { - - display: flex; - - flex-direction: column; - - justify-content: center; - - - - gap: 4px; - - - - padding: 0 17px; - - - - border-right: 1px solid #112b41; - -} - - - -.miner-game__mission-bar small { - - color: #687f94; - - - - font-size: 9px; - - - - letter-spacing: 0.14em; - -} - - - -.miner-game__mission-bar strong { - - color: #ddecfa; - - - - font-size: 17px; - -} - - - -.miner-game__mission-message { - - border-right: 0 !important; - - - - color: #9bb0c3; - - - - font-size: 12px; - - line-height: 1.45; - -} - - +/* --------------------------------------------------------- + Board (game art) + --------------------------------------------------------- */ .miner-game__board { display: grid; - - grid-template-rows: - repeat(5, minmax(0, 1fr)); - + grid-template-rows: repeat(5, minmax(0, 1fr)); gap: 6px; - height: 100%; width: auto; - max-width: 100%; - aspect-ratio: 7 / 5; - justify-self: center; align-self: center; - padding: 14px; - - background: - linear-gradient( - rgba(4, 13, 22, 0.36), - rgba(4, 13, 22, 0.36) - ), - repeating-linear-gradient( - 135deg, - rgba(22, 140, 255, 0.025) 0, - rgba(22, 140, 255, 0.025) 1px, - transparent 1px, - transparent 11px - ); - + background: var(--screen); overflow: hidden; } - - .miner-game__cell { position: relative; - min-width: 0; min-height: 0; - aspect-ratio: 1; - - border: 1px solid #142f45; - + border: 1px solid var(--screen-grid); overflow: hidden; } - - .miner-game__cell--open { - background: - - radial-gradient( - - circle at 40% 35%, - - rgba(27, 87, 111, 0.17), - - transparent 50% - - ), - - #081723; - + radial-gradient(circle at 40% 35%, rgba(142, 217, 192, 0.07), transparent 50%), #141c1d; } - - .miner-game__cell--open::after { - position: absolute; - - - right: 8px; - bottom: 7px; - left: 8px; - - - height: 1px; - - - content: ""; - - - - background: #12344b; - + background: var(--screen-grid); } - - .miner-game__cell--wall { - border-color: #25394a; - - - - background: - - linear-gradient( - - 145deg, - - #192733, - - #0d1720 - - ); - + background: linear-gradient(145deg, #192733, #0d1720); } - - .miner-game__rock { - position: absolute; - - - inset: 12%; - - - transform: rotate(-4deg); - } - - .miner-game__rock span { - position: absolute; - - - border: 1px solid #476070; - - - background: #283b48; - } - - .miner-game__rock span:nth-child(1) { - inset: 18% 8% 8% 18%; - - - - clip-path: - - polygon( - - 15% 0, - - 82% 7%, - - 100% 49%, - - 74% 100%, - - 16% 84%, - - 0 37% - - ); - + clip-path: polygon(15% 0, 82% 7%, 100% 49%, 74% 100%, 16% 84%, 0 37%); } - - .miner-game__rock span:nth-child(2) { - top: 12%; - left: 7%; - - - width: 42%; - height: 35%; - - - - clip-path: - - polygon( - - 20% 0, - - 100% 24%, - - 75% 100%, - - 0 80% - - ); - - - + clip-path: polygon(20% 0, 100% 24%, 75% 100%, 0 80%); background: #354c5c; - } - - .miner-game__rock span:nth-child(3) { - right: 5%; - bottom: 4%; - - - width: 35%; - height: 34%; - - - - clip-path: - - polygon( - - 30% 0, - - 100% 20%, - - 84% 100%, - - 0 73% - - ); - - - + clip-path: polygon(30% 0, 100% 20%, 84% 100%, 0 73%); background: #1d303d; - } - - .miner-game__miner, - .miner-game__diamond { - position: absolute; - - - z-index: 3; - - - inset: 8%; - - - display: grid; - - - place-items: center; - } - - .miner-game__miner-svg { - width: 82%; - height: 82%; - - - - color: #168cff; - - - - filter: - - drop-shadow( - - 0 4px 13px - - rgba(0, 0, 0, 0.65) - - ); - + color: #f76707; + filter: drop-shadow(0 4px 13px rgba(0, 0, 0, 0.65)); } - - .miner-game__diamond-svg { - width: 62%; - height: 62%; + filter: drop-shadow(0 0 12px rgba(34, 191, 255, 0.58)); +} +/* --------------------------------------------------------- + SSVEP stimulus (see note at the top before changing) + --------------------------------------------------------- */ - - filter: - - drop-shadow( - - 0 0 12px - - rgba(34, 191, 255, 0.58) - - ); - +.miner-ssvep-overlay--stimulus { + background: #050505; } - - -.miner-game__legend { - display: flex; - - gap: 24px; - - align-items: center; - - min-height: 38px; - - padding: 0 18px; - - border-top: 1px solid #17324a; - - color: #6f879d; - - font-size: 9px; - - letter-spacing: 0.1em; -} - - - -.miner-game__legend span { - - display: flex; - - - - gap: 7px; - - - - align-items: center; - -} - - - -.miner-game__legend i { - - width: 9px; - - height: 9px; - - - - display: inline-block; - -} - - - -.miner-game__legend-miner { - - background: #168cff; - -} - - - -.miner-game__legend-diamond { - - transform: rotate(45deg); - - - - background: #22c4ff; - -} - - - -.miner-game__legend-rock { - - background: #435968; - -} - - - -.miner-game__section { - - padding: 18px; - - - - border-bottom: 1px solid #17324a; - -} - - - -.miner-game__section h2 { - - margin: 7px 0 15px; - - - - font-size: 19px; - - font-weight: 600; - -} - - - -.miner-game__section-heading { - - display: flex; - - - - align-items: center; - - justify-content: space-between; - - - - gap: 10px; - -} - - - -.miner-game__section-heading button { - - padding: 4px 8px; - - - - border: 1px solid #1d527b; - - - - color: #8bc8ff; - - - - background: #071b2c; - - - - font-size: 9px; - - - - cursor: pointer; - -} - - - -.miner-game__primary, - -.miner-game__secondary, - -.miner-game__complete-card button { - - width: 100%; - - - - min-height: 43px; - - - - border: 1px solid #168cff; - - - - color: #ffffff; - - - - background: - - linear-gradient( - - 90deg, - - #0856d9, - - #0b85ff - - ); - - - - font-size: 11px; - - font-weight: 700; - - - - letter-spacing: 0.08em; - - - - cursor: pointer; - -} - - - -.miner-game__primary:disabled { - - opacity: 0.35; - - - - cursor: not-allowed; - -} - - - -.miner-game__secondary { - - margin: 18px; - - - - width: calc(100% - 36px); - - - - border-color: #31506a; - - - - color: #acc2d6; - - - - background: #0a1824; - -} - - - -.miner-game__frequency-list { - - display: grid; - - - - gap: 8px; - - - - margin-top: 13px; - -} - - - -.miner-game__frequency-list label { - - display: grid; - - - - grid-template-columns: - - 1fr - - 78px - - 20px; - - - - align-items: center; - - - - gap: 8px; - - - - color: #b9cbda; - - - - font-size: 11px; - -} - - - -.miner-game__frequency-list input { - - width: 100%; - - - - padding: 7px 8px; - - - - border: 1px solid #1d3e59; - - - - outline: none; - - - - color: #ffffff; - - - - background: #040c13; - - - - text-align: right; - -} - - - -.miner-game__frequency-list input:focus { - - border-color: #168cff; - -} - - - -.miner-game__frequency-list small { - - color: #627b91; - -} - - - -.miner-game__duration { - - display: grid; - - - - grid-template-columns: - - 42px - - 1fr - - 42px; - - - - gap: 8px; - - - - margin-top: 13px; - -} - - - -.miner-game__duration button { - - border: 1px solid #274763; - - - - color: #a7c5dc; - - - - background: #081521; - - - - cursor: pointer; - -} - - - -.miner-game__duration button:disabled { - - opacity: 0.3; - -} - - - -.miner-game__duration strong { - - display: grid; - - - - min-height: 39px; - - - - place-items: center; - - - - border: 1px solid #173650; - - - - color: #eef7ff; - - - - background: #071522; - - - - font-size: 14px; - -} - - - -.miner-game__hint, - -.miner-game__dev-note { - - margin: 10px 0 0; - - - - color: #71879a; - - - - font-size: 10px; - - line-height: 1.5; - -} - - - -.miner-game__winner { - - display: flex; - - - - align-items: baseline; - - justify-content: space-between; - - - - margin-top: 13px; - - - - padding: 11px; - - - - border: 1px solid #1b4768; - - - - background: #071a29; - -} - - - -.miner-game__winner span { - - color: #39b9ff; - - - - font-weight: 700; - -} - - - -.miner-game__winner strong { - - font-size: 12px; - -} - - - -.miner-game__scores { - - margin-top: 7px; - -} - - - -.miner-game__scores > div { - - display: flex; - - - - justify-content: space-between; - - - - padding: 6px 1px; - - - - color: #8ea5b8; - - - - font-size: 10px; - -} - - - -.miner-game__error { - - margin-top: 12px; - - - - padding: 10px; - - - - border: 1px solid #6d2833; - - - - color: #ff9daa; - - - - background: #210b10; - -} - - - -.miner-game__error span { - - font-size: 9px; - - font-weight: 700; - - - - letter-spacing: 0.12em; - -} - - - -.miner-game__error p { - - margin: 6px 0 8px; - - - - font-size: 10px; - - line-height: 1.4; - -} - - - -.miner-game__error button { - - border: 1px solid #7e3c47; - - - - color: #ffc4cb; - - - - background: transparent; - - - - font-size: 9px; - - - - cursor: pointer; - -} - - - -.miner-ssvep-overlay, - -.miner-game__complete { - - position: absolute; - - - - z-index: 100; - - - - inset: 0; - - - - background: #02060a; - -} - - - -.miner-ssvep-overlay__countdown, - -.miner-ssvep-overlay__analyzing, - -.miner-game__complete-card { - - position: absolute; - - - - top: 50%; - - left: 50%; - - - - width: min( - - 480px, - - calc(100% - 48px) - - ); - - - - transform: - - translate( - - -50%, - - -50% - - ); - - - - padding: 38px; - - - - border: 1px solid #1d405c; - - - - background: #06111c; - - - - text-align: center; - - - - box-shadow: - - 0 26px 90px - - rgba(0, 0, 0, 0.55); - -} - - - -.miner-ssvep-overlay__countdown > span, - -.miner-ssvep-overlay__analyzing > span, - -.miner-game__complete-card > span { - - color: #168cff; - - - - font-size: 10px; - - font-weight: 700; - - - - letter-spacing: 0.18em; - -} - - - -.miner-ssvep-overlay__countdown h2, - -.miner-ssvep-overlay__analyzing h2, - -.miner-game__complete-card h2 { - - margin: 9px 0; - - - - font-size: 27px; - -} - - - -.miner-ssvep-overlay__countdown strong { - - display: block; - - - - margin: 22px 0; - - - - color: #ffffff; - - - - font-size: 78px; - - font-weight: 300; - - - - line-height: 1; - -} - - - -.miner-ssvep-overlay__countdown p, - -.miner-ssvep-overlay__analyzing p, - -.miner-game__complete-card p { - - color: #7f96aa; - - - - font-size: 12px; - -} - - - -.miner-ssvep-overlay__countdown button, - -.miner-ssvep-overlay__footer button { - - margin-top: 17px; - - - - border: 0; - - - - color: #688198; - - - - background: none; - - - - font-size: 10px; - - - - cursor: pointer; - -} - - - -.miner-ssvep-overlay--stimulus { - - background: #050505; - -} - - - .miner-ssvep-overlay__instruction { - position: absolute; - - - top: 30px; - left: 50%; - - - - transform: - - translateX(-50%); - - - + transform: translateX(-50%); color: #e9edf1; - - - font-size: 15px; - font-weight: 600; - - - - letter-spacing: 0.14em; - } - - .miner-ssvep-overlay__targets { - position: absolute; - - - inset: 70px 80px 100px; - } - - .miner-ssvep-target { - position: absolute; - - - display: flex; - - - flex-direction: column; - align-items: center; - - - gap: 8px; - - - color: #ffffff; - } - - .miner-ssvep-target--up { - top: 4%; - left: 50%; - - - - transform: - - translateX(-50%); - + transform: translateX(-50%); } - - .miner-ssvep-target--left { - top: 50%; - left: 10%; - - - - transform: - - translateY(-50%); - + transform: translateY(-50%); } - - .miner-ssvep-target--right { - top: 50%; - right: 10%; - - - - transform: - - translateY(-50%); - + transform: translateY(-50%); } - - .miner-ssvep-target--down { - bottom: 4%; - left: 50%; - - - - transform: - - translateX(-50%); - + transform: translateX(-50%); } - - .miner-ssvep-target__square { - - width: clamp( - - 115px, - - 15vw, - - 185px - - ); - - - + width: clamp(115px, 15vw, 185px); aspect-ratio: 1; - - - border: 1px solid #3a3a3a; - - - background: #555555; - } - - .miner-ssvep-target span { - font-size: 12px; - font-weight: 700; - - - letter-spacing: 0.1em; - } - - .miner-ssvep-target small { - color: #8b949c; - - - font-size: 10px; +} -} - - - -.miner-ssvep-overlay__footer { - - position: absolute; - - - +.miner-ssvep-overlay__footer { + position: absolute; right: 80px; - bottom: 25px; - left: 80px; - - - display: grid; - - - grid-template-columns: - 1fr - auto - auto - auto; - - - align-items: center; - - - gap: 14px; - - - color: #a6b0b8; - - - font-size: 10px; - } - - .miner-ssvep-overlay__progress { - height: 5px; - - - background: #1c1c1c; - - - overflow: hidden; - } - - .miner-ssvep-overlay__progress > div { - height: 100%; - - - - background: #168cff; - + background: var(--trace); } - - .miner-ssvep-overlay__footer button { - margin: 0; - } - - .miner-ssvep-overlay__scanner { - height: 2px; - - - margin: 30px 0; - - - - background: - - linear-gradient( - - 90deg, - - transparent, - - #168cff, - - transparent - - ); - - - - animation: - - miner-scanner 1.2s - - linear infinite; - -} - - - -.miner-game__trial-error-banner { - - position: fixed; - - - - z-index: 120; - - - - right: 25px; - - bottom: 25px; - - - - width: 360px; - - - - padding: 16px; - - - - border: 1px solid #6f2f39; - - - - color: #ffb4be; - - - - background: #220b10; - - - - box-shadow: - - 0 15px 50px - - rgba(0, 0, 0, 0.5); - -} - - - -.miner-game__trial-error-banner span { - - font-size: 9px; - - font-weight: 700; - - - - letter-spacing: 0.14em; - -} - - - -.miner-game__trial-error-banner p { - - margin: 7px 0 10px; - - - - font-size: 11px; - -} - - - -.miner-game__trial-error-banner button { - - border: 1px solid #84414b; - - - - color: #ffc8ce; - - - - background: transparent; - - - - font-size: 9px; - - - - cursor: pointer; - -} - - - -.miner-game__complete { - - display: grid; - - - - place-items: center; - - - - background: - - rgba( - - 1, - - 5, - - 9, - - 0.94 - - ); - -} - - - -.miner-game__complete-card { - - position: static; - - - - transform: none; - -} - - - -.miner-game__complete-card .miner-game__diamond-svg { - - width: 96px; - - height: 96px; - - - - margin: 20px auto 6px; - + background: linear-gradient(90deg, transparent, var(--trace), transparent); + animation: miner-scanner 1.2s linear infinite; } - - -.miner-game__complete-card button { - - margin-top: 18px; - - - - max-width: 220px; - -} - - - @keyframes miner-scanner { - from { - - transform: - - translateX(-25%); - + transform: translateX(-25%); opacity: 0.25; - } - - - 50% { - - transform: - - translateX(25%); - + transform: translateX(25%); opacity: 1; - } - - - to { - - transform: - - translateX(-25%); - + transform: translateX(-25%); opacity: 0.25; - } - } - - -@media ( - - max-width: 1100px - -) { - .miner-game { - height: auto; - min-height: 100%; - overflow: auto; - } - - - +@media (max-width: 1100px) { .miner-game__layout { - - grid-template-columns: - - 1fr; - - } - - - - .miner-game__control-panel { - - display: grid; - - - - grid-template-columns: - - repeat( - - 2, - - minmax(0, 1fr) - - ); - - } - - - - .miner-game__secondary { - - align-self: center; - + grid-template-columns: minmax(0, 1fr) 270px; } - } diff --git a/src/modules/miner/tests/TrialSampleCollector.test.ts b/src/modules/miner/tests/TrialSampleCollector.test.ts index bb7b056..9109094 100644 --- a/src/modules/miner/tests/TrialSampleCollector.test.ts +++ b/src/modules/miner/tests/TrialSampleCollector.test.ts @@ -1,61 +1,38 @@ -import { - describe, - expect, - it, -} from "vitest"; - -import { - TrialSampleCollector, -} from "../eeg/TrialSampleCollector"; - -describe( - "TrialSampleCollector", - () => { - it( - "clears old samples at the beginning of every trial", - () => { - const collector = - new TrialSampleCollector(); - - collector.begin(2); - - collector.push([ - [1, 2, 3], - [4, 5, 6], - ]); - - expect( - collector.sampleCount(), - ).toBe(3); - - collector.begin(2); - - expect( - collector.sampleCount(), - ).toBe(0); - }, - ); - - it( - "returns the latest requested channel-major window", - () => { - const collector = - new TrialSampleCollector(); - - collector.begin(2); - - collector.push([ - [1, 2, 3, 4], - [5, 6, 7, 8], - ]); - - expect( - collector.latest(2), - ).toEqual([ - [3, 4], - [7, 8], - ]); - }, - ); - }, -); +import { describe, expect, it } from "vitest"; + +import { TrialSampleCollector } from "../eeg/TrialSampleCollector"; + +describe("TrialSampleCollector", () => { + it("clears old samples at the beginning of every trial", () => { + const collector = new TrialSampleCollector(); + + collector.begin(2); + + collector.push([ + [1, 2, 3], + [4, 5, 6], + ]); + + expect(collector.sampleCount()).toBe(3); + + collector.begin(2); + + expect(collector.sampleCount()).toBe(0); + }); + + it("returns the latest requested channel-major window", () => { + const collector = new TrialSampleCollector(); + + collector.begin(2); + + collector.push([ + [1, 2, 3, 4], + [5, 6, 7, 8], + ]); + + expect(collector.latest(2)).toEqual([ + [3, 4], + [7, 8], + ]); + }); +}); diff --git a/src/modules/miner/tests/minerGame.test.ts b/src/modules/miner/tests/minerGame.test.ts index 55d2e29..77e610d 100644 --- a/src/modules/miner/tests/minerGame.test.ts +++ b/src/modules/miner/tests/minerGame.test.ts @@ -1,132 +1,60 @@ -import { - describe, - expect, - it, -} from "vitest"; - -import { - createInitialMinerGame, - MINER_DIAMONDS, - moveMiner, -} from "../game/minerGame"; - -describe( - "miner game", - () => { - it( - "starts unfinished with no diamonds collected", - () => { - const state = - createInitialMinerGame(); - - expect( - state.completed, - ).toBe(false); - - expect( - state.collected, - ).toHaveLength(0); - - expect( - state.moves, - ).toBe(0); - }, - ); +import { describe, expect, it } from "vitest"; + +import { createInitialMinerGame, MINER_DIAMONDS, moveMiner } from "../game/minerGame"; + +describe("miner game", () => { + it("starts unfinished with no diamonds collected", () => { + const state = createInitialMinerGame(); + + expect(state.completed).toBe(false); + + expect(state.collected).toHaveLength(0); + + expect(state.moves).toBe(0); + }); + + it("does not move through a rock", () => { + let state = createInitialMinerGame(); + + // Start: (3,4). Move to (3,3), then try LEFT -> (2,3) open, + // then UP -> (2,2) rock. + state = moveMiner(state, "UP").state; + + state = moveMiner(state, "LEFT").state; + + const before = state; - it( - "does not move through a rock", - () => { - let state = - createInitialMinerGame(); - - // Start: (3,4). Move to (3,3), then try LEFT -> (2,3) open, - // then UP -> (2,2) rock. - state = - moveMiner( - state, - "UP", - ).state; - - state = - moveMiner( - state, - "LEFT", - ).state; - - const before = - state; - - const result = - moveMiner( - state, - "UP", - ); - - expect( - result.moved, - ).toBe(false); - - expect( - result.state, - ).toBe(before); - }, + const result = moveMiner(state, "UP"); + + expect(result.moved).toBe(false); + + expect(result.state).toBe(before); + }); + + it("finishes only after every diamond is collected", () => { + // Test the completion invariant directly by pre-populating every diamond + // except the one at (3,3), which is one move UP from the start. + const finalDiamond = MINER_DIAMONDS.find( + (diamond) => diamond.position[0] === 3 && diamond.position[1] === 3, ); - it( - "finishes only after every diamond is collected", - () => { - // Test the completion invariant directly by pre-populating every diamond - // except the one at (3,3), which is one move UP from the start. - const finalDiamond = - MINER_DIAMONDS.find( - (diamond) => - diamond.position[0] === - 3 && - diamond.position[1] === - 3, - ); - - expect( - finalDiamond, - ).toBeDefined(); - - const collected = - MINER_DIAMONDS.filter( - (diamond) => - diamond.id !== - finalDiamond?.id, - ).map( - (diamond) => - diamond.id, - ); - - const state = { - ...createInitialMinerGame(), - collected, - }; - - const result = - moveMiner( - state, - "UP", - ); - - expect( - result.collectedDiamond, - ).toBe( - finalDiamond?.id, - ); - - expect( - result.state.completed, - ).toBe(true); - - expect( - result.state.collected, - ).toHaveLength( - MINER_DIAMONDS.length, - ); - }, + expect(finalDiamond).toBeDefined(); + + const collected = MINER_DIAMONDS.filter((diamond) => diamond.id !== finalDiamond?.id).map( + (diamond) => diamond.id, ); - }, -); + + const state = { + ...createInitialMinerGame(), + collected, + }; + + const result = moveMiner(state, "UP"); + + expect(result.collectedDiamond).toBe(finalDiamond?.id); + + expect(result.state.completed).toBe(true); + + expect(result.state.collected).toHaveLength(MINER_DIAMONDS.length); + }); +}); diff --git a/src/modules/miner/tests/ssvepConfig.test.ts b/src/modules/miner/tests/ssvepConfig.test.ts index 4216385..c7b4950 100644 --- a/src/modules/miner/tests/ssvepConfig.test.ts +++ b/src/modules/miner/tests/ssvepConfig.test.ts @@ -1,8 +1,4 @@ -import { - describe, - expect, - it, -} from "vitest"; +import { describe, expect, it } from "vitest"; import { TAALON_DEFAULT_TARGETS, @@ -10,73 +6,35 @@ import { validateTaalonFrequencies, } from "../../../core/ssvep"; -describe( - "TaaLON SSVEP configuration", - () => { - it( - "preserves the supplied default frequency-to-command mapping", - () => { - expect( - TAALON_DEFAULT_TARGETS, - ).toEqual([ - { - direction: - "UP", - frequencyHz: - 10.25, - }, - { - direction: - "LEFT", - frequencyHz: - 13.75, - }, - { - direction: - "RIGHT", - frequencyHz: - 14.25, - }, - { - direction: - "DOWN", - frequencyHz: - 14.75, - }, - ]); +describe("TaaLON SSVEP configuration", () => { + it("preserves the supplied default frequency-to-command mapping", () => { + expect(TAALON_DEFAULT_TARGETS).toEqual([ + { + direction: "UP", + frequencyHz: 10.25, }, - ); - - it( - "uses the six posterior electrodes from the supplied game", - () => { - expect( - TAALON_SSVEP_CHANNELS, - ).toEqual([ - "POz", - "PO3", - "PO4", - "Oz", - "O1", - "O2", - ]); + { + direction: "LEFT", + frequencyHz: 13.75, }, - ); - - it( - "rejects frequencies separated by less than 0.5 Hz", - () => { - expect(() => { - validateTaalonFrequencies( - [ - 10.25, - 13.75, - 14.1, - 14.5, - ], - ); - }).toThrow(); + { + direction: "RIGHT", + frequencyHz: 14.25, }, - ); - }, -); + { + direction: "DOWN", + frequencyHz: 14.75, + }, + ]); + }); + + it("uses the six posterior electrodes from the supplied game", () => { + expect(TAALON_SSVEP_CHANNELS).toEqual(["POz", "PO3", "PO4", "Oz", "O1", "O2"]); + }); + + it("rejects frequencies separated by less than 0.5 Hz", () => { + expect(() => { + validateTaalonFrequencies([10.25, 13.75, 14.1, 14.5]); + }).toThrow(); + }); +}); diff --git a/src/modules/neuorrun/NeuorrunModule.tsx b/src/modules/neuorrun/NeuorrunModule.tsx index 6a38390..b47e2be 100644 --- a/src/modules/neuorrun/NeuorrunModule.tsx +++ b/src/modules/neuorrun/NeuorrunModule.tsx @@ -1,21 +1,10 @@ -import { - useEffect, - useMemo, - useRef, - useState, -} from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; -import type { - KNeuronModuleProps, -} from "../../features/modules/moduleDefinition"; +import type { KNeuronModuleProps } from "../../features/modules/moduleDefinition"; -import type { - BrainMetricsSnapshot, -} from "../../core/brainMetrics"; +import type { BrainMetricsSnapshot } from "../../core/brainMetrics"; -import { - useBrainMetrics, -} from "./hooks/useBrainMetrics"; +import { useBrainMetrics } from "./hooks/useBrainMetrics"; import "./styles/neuorrun.css"; @@ -33,11 +22,7 @@ interface UnityBuildManifest { } interface UnityInstance { - SendMessage( - objectName: string, - methodName: string, - value?: string | number, - ): void; + SendMessage(objectName: string, methodName: string, value?: string | number): void; Quit(): Promise; } @@ -66,15 +51,11 @@ function formatMetric(value: number | null | undefined): string { return value == null ? "--" : String(value); } -export function NeuorrunModule({ - onRequestClose, -}: KNeuronModuleProps): JSX.Element { +export function NeuorrunModule(_props: KNeuronModuleProps): JSX.Element { const canvasRef = useRef(null); const unityRef = useRef(null); - const [loadState, setLoadState] = useState< - "loading" | "ready" | "missing" | "error" - >("loading"); + const [loadState, setLoadState] = useState<"loading" | "ready" | "missing" | "error">("loading"); const [progress, setProgress] = useState(0); const [loadError, setLoadError] = useState(null); @@ -100,9 +81,7 @@ export function NeuorrunModule({ }); if (!response.ok) { - throw new Error( - `Neuorrun manifest could not be loaded (${response.status}).`, - ); + throw new Error(`Neuorrun manifest could not be loaded (${response.status}).`); } const manifest = (await response.json()) as UnityBuildManifest; @@ -114,12 +93,7 @@ export function NeuorrunModule({ return; } - if ( - !manifest.loader || - !manifest.data || - !manifest.framework || - !manifest.code - ) { + if (!manifest.loader || !manifest.data || !manifest.framework || !manifest.code) { throw new Error("Neuorrun Unity manifest is incomplete."); } @@ -135,8 +109,7 @@ export function NeuorrunModule({ script.async = true; script.dataset.kneuronUnityLoader = "neuorrun"; script.onload = () => resolve(); - script.onerror = () => - reject(new Error("Neuorrun Unity loader failed to load.")); + script.onerror = () => reject(new Error("Neuorrun Unity loader failed to load.")); document.body.appendChild(script); loaderScript = script; }); @@ -164,15 +137,11 @@ export function NeuorrunModule({ config.symbolsUrl = `/neuorrun/${manifest.symbols}`; } - const instance = await window.createUnityInstance( - canvas, - config, - (value) => { - if (!cancelled) { - setProgress(value); - } - }, - ); + const instance = await window.createUnityInstance(canvas, config, (value) => { + if (!cancelled) { + setProgress(value); + } + }); if (cancelled) { await instance.Quit(); @@ -217,11 +186,7 @@ export function NeuorrunModule({ } try { - instance.SendMessage( - "KNeuronBridge", - "SetMetricsJson", - metricsPayload(metrics), - ); + instance.SendMessage("KNeuronBridge", "SetMetricsJson", metricsPayload(metrics)); } catch { // Unity may be between scenes for a frame. The next metrics event will // retry automatically. @@ -231,50 +196,44 @@ export function NeuorrunModule({ return (
    -
    - NEUORRUN / BRAINLINK - Neuorrun -
    - -
    +
    - BRAINLINK - - {brain.state === "live" ? "LIVE" : "NOT CONNECTED"} - +
    Headset
    +
    + {brain.state === "live" ? "Live" : "Not connected"} +
    - ATTENTION - {formatMetric(brain.metrics?.attention)} +
    Attention
    +
    {formatMetric(brain.metrics?.attention)}
    - MEDITATION - {formatMetric(brain.metrics?.meditation)} +
    Meditation
    +
    {formatMetric(brain.metrics?.meditation)}
    - SIGNAL - {signalLabel} +
    Contact
    +
    {signalLabel}
    - -
    + +

    + Attention controls the game. Keyboard and controller also work. +

    - { - event.currentTarget.focus(); - }} - /> + { + event.currentTarget.focus(); + }} + /> {loadState === "loading" ? (
    - UNITY WEBGL

    Loading Neuorrun

    @@ -285,18 +244,16 @@ export function NeuorrunModule({ {loadState === "missing" ? (
    - UNITY BUILD REQUIRED -

    Neuorrun assets are not installed yet

    +

    Neuorrun game files are missing

    - Build the supplied Unity project for WebGL, then run the package - prepare script described in INTEGRATION.md. + Build the Unity project for WebGL, then copy it here with + unity-patch/prepare_webgl_build.py (see README, “Building Neuorrun manually”).

    ) : null} {loadState === "error" ? (
    - LOAD ERROR

    Neuorrun could not start

    {loadError}

    @@ -304,13 +261,13 @@ export function NeuorrunModule({ {loadState === "ready" && brain.state === "unavailable" ? (
    - BrainLink not connected + No attention signal {brain.message ?? - "Connect BrainLink Lite in Device. Keyboard/controller input remains available."} + "Connect BrainLink Lite on the Devices page. Keyboard and controller still work."}
    ) : null} diff --git a/src/modules/neuorrun/hooks/useBrainMetrics.ts b/src/modules/neuorrun/hooks/useBrainMetrics.ts index 18481cf..c897cd0 100644 --- a/src/modules/neuorrun/hooks/useBrainMetrics.ts +++ b/src/modules/neuorrun/hooks/useBrainMetrics.ts @@ -1,8 +1,4 @@ -import { - useCallback, - useEffect, - useState, -} from "react"; +import { useCallback, useEffect, useState } from "react"; import { brainMetricsService, @@ -10,10 +6,7 @@ import { type BrainMetricsSnapshot, } from "../../../core/brainMetrics"; -export type NeuorrunBrainState = - | "starting" - | "live" - | "unavailable"; +export type NeuorrunBrainState = "starting" | "live" | "unavailable"; export interface NeuorrunBrainSnapshot { state: NeuorrunBrainState; @@ -29,9 +22,7 @@ function errorMessage(error: unknown): string { export function useBrainMetrics(): NeuorrunBrainSnapshot { const [generation, setGeneration] = useState(0); const [state, setState] = useState("starting"); - const [metrics, setMetrics] = useState | null>( - null, - ); + const [metrics, setMetrics] = useState | null>(null); const [message, setMessage] = useState(null); const retry = useCallback(() => { diff --git a/src/modules/neuorrun/neuorrunManifest.ts b/src/modules/neuorrun/neuorrunManifest.ts index c6dc874..2835f9a 100644 --- a/src/modules/neuorrun/neuorrunManifest.ts +++ b/src/modules/neuorrun/neuorrunManifest.ts @@ -1,14 +1,11 @@ -import type { - KNeuronModuleManifest, -} from "../../types/module"; +import type { KNeuronModuleManifest } from "../../types/module"; export const neuorrunManifest: KNeuronModuleManifest = { schemaVersion: 1, id: "neuorrun", name: "Neuorrun", version: "1.0.0", - description: - "Original Neuorrun Unity game integrated with BrainLink Lite attention control.", + description: "Original Neuorrun Unity game integrated with BrainLink Lite attention control.", category: "GAME", appearance: {}, entryPoint: "/modules/neuorrun", @@ -16,5 +13,9 @@ export const neuorrunManifest: KNeuronModuleManifest = { eeg: { required: false, }, + + brainMetrics: { + required: true, + }, }, }; diff --git a/src/modules/neuorrun/neuorrunModuleDefinition.ts b/src/modules/neuorrun/neuorrunModuleDefinition.ts index 54741c2..5cbbdfb 100644 --- a/src/modules/neuorrun/neuorrunModuleDefinition.ts +++ b/src/modules/neuorrun/neuorrunModuleDefinition.ts @@ -1,16 +1,31 @@ +import { createElement, lazy, Suspense } from "react"; + import type { KNeuronModuleDefinition, + KNeuronModuleProps, } from "../../features/modules/moduleDefinition"; -import { - NeuorrunModule, -} from "./NeuorrunModule"; +import { neuorrunManifest } from "./neuorrunManifest"; + +const LazyNeuorrunModule = lazy(async () => { + const module = await import("./NeuorrunModule"); + + return { + default: module.NeuorrunModule, + }; +}); -import { - neuorrunManifest, -} from "./neuorrunManifest"; +function NeuorrunModuleLoader(props: KNeuronModuleProps) { + return createElement( + Suspense, + { + fallback: createElement("div", null, "Loading Neuorrun..."), + }, + createElement(LazyNeuorrunModule, props), + ); +} export const neuorrunModuleDefinition: KNeuronModuleDefinition = { manifest: neuorrunManifest, - component: NeuorrunModule, + component: NeuorrunModuleLoader, }; diff --git a/src/modules/neuorrun/styles/neuorrun.css b/src/modules/neuorrun/styles/neuorrun.css index 2a62ed6..f070499 100644 --- a/src/modules/neuorrun/styles/neuorrun.css +++ b/src/modules/neuorrun/styles/neuorrun.css @@ -1,196 +1,196 @@ +/* Neuorrun: light telemetry strip above the game, game on a dark screen. + Uses the shell tokens from src/styles/app.css. */ + .neuorrun-module { display: flex; flex-direction: column; + width: 100%; height: 100%; min-height: 0; - color: #edf5ff; - background: #02070c; - overflow: hidden; + + gap: 12px; + + color: var(--ink); } .neuorrun-module__bar { - position: relative; - z-index: 5; display: flex; flex: 0 0 auto; align-items: center; justify-content: space-between; - gap: 24px; - min-height: 66px; - padding: 10px 16px 10px 20px; - border-bottom: 1px solid #17324a; - background: #06111c; -} -.neuorrun-module__bar > div:first-child { - display: flex; - flex-direction: column; - gap: 3px; -} - -.neuorrun-module__bar strong { - font-size: 19px; - font-weight: 650; -} + gap: 24px; + padding: 10px 16px; -.neuorrun-module__eyebrow { - color: #168cff; - font-size: 9px; - font-weight: 700; - letter-spacing: 0.16em; + background: var(--face); + border: 1px solid var(--groove); + border-radius: var(--radius-panel); } .neuorrun-module__telemetry { display: flex; - align-items: stretch; - gap: 0; - min-width: 0; - border: 1px solid #18334c; - background: #040c13; -} -.neuorrun-module__telemetry > div { - display: flex; - min-width: 105px; - flex-direction: column; - justify-content: center; - gap: 2px; - padding: 8px 12px; - border-right: 1px solid #18334c; + gap: 32px; + margin: 0; } -.neuorrun-module__telemetry small { - color: #6f879d; - font-size: 8px; - letter-spacing: 0.12em; +.neuorrun-module__telemetry dt { + color: var(--ink-2); + font-size: 13px; } -.neuorrun-module__telemetry span { - color: #ddecfa; - font-size: 11px; - font-weight: 650; +.neuorrun-module__telemetry dd { + margin: 0; + + font-family: var(--font-display); + font-size: 24px; + font-weight: 600; + line-height: 1.1; } -.neuorrun-module__telemetry span.is-live { - color: #28d79b; +.neuorrun-module__telemetry dd.is-live { + color: var(--led-on); } -.neuorrun-module__telemetry button, -.neuorrun-module__brain-warning button { - border: 0; - color: #dbeeff; - background: #0b68d8; - padding: 0 16px; - font-size: 9px; - font-weight: 700; - letter-spacing: 0.08em; - cursor: pointer; +.neuorrun-module__hint { + max-width: 34ch; + + color: var(--ink-3); + font-size: 13px; + text-align: right; } .neuorrun-module__stage { position: relative; + flex: 1; min-height: 0; + background: #000; + border: 1px solid var(--screen-edge); + border-radius: 4px; + box-shadow: inset 0 2px 6px rgb(0 0 0 / 55%); + overflow: hidden; } .neuorrun-module__canvas { display: block; + width: 100%; height: 100%; - outline: none; + background: #000; + outline: none; } .neuorrun-module__overlay { position: absolute; z-index: 10; inset: 0; + display: grid; place-content: center; justify-items: center; + padding: 32px; - color: #dcecff; - background: rgba(2, 7, 12, 0.94); - text-align: center; -} -.neuorrun-module__overlay > span { - color: #168cff; - font-size: 9px; - font-weight: 700; - letter-spacing: 0.17em; + color: #e3ece8; + background: var(--screen); + text-align: center; } .neuorrun-module__overlay h2 { - margin: 10px 0 8px; - font-size: 26px; + font-family: var(--font-display); + font-size: 28px; font-weight: 600; } .neuorrun-module__overlay p { - max-width: 580px; - margin: 0; - color: #8099ad; - font-size: 12px; - line-height: 1.6; + max-width: 58ch; + margin-top: 8px; + + color: var(--screen-label); } -.neuorrun-module__overlay--error > span, -.neuorrun-module__overlay--error p { - color: #ff93a1; +.neuorrun-module__overlay--error h2 { + color: #ff8a80; } .neuorrun-module__progress { - width: min(480px, 70vw); - height: 5px; - margin: 20px 0 9px; - background: #112334; + width: min(420px, 70vw); + height: 4px; + + margin-top: 20px; + + background: var(--screen-grid); + border-radius: 2px; + overflow: hidden; } .neuorrun-module__progress > div { height: 100%; - background: #168cff; + + background: var(--trace); + transition: width 120ms linear; } .neuorrun-module__brain-warning { position: absolute; z-index: 8; - right: 18px; - bottom: 18px; + right: 16px; + bottom: 16px; + display: grid; - grid-template-columns: auto minmax(220px, 420px) auto; + grid-template-columns: minmax(220px, 380px) auto; align-items: center; - gap: 12px; - padding: 10px 10px 10px 14px; - border: 1px solid #6e5522; - color: #f4d99a; - background: rgba(30, 23, 8, 0.94); - box-shadow: 0 14px 45px rgba(0, 0, 0, 0.45); + + gap: 4px 16px; + padding: 12px 12px 12px 16px; + + color: var(--ink); + background: var(--face-raised); + border: 1px solid var(--groove); + border-left: 3px solid var(--led-busy); + border-radius: var(--radius-control); + box-shadow: 0 8px 24px rgb(0 0 0 / 35%); } .neuorrun-module__brain-warning strong { - font-size: 10px; - letter-spacing: 0.06em; + grid-column: 1; + font-weight: 600; } .neuorrun-module__brain-warning span { - color: #baa979; - font-size: 10px; - line-height: 1.4; + grid-column: 1; + + color: var(--ink-2); + font-size: 14px; } .neuorrun-module__brain-warning button { - min-height: 32px; + grid-column: 2; + grid-row: 1 / span 2; + + height: 34px; + + padding: 0 14px; + + color: var(--face-raised); + background: var(--ink); + border: 0; + border-radius: var(--radius-control); + + font-weight: 600; + + cursor: pointer; } @media (max-width: 1100px) { - .neuorrun-module__telemetry > div:nth-child(3), - .neuorrun-module__telemetry > div:nth-child(4) { + .neuorrun-module__hint { display: none; } } diff --git a/src/styles/app.css b/src/styles/app.css index 6894b29..bf1d800 100644 --- a/src/styles/app.css +++ b/src/styles/app.css @@ -1,24 +1,56 @@ -:root { - font-family: Inter, "Segoe UI", Arial, sans-serif; - color: #eef5ff; - background: #030a12; - font-synthesis: none; - - --bg: #030a12; - --panel: #07121e; - --panel-2: #091827; - - --line: #18334c; - --line-soft: #0f2940; - - --text: #f5f8fc; - --muted: #9db0c7; +/* ========================================================= + KNeuron shell — "lab instrument" - --blue: #0b7cff; - --blue-bright: #168cff; - --blue-dark: #1547b9; + Light painted-metal enclosure, dark inset screens where a signal is + shown, status LEDs. Orange marks the one thing that is active. + Module styles live in src/modules//styles and reuse these tokens: + visualizations sit on a dark screen, controls on the light face. + ========================================================= */ - --danger: #ff4655; +:root { + /* Enclosure */ + --enclosure: #d3d6d2; + --face: #e7e9e5; + --face-raised: #f2f3f0; + --groove: #b9beb9; + --groove-soft: #c8ccc7; + + /* Ink */ + --ink: #1c2226; + --ink-2: #4c555a; + --ink-3: #6f777b; + + /* Screen */ + --screen: #101617; + --screen-edge: #050808; + --screen-grid: #1c2829; + --screen-label: #6f8f86; + --trace: #8ed9c0; + --trace-dim: #5d9283; + + /* Indicators */ + --active: #d9480f; + --led-on: #2f9e44; + --led-busy: #e8a317; + --led-error: #c92a2a; + --led-off: #9aa19c; + + --focus: #d9480f; + + --font: "Barlow", "Segoe UI", system-ui, sans-serif; + --font-display: "Barlow Semi Condensed", "Barlow", "Segoe UI", sans-serif; + + --radius-panel: 6px; + --radius-control: 3px; + + font-family: var(--font); + font-size: 15px; + line-height: 1.45; + color: var(--ink); + background: var(--enclosure); + font-synthesis: none; + font-variant-numeric: tabular-nums; + -webkit-font-smoothing: antialiased; } * { @@ -31,7 +63,7 @@ body, margin: 0; min-width: 900px; min-height: 100%; - background: var(--bg); + background: var(--enclosure); } body { @@ -53,12 +85,25 @@ button:focus-visible, input:focus-visible, textarea:focus-visible, select:focus-visible { - outline: 1px solid var(--blue-bright); + outline: 2px solid var(--focus); outline-offset: 2px; } +h1, +h2, +h3, +p { + margin: 0; +} + +/* Kept for module code that still uses it. */ +.eyebrow { + font-size: 12px; + opacity: 0.7; +} + /* ========================================================= - Application shell + Shell grid ========================================================= */ .app-shell { @@ -68,48 +113,57 @@ select:focus-visible { height: 100vh; min-height: 620px; - grid-template-columns: 232px minmax(520px, 1fr) 310px; - grid-template-rows: 86px minmax(0, 1fr); + grid-template-columns: minmax(0, 1fr) 340px; + grid-template-rows: 46px minmax(0, 1fr); - background: radial-gradient(circle at 50% 35%, #071829 0, #030a12 44%, #02070c 100%); + background: var(--enclosure); overflow: hidden; - - transition: grid-template-columns 180ms ease; } .main-content { grid-row: 2; + grid-column: 1; min-width: 0; min-height: 0; - padding: 34px 30px 40px; + margin: 0 0 12px 12px; + padding: 36px 44px 48px; + + background: var(--face); + border: 1px solid var(--groove); + border-radius: var(--radius-panel); overflow: auto; +} + +.app-shell--module-open { + grid-template-columns: minmax(0, 1fr) 0; +} + +.app-shell--module-open .main-content { + margin-right: 12px; + padding: 12px; +} - transition: - padding-left 180ms ease, - padding-right 180ms ease; +.app-shell--module-open .readout { + visibility: hidden; } /* ========================================================= - Title bar + Title bar + mode switch ========================================================= */ .titlebar { grid-column: 1 / -1; + grid-row: 1; display: flex; align-items: center; - justify-content: space-between; - padding-left: 25px; - - border-top: 1px solid var(--line); - border-bottom: 1px solid var(--line); - - background: #06111d; + gap: 28px; + padding-left: 18px; user-select: none; } @@ -118,1846 +172,951 @@ select:focus-visible { display: flex; align-items: center; - gap: 12px; + gap: 9px; - color: #0784ff; + color: var(--ink); } .brand strong { - display: block; - - color: #ffffff; - - font-size: 25px; - font-weight: 700; - letter-spacing: 0.2px; + font-family: var(--font-display); + font-size: 19px; + font-weight: 600; + letter-spacing: 0.01em; } -.brand span { - display: block; - - margin-top: 2px; - - color: #b9c8dc; - - font-size: 12px; +.titlebar__spacer { + flex: 1; + align-self: stretch; } -.window-controls { +.mode-switch { display: flex; - height: 100%; + padding: 3px; + + background: var(--groove-soft); + border: 1px solid var(--groove); + border-radius: var(--radius-control); + box-shadow: inset 0 1px 2px rgb(28 34 38 / 12%); } -.window-controls button { - display: grid; - place-items: center; +.mode-switch__item { + position: relative; - width: 48px; + min-width: 92px; + height: 28px; - border: 0; + padding: 0 14px; - color: #d8e5f4; + color: var(--ink-2); background: transparent; + border: 0; + border-radius: 2px; + + font-weight: 500; cursor: pointer; } -.window-controls button:hover { - background: #122235; +.mode-switch__item:hover { + color: var(--ink); } -.window-controls .close:hover { - color: #ffffff; - background: #c42b36; +.mode-switch__item.is-active { + color: var(--ink); + background: var(--face-raised); + box-shadow: + 0 1px 0 rgb(255 255 255 / 70%) inset, + 0 1px 2px rgb(28 34 38 / 18%); } -/* ========================================================= - Left navigation - ========================================================= */ - -.sidebar { - grid-row: 2; +/* Orange indicator lamp on the selected mode. */ +.mode-switch__item.is-active::before { + content: ""; - position: relative; + position: absolute; + top: 50%; + left: 8px; - display: flex; - min-width: 0; + width: 5px; + height: 5px; - flex-direction: column; + margin-top: -2.5px; - border-right: 1px solid var(--line); + background: var(--active); + border-radius: 50%; +} - background: linear-gradient(180deg, #06121f, #06101b 70%, #07131f); +.window-controls { + display: flex; + align-self: stretch; +} - overflow: hidden; +.window-controls button { + width: 46px; - transition: - opacity 140ms ease, - border-color 140ms ease; -} + display: grid; + place-items: center; -.sidebar nav { - z-index: 2; + color: var(--ink-2); + background: transparent; + border: 0; - padding: 20px 8px; + cursor: pointer; } -.nav-item { - position: relative; +.window-controls button:hover { + color: var(--ink); + background: var(--groove-soft); +} - display: flex; - align-items: center; +.window-controls .close:hover { + color: #fff; + background: var(--led-error); +} - width: 100%; - height: 56px; +/* ========================================================= + Buttons + ========================================================= */ - gap: 18px; - padding: 0 25px; +.primary-button, +.secondary-button { + height: 36px; - border: 0; + padding: 0 18px; - color: #bdcce0; - background: transparent; + border-radius: var(--radius-control); - text-align: left; + font-weight: 600; + white-space: nowrap; cursor: pointer; } -.nav-item:hover { - color: #ffffff; - background: #0a1a2a; +.primary-button { + color: var(--face-raised); + background: var(--ink); + border: 1px solid var(--ink); + box-shadow: 0 1px 0 rgb(255 255 255 / 12%) inset; } -.nav-item.active { - color: #ffffff; - background: linear-gradient(90deg, #173ac0, #123475 65%, #102a57); +.primary-button:hover:not(:disabled) { + background: #2c353a; } -.nav-item.active::before { - content: ""; - - position: absolute; - - top: 0; - bottom: 0; - left: 0; - - width: 3px; - - background: #13a0ff; +.secondary-button { + color: var(--ink); + background: var(--face-raised); + border: 1px solid var(--groove); + box-shadow: 0 1px 0 rgb(255 255 255 / 80%) inset; +} - box-shadow: 0 0 14px #13a0ff; +.secondary-button:hover:not(:disabled) { + border-color: var(--ink-3); } -.wave { - position: absolute; +.primary-button:disabled, +.secondary-button:disabled { + opacity: 0.45; + cursor: default; +} - right: 0; - bottom: 78px; - left: 0; +/* ========================================================= + LEDs + ========================================================= */ - height: 190px; +.led { + display: inline-block; + flex: none; - opacity: 0.8; + width: 9px; + height: 9px; - pointer-events: none; + background: var(--led-off); + border-radius: 50%; + box-shadow: inset 0 -1px 1px rgb(0 0 0 / 25%); } -.wave svg { - width: 100%; - height: 100%; +.led--on, +.led--connected { + background: var(--led-on); + box-shadow: + inset 0 -1px 1px rgb(0 0 0 / 20%), + 0 0 0 3px rgb(47 158 68 / 18%); } -.wave path { - fill: none; +.led--connecting, +.led--disconnecting { + background: var(--led-busy); + animation: led-blink 0.9s steps(2, jump-none) infinite; +} - stroke: #096dff; - stroke-width: 1; +.led--error { + background: var(--led-error); + box-shadow: 0 0 0 3px rgb(201 42 42 / 18%); +} - opacity: 0.58; +@keyframes led-blink { + 50% { + opacity: 0.35; + } } -.sidebar footer { - z-index: 2; +/* ========================================================= + Pages + ========================================================= */ - margin-top: auto; - padding: 0 28px 24px; +.page { + max-width: 880px; } -.sidebar footer strong, -.sidebar footer span { - display: block; +.page__header { + margin-bottom: 28px; } -.sidebar footer strong { - font-size: 13px; +.page__header h1 { + font-family: var(--font-display); + font-size: 34px; + font-weight: 600; + line-height: 1.1; + letter-spacing: -0.005em; } -.sidebar footer span { +.page__header p { + max-width: 62ch; margin-top: 6px; - color: #9cb0c8; - - font-size: 10px; + color: var(--ink-2); } -/* ========================================================= - Global device panel - ========================================================= */ +.empty-state { + padding: 28px; -.device-panel { - grid-row: 2; + border: 1px dashed var(--groove); + border-radius: var(--radius-panel); +} - min-width: 0; +.empty-state p { + margin-top: 4px; + color: var(--ink-2); +} - border-left: 1px solid var(--line); +/* ========================================================= + Module list + ========================================================= */ - background: #06111d; +.module-list, +.device-list { + margin: 0; + padding: 0; - overflow: hidden; + list-style: none; - transition: - opacity 140ms ease, - border-color 140ms ease; + border-top: 1px solid var(--groove); } -.panel-title { - display: flex; +.module-row { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; align-items: center; - justify-content: space-between; - height: 64px; - - padding: 0 22px; + gap: 24px; + padding: 22px 0; - border-bottom: 1px solid var(--line-soft); + border-bottom: 1px solid var(--groove); } -.panel-title h2 { - margin: 0; - - font-size: 18px; +.module-row__title { + font-family: var(--font-display); + font-size: 23px; + font-weight: 600; + line-height: 1.15; } -.device-empty { - padding: 54px 26px; - - color: var(--muted); +.module-row__description { + max-width: 64ch; + margin-top: 4px; - text-align: center; + color: var(--ink-2); } -.device-orbit { - display: grid; - place-items: center; - - width: 92px; - height: 92px; - - margin: 0 auto 24px; +.module-row__requirement { + display: flex; + flex-wrap: wrap; + align-items: center; - border: 1px solid #294865; + gap: 4px 10px; + margin-top: 12px; - transform: rotate(45deg); + font-size: 14px; } -.device-orbit span { - width: 34px; - height: 34px; +.module-row__need { + display: inline-flex; + align-items: center; - border: 2px solid #0d7dff; + gap: 9px; } -.device-empty h3 { - color: #edf5ff; +.module-row__missing { + padding: 0 6px; - font-size: 16px; -} + color: var(--ink-2); + background: var(--groove-soft); + border-radius: 2px; -.device-empty p { font-size: 13px; - line-height: 1.6; } -/* ========================================================= - Shared shell UI - ========================================================= */ - -.eyebrow { - color: var(--blue-bright); +.module-row__error { + margin-top: 8px; - font-size: 10px; - font-weight: 700; - letter-spacing: 2px; + color: var(--led-error); + font-size: 14px; } -.primary-button, -.secondary-button { - display: inline-flex; - align-items: center; - justify-content: center; - - min-height: 44px; +/* ========================================================= + Device list + ========================================================= */ - gap: 8px; - padding: 0 18px; +.device-row { + display: grid; + grid-template-columns: auto minmax(0, 1fr) auto; + align-items: start; - border: 1px solid #197ff4; + gap: 18px; + padding: 20px 0; - color: #ffffff; + border-bottom: 1px solid var(--groove); +} - cursor: pointer; +.device-row .led { + margin-top: 9px; } -.primary-button { - background: linear-gradient(110deg, #1655e9, #087cff); +.device-row > button { + align-self: center; } -.primary-button:hover:not(:disabled) { - filter: brightness(1.12); +.device-row h2 { + font-family: var(--font-display); + font-size: 21px; + font-weight: 600; + line-height: 1.2; } -.secondary-button { - border-color: #28445e; +.device-row__identity { + margin-top: 2px; + color: var(--ink-2); +} - background: #0a1724; +.device-row__signal { + margin-top: 8px; + font-size: 14px; } -.secondary-button:hover:not(:disabled) { - border-color: #3c6385; +.device-row__message { + margin-top: 6px; - background: #0d1e2e; + color: var(--ink-3); + font-size: 14px; } -.primary-button:disabled, -.secondary-button:disabled { - opacity: 0.45; +.device-row__error { + margin-top: 6px; - cursor: not-allowed; + color: var(--led-error); + font-size: 14px; } /* ========================================================= - Placeholder shell pages + Readout (right panel) ========================================================= */ -.placeholder-page { - max-width: 680px; -} +.readout { + grid-row: 2; + grid-column: 2; -.placeholder-page h1 { - margin: 6px 0 4px; + display: flex; + flex-direction: column; - font-size: 34px; -} + min-height: 0; -.placeholder-page > p { - margin: 0; + gap: 18px; + margin: 0 12px 12px; + padding: 18px; - color: var(--muted); + background: var(--face); + border: 1px solid var(--groove); + border-radius: var(--radius-panel); - line-height: 1.6; + overflow: auto; } -.architecture-note { - margin-top: 36px; - padding: 20px; +.readout__header { + display: flex; + align-items: center; - border-left: 3px solid #167fff; + gap: 12px; +} - background: #071522; +.readout__header h2 { + font-family: var(--font-display); + font-size: 20px; + font-weight: 600; + line-height: 1.15; } -.architecture-note strong, -.architecture-note span { - display: block; +.readout__header p { + color: var(--ink-2); + font-size: 14px; } -.architecture-note span { - margin-top: 8px; +.readout__screen { + padding: 8px; - color: #9eb1c7; + background: var(--screen); + border: 1px solid var(--screen-edge); + border-radius: 4px; + box-shadow: + inset 0 2px 6px rgb(0 0 0 / 55%), + 0 1px 0 rgb(255 255 255 / 70%); +} - line-height: 1.6; +.scope { + position: relative; } -/* ========================================================= - Dashboard - ========================================================= */ +.scope__canvas { + display: block; -.dashboard { width: 100%; - min-width: 0; + height: 300px; } -.dashboard__header { - display: flex; - align-items: flex-start; - justify-content: space-between; +.scope__error { + position: absolute; + inset: auto 8px 8px; - margin-bottom: 20px; + color: #ff8a80; + font-size: 13px; } -.dashboard__title { - margin: 0; - - color: #f4f8fc; +.readout__caption { + padding: 6px 2px 0; - font-size: 24px; - font-weight: 600; + color: var(--screen-label); + font-size: 12.5px; } -.dashboard__subtitle { - margin: 5px 0 0; +.readout__figures { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(80px, 1fr)); - color: #8297a9; + margin: 0; - font-size: 13px; + border-top: 1px solid var(--groove); } -.dashboard-empty { - display: flex; - min-height: 260px; - - flex-direction: column; - align-items: center; - justify-content: center; - - margin-top: 28px; +.readout__figures > div { + padding: 12px 0 0; +} - border: 1px solid #142d43; +.readout__figures dt { + color: var(--ink-2); + font-size: 13px; +} - background: #06111c; +.readout__figures dd { + margin: 0; - text-align: center; + font-family: var(--font-display); + font-size: 30px; + font-weight: 600; + line-height: 1.1; } -.dashboard-empty h2 { - margin: 0 0 8px; - - color: #eaf3fa; +.readout__figures small { + margin-left: 3px; - font-size: 17px; + color: var(--ink-3); + font-family: var(--font); + font-size: 14px; font-weight: 500; } -.dashboard-empty p { - margin: 0; - - color: #71869a; +.readout__error { + color: var(--led-error); + font-size: 14px; +} - font-size: 13px; +.readout__action { + align-self: flex-start; } /* ========================================================= - Module launcher + Settings ========================================================= */ -.module-grid { - display: grid; - - width: 100%; - - grid-template-columns: repeat(3, minmax(0, 1fr)); +.settings-section { + margin-bottom: 30px; +} - gap: 16px; - margin-top: 28px; - - align-items: stretch; -} - -.module-card { - display: flex; - min-width: 0; - - flex-direction: column; - - overflow: hidden; - - border: 1px solid #16314a; - - background: #07131f; -} - -.module-thumbnail { - display: block; - - width: 100%; - - aspect-ratio: 16 / 9; - - object-fit: cover; - - border-bottom: 1px solid #142d43; - - background: #07111d; - - user-select: none; -} - -.module-thumbnail--fallback { - display: flex; - align-items: center; - justify-content: center; - - background: linear-gradient(135deg, rgba(0, 119, 255, 0.12), rgba(0, 183, 255, 0.03)), #07111d; -} - -.module-thumbnail--fallback span { - color: rgba(144, 199, 255, 0.55); - - font-size: 48px; - font-weight: 600; - line-height: 1; -} - -.module-card__content { - display: flex; - flex: 1; - - flex-direction: column; - - padding: 14px; -} - -.module-card__header { - display: flex; - align-items: baseline; - justify-content: space-between; - - gap: 12px; -} - -.module-card__title { - margin: 0; - - color: #f4f8fc; - - font-size: 18px; - font-weight: 600; -} - -.module-card__version { - flex-shrink: 0; - - color: #71869a; - - font-size: 11px; -} - -.module-card__description { - margin: 8px 0 14px; - - color: #a8bac9; - - font-size: 13px; - line-height: 1.5; -} - -.module-card__meta { - display: flex; - flex-wrap: wrap; - - gap: 6px; - - margin-top: auto; - margin-bottom: 12px; -} - -.module-card__meta span { - padding: 4px 7px; - - border: 1px solid #173650; - - color: #6ebcff; - background: #081a29; - - font-size: 10px; - font-weight: 500; - letter-spacing: 0.04em; - text-transform: uppercase; -} - -.module-card__error { - margin: 0 0 10px; - - color: #e49ba7; - - font-size: 11px; - line-height: 1.4; -} - -.module-card__open { - width: 100%; - - padding: 10px 12px; - - border: 1px solid #168cff; - - color: #ffffff; - background: linear-gradient(90deg, #006eff 0%, #168cff 100%); - - font: inherit; - font-size: 13px; - font-weight: 600; - - cursor: pointer; -} - -.module-card__open:hover:not(:disabled) { - filter: brightness(1.08); -} - -.module-card__open:disabled { - opacity: 0.45; - - cursor: not-allowed; -} - -/* ========================================================= - Module host - ========================================================= */ - -.module-host { - width: 100%; - min-width: 0; -} - -.module-host__header { - display: flex; - align-items: flex-start; - justify-content: space-between; - - gap: 24px; - - margin-bottom: 20px; -} - -.module-host__header h1 { - margin: 6px 0; - - color: #f4f8fc; - - font-size: 24px; - font-weight: 600; -} - -.module-host__header p { - margin: 0; - - color: #8297a9; - - font-size: 13px; -} - -.module-host__back { - display: inline-flex; - align-items: center; - - gap: 7px; - - margin-bottom: 18px; - padding: 0; - - border: 0; - - color: #62b6ff; - background: transparent; - - font: inherit; - font-size: 12px; - - cursor: pointer; -} - -.module-host__back:hover { - color: #98d0ff; -} - -.module-host__status { - display: flex; - min-width: 110px; - - flex-direction: column; - - padding: 10px 12px; - - border: 1px solid #173650; - - background: #07131f; -} - -.module-host__status span { - color: #71869a; - - font-size: 10px; - letter-spacing: 0.06em; - text-transform: uppercase; -} - -.module-host__status strong { - margin-top: 4px; - - color: #61b8ff; - - font-size: 13px; - font-weight: 500; - text-transform: uppercase; -} - -.module-host__placeholder { - display: flex; - min-height: 420px; - - flex-direction: column; - align-items: center; - justify-content: center; - - border: 1px solid #16314a; - - background: #06111c; - - text-align: center; -} - -.module-host__placeholder h2 { - margin: 8px 0; - - color: #edf6fc; - - font-size: 20px; - font-weight: 500; -} - -.module-host__placeholder p { - max-width: 520px; - - margin: 0; - - color: #7f95a8; - - font-size: 13px; - line-height: 1.6; -} - -.module-host__placeholder code { - margin-top: 20px; - padding: 7px 10px; - - border: 1px solid #16314a; - - color: #5eb6ff; - background: #07131f; - - font-family: Consolas, "Courier New", monospace; - font-size: 11px; -} - -.module-host--missing { - padding: 24px; - - border: 1px solid #56333a; - - background: #160d12; -} - -/* ========================================================= - Module error boundary - ========================================================= */ - -.module-error { - display: flex; - min-height: 420px; - - flex-direction: column; - align-items: center; - justify-content: center; - - padding: 32px; - - border: 1px solid #57313a; - - background: #150d12; - - text-align: center; -} - -.module-error h1 { - margin: 8px 0; - - color: #f3dfe3; - - font-size: 22px; -} - -.module-error p { - max-width: 520px; - - color: #aa858d; - - font-size: 13px; - line-height: 1.6; -} - -.module-error code { - margin: 10px 0 20px; - padding: 8px 10px; - - border: 1px solid #57313a; - - color: #e29ca9; - background: #0c080a; - - font-family: Consolas, "Courier New", monospace; -} - -/* ========================================================= - Module focus mode - ========================================================= */ - -/* - * Both global side panels remain mounted while a module is open. - * Only their visual columns collapse. This preserves future shell-level - * state such as DeviceManager state across module navigation. - */ -.app-shell--module-open { - grid-template-columns: 0 minmax(0, 1fr) 0; -} - -.app-shell--module-open .sidebar, -.app-shell--module-open .device-panel { - width: 0; - min-width: 0; - - opacity: 0; - - overflow: hidden; - pointer-events: none; - - border-color: transparent; -} - -.app-shell--module-open .main-content { - padding-right: 24px; - padding-left: 24px; -} - -/* ========================================================= - Responsive layout - ========================================================= */ - -@media (max-width: 1200px) { - .module-grid { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } -} - -@media (max-width: 1150px) { - .app-shell:not(.app-shell--module-open) { - grid-template-columns: 210px minmax(500px, 1fr); - } - - .app-shell:not(.app-shell--module-open) .device-panel { - display: none; - } -} - -@media (max-width: 820px) { - .module-grid { - grid-template-columns: minmax(0, 1fr); - } -} - -/* ========================================================= - Accessibility - ========================================================= */ - -@media (prefers-reduced-motion: reduce) { - *, - *::before, - *::after { - scroll-behavior: auto !important; - transition-duration: 0.01ms !important; - animation-duration: 0.01ms !important; - animation-iteration-count: 1 !important; - } -} - -/* ========================================================= - Settings - ========================================================= */ - -.settings-page { - width: 100%; - max-width: 960px; -} - -.settings-page__header { - margin-bottom: 32px; -} - -.settings-page__header h1 { - margin: 6px 0 4px; - - color: #f4f8fc; - - font-size: 30px; - font-weight: 600; -} - -.settings-page__header p { - margin: 0; - - color: #8297a9; - - font-size: 13px; -} - -.settings-section { - margin-bottom: 28px; -} - -.settings-section__heading { - margin-bottom: 10px; -} - -.settings-section__heading h2 { - margin: 0; - - color: #dce8f3; - - font-size: 13px; - font-weight: 600; - - letter-spacing: 0.08em; - text-transform: uppercase; -} - -.settings-section__heading p { - margin: 5px 0 0; - - color: #71869a; - - font-size: 12px; -} - -.settings-panel { - border: 1px solid #16314a; - - background: #07131f; -} - -.settings-row { - display: flex; - align-items: center; - justify-content: space-between; - - min-height: 72px; - - gap: 24px; - padding: 14px 18px; - - border-bottom: 1px solid #10283d; -} - -.settings-row:last-child { - border-bottom: 0; -} - -.settings-row > div { - display: flex; - min-width: 0; - - flex-direction: column; - - gap: 4px; -} - -.settings-row strong { - color: #edf5fc; - - font-size: 13px; - font-weight: 500; -} - -.settings-row span { - color: #7e94a7; - - font-size: 11px; - line-height: 1.5; -} - -.settings-row code { - flex-shrink: 0; - - padding: 5px 8px; - - border: 1px solid #173650; - - color: #61b8ff; - background: #081a29; - - font-family: Consolas, "Courier New", monospace; - font-size: 11px; - text-transform: uppercase; -} - -/* ========================================================= - Interactive settings controls - ========================================================= */ - -.setting-toggle { - display: inline-flex; - align-items: center; - - gap: 10px; - - padding: 0; - - border: 0; - - color: #7f95aa; - background: transparent; - - cursor: pointer; -} - -.setting-toggle__track { - position: relative; - - display: block; - - width: 38px; - height: 20px; - - border: 1px solid #29445d; - - background: #091522; - - transition: - border-color 140ms ease, - background 140ms ease; -} - -.setting-toggle__thumb { - position: absolute; - - top: 3px; - left: 3px; - - width: 12px; - height: 12px; - - background: #60778d; - - transition: - transform 140ms ease, - background 140ms ease; -} - -.setting-toggle--enabled .setting-toggle__track { - border-color: #168cff; - - background: #0a2847; -} - -.setting-toggle--enabled .setting-toggle__thumb { - background: #39a5ff; - - transform: translateX(18px); -} - -.setting-toggle__value { - min-width: 25px; - - color: #7f95aa; - - font-family: Consolas, "Courier New", monospace; - font-size: 10px; -} - -.setting-toggle--enabled .setting-toggle__value { - color: #5eb6ff; -} - -.setting-select { - min-width: 92px; - height: 36px; - - padding: 0 10px; - - border: 1px solid #173650; - - border-radius: 0; - - color: #61b8ff; - background: #081a29; - - font-family: Consolas, "Courier New", monospace; - font-size: 11px; - - outline: none; - - cursor: pointer; -} - -.setting-select:hover { - border-color: #2e6795; -} - -.setting-select:focus { - border-color: #168cff; -} - -.settings-actions { - display: flex; - justify-content: flex-end; - - padding-top: 4px; - padding-bottom: 32px; -} -/* ========================================================= - Notifications - ========================================================= */ - -.notification-center { - position: fixed; - - top: 104px; - right: 20px; - - z-index: 100; - - display: flex; - width: 340px; - - flex-direction: column; - - gap: 8px; - - pointer-events: none; -} - -.notification { - display: grid; - - grid-template-columns: 28px minmax(0, 1fr) 30px; - - gap: 10px; - - padding: 12px; - - border: 1px solid #24415d; - - background: #07131f; - - box-shadow: 0 12px 30px rgba(0, 0, 0, 0.32); - - pointer-events: auto; -} - -.notification--success { - border-left: 3px solid #23c483; -} - -.notification--info { - border-left: 3px solid #168cff; -} - -.notification--warning { - border-left: 3px solid #e5a83a; -} - -.notification--error { - border-left: 3px solid #e45666; -} - -.notification__icon { - display: flex; - align-items: flex-start; - justify-content: center; - - padding-top: 2px; - - color: #79bfff; -} - -.notification--success .notification__icon { - color: #23c483; -} - -.notification--warning .notification__icon { - color: #e5a83a; -} - -.notification--error .notification__icon { - color: #e45666; -} - -.notification__content { - display: flex; - - flex-direction: column; - - gap: 4px; -} - -.notification__content strong { - color: #f1f6fb; - - font-size: 12px; - font-weight: 600; -} - -.notification__content span { - color: #889eb1; - - font-size: 11px; - line-height: 1.45; -} - -.notification__close { - display: grid; - place-items: center; - - width: 28px; - height: 28px; - - border: 0; - - color: #738aa0; - background: transparent; - - cursor: pointer; -} - -.notification__close:hover { - color: #ffffff; - - background: #102338; -} - -/* ========================================================= - Debug panel - ========================================================= */ - -.debug-panel { - position: fixed; - - right: 20px; - bottom: 20px; - - z-index: 90; - - width: 460px; - max-height: 320px; - - border: 1px solid #24415d; - - background: #050d16; - - box-shadow: 0 16px 40px rgba(0, 0, 0, 0.38); - - overflow: hidden; -} - -.debug-panel__header { - display: flex; - align-items: center; - justify-content: space-between; - - height: 48px; - - padding: 0 12px; - - border-bottom: 1px solid #173047; - - background: #081522; -} - -.debug-panel__header > div { - display: flex; - - flex-direction: column; - - gap: 2px; -} - -.debug-panel__header strong { - color: #eef6fc; - - font-size: 12px; -} - -.debug-panel__header span { - color: #6f869b; - - font-size: 9px; -} - -.debug-panel__header button { - display: grid; - place-items: center; - - width: 30px; - height: 30px; - - border: 0; - - color: #7690a7; - background: transparent; - - cursor: pointer; -} - -.debug-panel__header button:hover { - color: #ffffff; - - background: #102338; -} - -.debug-panel__entries { - max-height: 270px; - - overflow-y: auto; -} - -.debug-panel__empty { - padding: 18px; - - color: #667e94; - - font-size: 11px; -} - -.debug-entry { - display: grid; - - grid-template-columns: 62px 100px minmax(0, 1fr); - - gap: 8px; - - padding: 7px 10px; - - border-bottom: 1px solid #0d2234; - - font-family: Consolas, "Courier New", monospace; - - font-size: 10px; -} - -.debug-entry__time { - color: #536e86; -} - -.debug-entry__source { - color: #5eb6ff; -} - -.debug-entry__message { - color: #a7bacb; - - overflow-wrap: anywhere; -} - -.debug-entry--warning .debug-entry__message { - color: #d8a84d; -} - -.debug-entry--error .debug-entry__message { - color: #e78f9a; -} - -/* ========================================================= - Device page - ========================================================= */ - -.device-page { - width: 100%; - max-width: 1080px; -} - -.device-page__header { - margin-bottom: 32px; -} - -.device-page__header h1 { - margin: 6px 0 4px; - - color: #f4f8fc; - - font-size: 30px; - font-weight: 600; -} - -.device-page__header p { - margin: 0; - - color: #8297a9; - - font-size: 13px; -} - -.device-section { - width: 100%; -} - -.device-section__heading { - display: flex; - align-items: flex-end; - justify-content: space-between; - - gap: 20px; - - margin-bottom: 12px; -} - -.device-section__heading h2 { - margin: 0; - - color: #dce8f3; - - font-size: 13px; - font-weight: 600; - - letter-spacing: 0.08em; - text-transform: uppercase; -} - -.device-section__heading p { - margin: 5px 0 0; - - color: #71869a; - - font-size: 11px; -} - -.device-active-indicator { - color: #55b4ff; - - font-family: Consolas, "Courier New", monospace; - font-size: 10px; - - text-transform: uppercase; -} - -.device-grid { - display: grid; - - grid-template-columns: repeat(2, minmax(0, 1fr)); - - gap: 14px; -} - -.device-card { - display: flex; - - min-height: 260px; - - flex-direction: column; - - padding: 18px; - - border: 1px solid #17314a; - - background: #07131f; -} - -.device-card--active { - border-color: #236fa8; - - box-shadow: inset 3px 0 0 #168cff; -} - -.device-card__header { - display: flex; - align-items: flex-start; - justify-content: space-between; - - gap: 20px; +.settings-section__heading { + margin-bottom: 10px; } -.device-card__header h2 { - margin: 5px 0 0; - - color: #f0f6fc; - - font-size: 18px; +.settings-section__heading h2 { + font-family: var(--font-display); + font-size: 21px; font-weight: 600; } -.device-status { - padding: 5px 8px; +.settings-section__heading p { + color: var(--ink-3); + font-size: 14px; +} - border: 1px solid #24405a; +.settings-panel { + border-top: 1px solid var(--groove); +} - color: #7890a5; - background: #081522; +.settings-row { + display: flex; + align-items: center; + justify-content: space-between; - font-family: Consolas, "Courier New", monospace; - font-size: 9px; + gap: 24px; + padding: 14px 0; - text-transform: uppercase; + border-bottom: 1px solid var(--groove); } -.device-status--connected { - border-color: #1c6b57; - - color: #48cf9c; +.settings-row > div { + display: flex; + flex-direction: column; } -.device-status--connecting, -.device-status--disconnecting { - border-color: #735d25; - - color: #e3bb54; +.settings-row strong { + font-weight: 600; } -.device-status--error { - border-color: #71333b; +.settings-row span { + color: var(--ink-2); + font-size: 14px; +} - color: #e9818d; +.settings-row code { + font-family: var(--font-display); + font-size: 17px; + font-weight: 600; } -.device-card__identity { +.setting-toggle { display: flex; + align-items: center; - gap: 5px; - - margin-top: 10px; + gap: 10px; + padding: 0; - color: #748a9d; + color: var(--ink-2); + background: transparent; + border: 0; - font-size: 11px; + cursor: pointer; } -.device-card__metadata { - display: grid; - - grid-template-columns: repeat(2, minmax(0, 1fr)); - - gap: 1px; - - margin-top: 22px; +.setting-toggle__track { + position: relative; - border: 1px solid #122a40; + width: 44px; + height: 22px; - background: #122a40; + background: var(--groove-soft); + border: 1px solid var(--groove); + border-radius: var(--radius-control); + box-shadow: inset 0 1px 2px rgb(28 34 38 / 15%); } -.device-card__metadata > div { - display: flex; - - min-height: 58px; - - flex-direction: column; - justify-content: center; +.setting-toggle__thumb { + position: absolute; + top: 2px; + left: 2px; - gap: 4px; + width: 18px; + height: 16px; - padding: 10px 12px; + background: var(--face-raised); + border: 1px solid var(--groove); + border-radius: 2px; + box-shadow: 0 1px 2px rgb(28 34 38 / 20%); - background: #07131f; + transition: transform 120ms ease; } -.device-card__metadata span { - color: #667f95; - - font-size: 9px; - - letter-spacing: 0.05em; - text-transform: uppercase; +.setting-toggle--enabled .setting-toggle__track { + background: var(--ink); + border-color: var(--ink); } -.device-card__metadata strong { - color: #bdd1e2; - - font-family: Consolas, "Courier New", monospace; - font-size: 11px; - font-weight: 500; +.setting-toggle--enabled .setting-toggle__thumb { + transform: translateX(20px); } -.device-card__message { - margin: 14px 0 0; - - color: #7e94a7; +.setting-toggle__value { + min-width: 28px; - font-size: 11px; + font-size: 13px; + font-weight: 600; + text-align: left; } -.device-card__error { - margin: 14px 0 0; +.setting-toggle--enabled .setting-toggle__value { + color: var(--ink); +} - color: #e9818d; +.setting-select { + min-width: 92px; + height: 34px; - font-size: 11px; -} + padding: 0 10px; -.device-card__actions { - display: flex; - justify-content: flex-end; + color: var(--ink); + background: var(--face-raised); + border: 1px solid var(--groove); + border-radius: var(--radius-control); - margin-top: auto; - padding-top: 20px; + cursor: pointer; } -.device-empty-state { - display: flex; +.settings-actions { + padding-bottom: 32px; +} - min-height: 180px; +/* ========================================================= + Notifications + ========================================================= */ - align-items: center; - justify-content: center; +.notification-center { + position: fixed; + right: 24px; + bottom: 24px; + z-index: 100; + display: flex; flex-direction: column; - gap: 6px; - - border: 1px solid #17314a; + width: 340px; - background: #07131f; + gap: 8px; } -.device-empty-state strong { - color: #d9e6f1; +.notification { + display: grid; + grid-template-columns: auto minmax(0, 1fr) auto; + align-items: start; - font-size: 13px; -} + gap: 10px; + padding: 12px 12px 12px 14px; -.device-empty-state span { - color: #70869a; + color: var(--ink); + background: var(--face-raised); + border: 1px solid var(--groove); + border-left: 3px solid var(--ink-3); + border-radius: var(--radius-control); + box-shadow: 0 6px 18px rgb(28 34 38 / 18%); +} - font-size: 11px; +.notification--success { + border-left-color: var(--led-on); } -/* ========================================================= - Device panel runtime information - ========================================================= */ +.notification--warning { + border-left-color: var(--led-busy); +} -.device-panel__header { - padding-bottom: 18px; +.notification--error { + border-left-color: var(--led-error); +} - border-bottom: 1px solid #132d44; +.notification__icon { + padding-top: 2px; + color: var(--ink-2); } -.device-panel__header h2 { - margin: 5px 0 0; +.notification--success .notification__icon { + color: var(--led-on); +} - color: #edf5fc; +.notification--warning .notification__icon { + color: #b07a0c; +} - font-size: 18px; +.notification--error .notification__icon { + color: var(--led-error); } -.device-panel__empty { +.notification__content { display: flex; - - align-items: flex-start; - flex-direction: column; - - gap: 7px; - - padding-top: 22px; } -.device-panel__empty strong { - color: #b9c9d8; - - font-size: 12px; +.notification__content strong { + font-weight: 600; } -.device-panel__empty p { - margin: 0; - - color: #6e8498; - - font-size: 10px; - line-height: 1.5; +.notification__content span { + color: var(--ink-2); + font-size: 14px; } -.device-panel__dot { - display: block; +.notification__close { + display: grid; + place-items: center; - width: 7px; - height: 7px; + width: 24px; + height: 24px; - background: #52697d; -} + color: var(--ink-3); + background: transparent; + border: 0; + border-radius: 2px; -.device-panel__dot--connected { - background: #36ca92; + cursor: pointer; } -.device-panel__dot--connecting, -.device-panel__dot--disconnecting { - background: #dfb54c; +.notification__close:hover { + color: var(--ink); + background: var(--groove-soft); } -.device-panel__dot--error { - background: #df6574; -} +/* ========================================================= + Debug panel (development only) + ========================================================= */ -.device-panel__active { - padding-top: 22px; -} +.debug-panel { + position: fixed; + left: 24px; + bottom: 24px; + z-index: 90; -.device-panel__status { display: flex; - align-items: center; - - gap: 8px; - - margin-bottom: 12px; - - color: #8198ac; + flex-direction: column; - font-family: Consolas, "Courier New", monospace; - font-size: 9px; + width: 520px; + max-height: 260px; - text-transform: uppercase; + color: #c9d6d2; + background: var(--screen); + border: 1px solid var(--screen-edge); + border-radius: var(--radius-panel); + box-shadow: 0 8px 24px rgb(0 0 0 / 30%); } -.device-panel__name { - display: block; +.debug-panel__header { + display: flex; + align-items: center; + justify-content: space-between; - color: #edf5fc; + padding: 8px 10px 8px 14px; - font-size: 13px; + border-bottom: 1px solid var(--screen-grid); } -.device-panel__model { - display: block; - - margin-top: 4px; +.debug-panel__header > div { + display: flex; + align-items: baseline; - color: #71889c; + gap: 10px; +} - font-size: 10px; +.debug-panel__header span { + color: var(--screen-label); + font-size: 13px; } -.device-panel__details { +.debug-panel__header button { display: grid; + place-items: center; - gap: 1px; - - margin-top: 18px; + width: 28px; + height: 28px; - border: 1px solid #132d44; + color: var(--screen-label); + background: transparent; + border: 0; - background: #132d44; + cursor: pointer; } -.device-panel__details > div { - display: flex; - align-items: center; - justify-content: space-between; - - min-height: 40px; +.debug-panel__header button:hover { + color: var(--trace); +} - gap: 14px; - padding: 8px 10px; +.debug-panel__entries { + overflow: auto; + padding: 6px 0; +} - background: #07131f; +.debug-panel__empty { + padding: 10px 14px; + color: var(--screen-label); } -.device-panel__details span { - color: #698096; +.debug-entry { + display: grid; + grid-template-columns: 70px 110px minmax(0, 1fr); - font-size: 9px; + gap: 8px; + padding: 3px 14px; - text-transform: uppercase; + font-size: 13px; } -.device-panel__details strong { - color: #a9c3d8; - - font-family: Consolas, "Courier New", monospace; - font-size: 9px; - font-weight: 500; +.debug-entry__time, +.debug-entry__source { + color: var(--screen-label); +} - text-transform: uppercase; +.debug-entry--warning .debug-entry__message { + color: #f2c14e; } -@media (max-width: 1000px) { - .device-grid { - grid-template-columns: 1fr; - } +.debug-entry--error .debug-entry__message { + color: #ff8a80; } /* ========================================================= - Module runtime host + Module host ========================================================= */ .module-host { display: flex; + flex-direction: column; width: 100%; height: 100%; min-height: 0; - - flex-direction: column; } .module-host__toolbar { display: flex; align-items: center; - justify-content: space-between; - - min-height: 52px; gap: 16px; padding-bottom: 12px; - - border-bottom: 1px solid #17314a; } .module-host__identity { display: flex; - align-items: center; - - gap: 10px; - - color: #8aa0b3; + align-items: baseline; - font-size: 11px; + gap: 8px; } -.module-host__identity code { - color: #55b4ff; - - font-family: Consolas, "Courier New", monospace; +.module-host__identity > span:first-child { + font-family: var(--font-display); + font-size: 19px; + font-weight: 600; +} - font-size: 10px; +.module-host__version { + color: var(--ink-3); + font-size: 13px; } +/* + * Modules lay out their own screens (dark) and control panels (light), + * using the shell tokens above. + */ .module-host__content { flex: 1; min-width: 0; min-height: 0; - padding-top: 14px; - overflow: hidden; } .module-host__fallback { - padding-top: 28px; + padding: 24px 4px; } -.module-host__fallback h1 { - margin: 7px 0 6px; +.module-host__fallback h1, +.module-error h1 { + font-family: var(--font-display); + font-size: 30px; + font-weight: 600; +} - color: #f2f7fc; +.module-host__fallback p, +.module-error p { + max-width: 62ch; + margin-top: 6px; - font-size: 26px; + color: var(--ink-2); } -.module-host__fallback p { - margin: 0; +.architecture-note { + display: flex; + flex-direction: column; - color: #8095a8; + max-width: 62ch; + margin-top: 20px; + padding: 14px 16px; - font-size: 12px; + border: 1px dashed var(--groove); + border-radius: var(--radius-panel); +} + +.architecture-note span { + color: var(--ink-2); +} + +.module-error { + display: flex; + flex-direction: column; + align-items: flex-start; + + gap: 14px; + padding: 24px 4px; +} + +.module-error code { + padding: 10px 12px; + + color: var(--trace); + background: var(--screen); + border-radius: var(--radius-control); + + font-size: 13px; +} + +/* ========================================================= + Responsive + motion + ========================================================= */ + +@media (max-width: 1150px) { + .app-shell:not(.app-shell--module-open) { + grid-template-columns: minmax(0, 1fr) 290px; + } + + .main-content { + padding: 28px 28px 40px; + } + + .scope__canvas { + height: 240px; + } +} + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + scroll-behavior: auto !important; + transition-duration: 0.01ms !important; + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + } } diff --git a/src/types/module.ts b/src/types/module.ts index 02b4342..c84a455 100644 --- a/src/types/module.ts +++ b/src/types/module.ts @@ -43,6 +43,13 @@ export interface ModuleEEGCapability { */ export interface ModuleCapabilities { eeg?: ModuleEEGCapability; + + /** + * Derived attention/meditation metrics (BrainMetricsService). + */ + brainMetrics?: { + required: boolean; + }; } /** diff --git a/vite.config.ts b/vite.config.ts index 98e03b0..730ab4e 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,10 +1,10 @@ -import { defineConfig } from 'vite'; -import react from '@vitejs/plugin-react'; +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; export default defineConfig({ plugins: [react()], clearScreen: false, server: { port: 1420, strictPort: true }, - envPrefix: ['VITE_', 'TAURI_'], - build: { target: ['es2021', 'chrome100', 'safari13'] } + envPrefix: ["VITE_", "TAURI_"], + build: { target: ["es2021", "chrome100", "safari13"] }, }); diff --git a/vitest.config.ts b/vitest.config.ts index 26609d0..11a7e4d 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -4,9 +4,7 @@ export default defineConfig({ test: { environment: "jsdom", - setupFiles: [ - "./src/test/setup.ts", - ], + setupFiles: ["./src/test/setup.ts"], globals: false, @@ -17,24 +15,13 @@ export default defineConfig({ coverage: { provider: "v8", - reporter: [ - "text", - "html", - ], + reporter: ["text", "html"], reportsDirectory: "coverage", - include: [ - "src/**/*.{ts,tsx}", - ], - - exclude: [ - "src/main.tsx", - "src/test/**", - "**/*.d.ts", - "**/*.test.ts", - "**/*.test.tsx", - ], + include: ["src/**/*.{ts,tsx}"], + + exclude: ["src/main.tsx", "src/test/**", "**/*.d.ts", "**/*.test.ts", "**/*.test.tsx"], }, }, -}); \ No newline at end of file +}); From 466bfb2ee465b826ef86b4f408e1e89e00a69335 Mon Sep 17 00:00:00 2001 From: pawel120 Date: Sat, 3 Oct 2026 13:53:02 +0200 Subject: [PATCH 2/7] ui changes --- src/App.tsx | 28 +++- src/components/DevicePanel.tsx | 101 ++++++++++++- .../brainaccess/BrainAccessEEGAdapter.ts | 1 + src/core/devices/models/device.ts | 7 + src/modules/cortex/CortexModule.tsx | 67 ++++++++- .../cortex/rendering/CortexRenderer.ts | 131 +++++++++++++++- src/modules/cortex/styles/cortex.css | 89 ++++++++++- src/modules/miner/MinerModule.tsx | 24 ++- src/modules/miner/styles/miner.css | 14 +- src/styles/app.css | 142 ++++++++++++++++-- 10 files changed, 567 insertions(+), 37 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 8fdc9b2..79232a0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,6 +7,7 @@ import { TitleBar } from "./components/TitleBar"; import { Sidebar } from "./components/Sidebar"; import { DevicePanel } from "./components/DevicePanel"; +import { PanelResizer } from "./components/PanelResizer"; import { Dashboard } from "./features/dashboard/Dashboard"; @@ -30,6 +31,7 @@ import { WelcomeScreen } from "./features/welcome/WelcomeScreen"; import { logger } from "./lib/logger"; import { notificationStore } from "./lib/notificationStore"; +import { usePanelSize } from "./lib/usePanelSize"; import type { AppRoute, ShellPage } from "./types/navigation"; @@ -49,6 +51,13 @@ export default function App() { const [showWelcome, setShowWelcome] = useState(true); + const readoutPanel = usePanelSize({ + storageKey: "kneuron.panel.readout", + defaultWidth: 340, + minWidth: 260, + maxWidth: 560, + }); + const [route, setRoute] = useState({ kind: "shell", page: "dashboard", @@ -188,21 +197,34 @@ export default function App() { const isModuleOpen = route.kind === "module"; - const shellClassName = isModuleOpen ? "app-shell app-shell--module-open" : "app-shell"; + const shellClassName = [ + "app-shell", + isModuleOpen && "app-shell--module-open", + readoutPanel.collapsed && "app-shell--readout-collapsed", + ] + .filter(Boolean) + .join(" "); if (showWelcome) { return ; } + const readoutWidth = readoutPanel.collapsed ? 0 : readoutPanel.width; + return ( -
    +
    {renderContent()}
    - handleShellNavigation("device")} /> + {!isModuleOpen && } + + handleShellNavigation("device")} + /> diff --git a/src/components/DevicePanel.tsx b/src/components/DevicePanel.tsx index 50100e5..b34c761 100644 --- a/src/components/DevicePanel.tsx +++ b/src/components/DevicePanel.tsx @@ -7,7 +7,7 @@ import { isBrainMetricsDeviceAdapter } from "../core/devices/contracts/BrainMetr import type { BrainMetricsSnapshot } from "../core/brainMetrics"; import type { DeviceConnectionState } from "../core/devices/models/device"; -import { SignalScope, type ScopeSource } from "./SignalScope"; +import { SignalScope, type ScopeSource, type ScopeStats } from "./SignalScope"; const STATE_LABEL: Record = { disconnected: "Disconnected", @@ -21,6 +21,53 @@ function formatMetric(value: number | null | undefined): string { return value === null || value === undefined ? "–" : String(value); } +/** + * Seconds without new data before the link is reported as delayed / lost. + * BrainLink-style metrics arrive about once a second, raw EEG every ~40 ms. + */ +const DATA_AGE_LIMITS: Record<"eeg" | "metrics", { delayed: number; lost: number }> = { + eeg: { delayed: 0.5, lost: 2 }, + metrics: { delayed: 3, lost: 6 }, +}; + +type LinkHealth = "waiting" | "ok" | "delayed" | "lost"; + +function getLinkHealth(source: "eeg" | "metrics", age: number | null): LinkHealth { + if (age === null) { + return "waiting"; + } + + const limits = DATA_AGE_LIMITS[source]; + + if (age >= limits.lost) { + return "lost"; + } + + return age >= limits.delayed ? "delayed" : "ok"; +} + +const LINK_LED: Record = { + waiting: "led led--connecting", + ok: "led led--on", + delayed: "led led--connecting", + lost: "led led--error", +}; + +const LINK_LABEL: Record = { + waiting: "Waiting for data", + ok: "Receiving", + delayed: "Data delayed", + lost: "No data", +}; + +function formatAge(age: number | null): string { + if (age === null) { + return "–"; + } + + return age < 10 ? `${age.toFixed(2)} s` : `${Math.round(age)} s`; +} + /** * Right-hand readout: live view of whatever the active device produces. */ @@ -35,6 +82,8 @@ export function DevicePanel({ const [metrics, setMetrics] = useState | null>(null); + const [stats, setStats] = useState(null); + const activeDevice = snapshot.activeDeviceInfo; const state = snapshot.activeStatus?.state ?? "disconnected"; const connected = activeDevice !== null && state === "connected"; @@ -73,6 +122,7 @@ export function DevicePanel({ sessionKey={sessionKey} paused={paused} onMetrics={setMetrics} + onStats={setStats} />

    {source === "eeg" && "Last 4 seconds, each channel scaled to fit"} @@ -118,6 +168,14 @@ export function DevicePanel({ )} + {source !== "none" && ( + + )} + {snapshot.activeStatus?.error && (

    {snapshot.activeStatus.error}

    )} @@ -130,3 +188,44 @@ export function DevicePanel({ ); } + +function LinkHealthPanel({ + source, + stats, + batteryPercent, +}: { + source: "eeg" | "metrics"; + stats: ScopeStats | null; + batteryPercent: number | null | undefined; +}) { + const age = stats?.dataAgeSeconds ?? null; + const health = getLinkHealth(source, age); + + return ( +
    +
    +
    +
    +
    last data {formatAge(age)} ago
    +
    + + {source === "eeg" && ( +
    +
    Dropped samples
    +
    0 ? "is-warning" : undefined}> + {stats?.droppedSamples ?? 0} +
    +
    + )} + + {batteryPercent !== undefined && batteryPercent !== null && ( +
    +
    Battery at connect
    +
    {batteryPercent}%
    +
    + )} +
    + ); +} diff --git a/src/core/devices/adapters/brainaccess/BrainAccessEEGAdapter.ts b/src/core/devices/adapters/brainaccess/BrainAccessEEGAdapter.ts index a013631..097b7a7 100644 --- a/src/core/devices/adapters/brainaccess/BrainAccessEEGAdapter.ts +++ b/src/core/devices/adapters/brainaccess/BrainAccessEEGAdapter.ts @@ -226,6 +226,7 @@ export class BrainAccessEEGAdapter implements EEGDeviceAdapter { `${connection.sampleRateHz} Hz · ` + `${connection.channelCount} EEG channels. ` + mappingMessage, + batteryPercent: connection.battery ?? null, updatedAt: Date.now(), }); } catch (error) { diff --git a/src/core/devices/models/device.ts b/src/core/devices/models/device.ts index 64a3823..0c1a26e 100644 --- a/src/core/devices/models/device.ts +++ b/src/core/devices/models/device.ts @@ -92,6 +92,13 @@ export interface DeviceStatus { */ error?: string; + /** + * Battery level in percent, when the device reports it. + * + * BrainAccess reports it once, when the connection is established. + */ + batteryPercent?: number | null; + /** * Unix timestamp in milliseconds indicating when this snapshot was created * or last changed. diff --git a/src/modules/cortex/CortexModule.tsx b/src/modules/cortex/CortexModule.tsx index 8aaf653..09edb28 100644 --- a/src/modules/cortex/CortexModule.tsx +++ b/src/modules/cortex/CortexModule.tsx @@ -10,7 +10,9 @@ import { CORTEX_WARMUP_SECONDS, } from "./eeg/CortexSignalProcessor"; import { useCortexEEG } from "./hooks/useCortexEEG"; -import { CortexRenderer } from "./rendering/CortexRenderer"; +import { CortexRenderer, type CortexView } from "./rendering/CortexRenderer"; +import { PanelResizer } from "../../components/PanelResizer"; +import { usePanelSize } from "../../lib/usePanelSize"; import { cloneDefaultCortexElectrodes } from "./models/defaultElectrodeLayout"; import type { CortexBand, @@ -81,6 +83,22 @@ function loadSavedElectrodes(): CortexElectrode[] { } } +const CAMERA_VIEWS: ReadonlyArray<{ view: CortexView; label: string }> = [ + { view: "top", label: "Top" }, + { view: "front", label: "Front" }, + { view: "back", label: "Back" }, + { view: "left", label: "Left" }, + { view: "right", label: "Right" }, +]; + +const BAND_NAMES: Record = { + delta: "Delta", + theta: "Theta", + alpha: "Alpha", + beta: "Beta", + gamma: "Gamma", +}; + function formatEEGState(state: string): string { return state.charAt(0).toUpperCase() + state.slice(1); } @@ -188,6 +206,13 @@ export function CortexModule(_props: KNeuronModuleProps) { const [deformation, setDeformation] = useState(0.2); const [meshLinesVisible, setMeshLinesVisible] = useState(false); const [editMode, setEditMode] = useState(false); + + const controlsPanel = usePanelSize({ + storageKey: "kneuron.panel.cortex-controls", + defaultWidth: 320, + minWidth: 260, + maxWidth: 520, + }); const [labelsVisible, setLabelsVisible] = useState(true); const [electrodes, setElectrodes] = useState(loadSavedElectrodes); const [selectedIndex, setSelectedIndex] = useState(-1); @@ -278,7 +303,16 @@ export function CortexModule(_props: KNeuronModuleProps) { } return ( -
    +
    {processingStatus}}
    +
    + {CAMERA_VIEWS.map(({ view, label }) => ( + + ))} +
    + + {eeg.started && ( +
    + + {visualMode === "fast" ? "Activity" : `${BAND_NAMES[selectedBand]} power`} vs. + baseline + +
    + )} + {calibration.visible && (
    @@ -338,6 +399,8 @@ export function CortexModule(_props: KNeuronModuleProps) { )}
    + +