diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 46442ed..9afcc05 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,6 +62,7 @@ jobs: brainaccess-sidecar/requirements.txt brainlink-sidecar/requirements.txt ssvep-sidecar/requirements.txt + neurostate-sidecar/neurostate.txt - name: Upgrade pip run: python -m pip install --upgrade pip @@ -77,6 +78,7 @@ jobs: python -m pip install "scipy>=1.14,<2" python -m pip install "scikit-learn>=1.5,<2" python -m pip install "pyserial>=3.5,<4" + python -m pip install -r neurostate-sidecar/neurostate.txt - name: Test BrainAccess sidecar working-directory: brainaccess-sidecar @@ -89,3 +91,9 @@ jobs: - name: Test SSVEP sidecar working-directory: ssvep-sidecar run: python -m unittest discover -s tests -p "test_*.py" -v + + # Runs neurostate on a mock LSL stream, so it also checks the pinned + # neurostate version against the sidecar. + - name: Test neurostate sidecar + working-directory: neurostate-sidecar + run: python -m unittest discover -s tests -p "test_*.py" -v diff --git a/.gitignore b/.gitignore index afb2976..12f47ac 100644 --- a/.gitignore +++ b/.gitignore @@ -52,6 +52,9 @@ brainlink-sidecar/dist/ ssvep-sidecar/build/ ssvep-sidecar/dist/ +neurostate-sidecar/build/ +neurostate-sidecar/dist/ + # --------------------------------------------------------- # IDE / editor files diff --git a/.prettierignore b/.prettierignore index 8a58ebe..e11b697 100644 --- a/.prettierignore +++ b/.prettierignore @@ -3,6 +3,8 @@ dist/ src-tauri/target/ src-tauri/binaries/ +# Regenerated by every Tauri build. +src-tauri/gen/ public/neuorrun/ @@ -15,5 +17,8 @@ brainlink-sidecar/dist/ ssvep-sidecar/build/ ssvep-sidecar/dist/ +neurostate-sidecar/build/ +neurostate-sidecar/dist/ + **/.venv/ **/venv/ \ No newline at end of file diff --git a/README.md b/README.md index 3cad7fa..5243dc6 100644 --- a/README.md +++ b/README.md @@ -11,11 +11,13 @@ The application combines multiple brain-computer interface workflows in one desk - visualization, - device-specific integrations. -The current application contains three production modules: +The current application contains these production modules: - **Cortex 3D** — live EEG visualization on an interactive 3D brain, - **TaaLON Miner** — SSVEP control using FBCCA, -- **Neuorrun** — attention-based interaction using BrainLink Lite / ThinkGear metrics. +- **Neuorrun** — attention-based interaction using BrainLink Lite / ThinkGear metrics (Unity WebGL), +- **Neurosniper** and **Neurorun** — the native Unity games, launched from KNeuron and controlled by attention and relaxation that [neurostate](https://github.com/KN-Neuron/NeuroState) computes from a BrainAccess cap, +- **Signal Check** — neurostate's `check` inside KNeuron: live EEG and per-channel status, for fitting the cap. --- @@ -43,6 +45,7 @@ The current application contains three production modules: ### Supported devices - **BrainAccess MAXI 009** +- **BrainAccess MIDI** (any `BA MIDI …` headset) - **BrainLink Lite BL002 V2.0** --- @@ -70,6 +73,30 @@ KNeuron is designed so that application modules do not communicate directly with FBCCA Neuorrun ``` +BrainAccess caps also feed the native games, through neurostate: + +```text +BrainAccess MAXI / MIDI + ↓ +brainaccess-bridge ──→ raw EEG → EEGStreamService → Cortex 3D, TaaLON Miner + ↓ +LSL stream "KNeuron " + ↓ +neurostate-bridge (neurostate) + ├──→ JSON over TCP 127.0.0.1:13855 → Neurosniper, Neurorun (native) + ├──→ ThinkGear TCP 13854, WebSocket 13856, LSL "NeuroState" + └──→ NeuroStateService → BrainMetricsService → game modules, Neuorrun +``` + +BrainLink has no raw EEG for neurostate, so the native games get the attention and meditation the headset computes itself, on the same outputs: + +```text +BrainLink Lite → brainlink-bridge → BrainLinkAdapter + ├──→ BrainMetricsService → Neuorrun (WebGL) + └──→ NeuroStateService → neurostate-bridge (relay) + └──→ JSON over TCP 127.0.0.1:13855 → Neurosniper, Neurorun (native) +``` + The key architectural rule is: > Modules consume normalized application-level data and do not depend on hardware-native channel indexes, Bluetooth implementation details, COM ports, or vendor SDK internals. @@ -128,6 +155,21 @@ Fp2 = BIAS Therefore these two positions are not exposed as EEG measurement channels. +## BrainAccess MIDI + +The 16-channel BrainAccess MIDI uses the electrode order of BrainAccess Board's MIDI template: + +```text +0 P8 1 O2 2 P4 3 C4 4 F8 5 F4 6 Oz 7 Cz +8 Fz 9 Pz 10 F3 11 O1 12 P7 13 C3 14 P3 15 F7 +``` + +As in Board's template, the reference is Fp1 (not streamed) and the bias feedback is driven from pin 0 (P8). KNeuron connects to the first headset whose Bluetooth name starts with `BA MIDI`. + +Each BrainAccess model is a profile in `src/core/devices/adapters/brainaccess/brainAccessProfiles.ts` (labels, bias electrodes, neurostate profile); the adapter and the bridge are shared. + +## BrainAccess communication + BrainAccess communication is handled by a Python sidecar: ```text @@ -177,6 +219,15 @@ Neuorrun The UI presents signal quality as a percentage derived from `poorSignalLevel`. +On the Device page, BrainLink Lite has an optional **Bluetooth MAC address** field: + +- With an address, the bridge opens a direct RFCOMM socket to the headset. No `/dev/rfcomm0` or `rfcomm bind` is needed. This is the normal way to connect on Linux. +- Left blank, the bridge probes serial ports instead (Windows COM ports, or an existing `/dev/rfcomm*` on Linux). + +The headset must be paired once through the system's Bluetooth settings. Find its address on Linux with `bluetoothctl devices`. The last address used is remembered. + +The bridge uses RFCOMM channel 1. Set `KNEURON_BRAINLINK_RFCOMM_CHANNEL` before starting KNeuron if a headset uses a different channel. + --- # Modules @@ -310,6 +361,73 @@ original game Controller No FBCCA is used in Neuorrun. +With a BrainAccess cap connected instead of BrainLink, Neuorrun gets attention from neurostate through the same `BrainMetricsService`. + +--- + +## Signal Check + +Signal Check is `neurostate check` inside KNeuron, for fitting the cap before you play. Connect your BrainAccess cap, open **Signal Check** on the Dashboard, and adjust electrodes until every channel is fine. + +It shows: + +- the last 5 seconds of every channel, filtered (1-45 Hz, mains notches), one row per channel; the scale buttons set the µV between rows, +- each channel's status, coloured as in neurostate's window: OK (light grey), FLAT (grey, no signal from the electrode), NOISY (red, poor contact or movement) and MAINS (orange, mains hum), +- how many channels are good in the frontal, parietal and occipital regions, which neurostate uses for attention and relaxation, +- the stream's health: received sampling rate, and gaps (samples lost over Bluetooth), +- the spectrum over O1/Oz/O2, with the alpha band (8-13 Hz) shaded: alpha should rise clearly when you close your eyes. + +The judgement is neurostate's own: the sidecar runs neurostate's filter and channel assessment with the cap's profile, so the thresholds match what neurostate applies while you play (for the MIDI, mains hum may reach 10× the EEG power before a channel counts as bad). The check runs in its own sidecar process, so it can stay open while a game runs. + +--- + +## Neurosniper and Neurorun (native games) + +The native Unity builds of [Neurosniper](https://github.com/KN-Neuron/Neurosniper) and [Neurorun](https://github.com/KN-Neuron/Neurorun) run in their own windows. They read attention and relaxation from neurostate's JSON output on `127.0.0.1:13855` themselves; KNeuron: + +1. installs each game's latest GitHub release (`src-tauri/src/releases.rs`), +2. runs neurostate on the connected BrainAccess cap, or relays BrainLink's own metrics (`NeuroStateService`), +3. starts and stops the game process (`src-tauri/src/games.rs`), +4. shows the values the game receives. + +To play: + +```text +1. Device → connect your BrainAccess cap or BrainLink Lite. +2. Dashboard → Neurosniper or Neurorun → PLAY. +3. BrainAccess only: for the first minute of good signal, sit still with your eyes + open: neurostate measures your resting baseline (BASELINE). After that the game + uses your values. BrainLink's values are usable as soon as the signal is good. +``` + +### Releases + +PLAY first asks GitHub for the game's latest release. When it is newer than the installed one, KNeuron downloads this platform's build: + +```text +Linux: -linux-x86_64-.tar.gz +Windows: -windows-x64-.zip +``` + +It checks the download against the SHA-256 digest GitHub publishes for the asset, unpacks it, and starts it. Builds live in KNeuron's data folder, and only the newest version is kept: + +```text +Linux: ~/.local/share/pl.kneuron.desktop/games/// +Windows: %APPDATA%\pl.kneuron.desktop\games\\\ +``` + +To publish a new version of a game, create a GitHub release with assets named as above; KNeuron picks it up the next time someone presses PLAY. Without internet access, or when GitHub's rate limit (60 requests an hour per network) is reached, PLAY starts the installed version. + +**LOCAL BUILD...** plays a build from your computer instead, for example one that isn't released yet. **USE RELEASES** switches back. + +### Running games + +The game keeps running if you leave its module, and closes with KNeuron. QUIT GAME asks the game to quit and kills it after 5 seconds. + +neurostate keeps running while the cap stays connected, so the baseline is measured once per connection, not per game. NEW BASELINE restarts it. The games are playable without a cap; they then behave as if there were no signal. + +For safety, the shell only downloads from the games' own GitHub releases, and only launches the known games, from an executable named like the game. + --- # Repository structure @@ -319,6 +437,7 @@ KNeuron---Interface/ │ ├── brainaccess-sidecar/ ├── brainlink-sidecar/ +├── neurostate-sidecar/ ├── ssvep-sidecar/ │ ├── unity-patch/ @@ -335,12 +454,17 @@ KNeuron---Interface/ │ │ ├── devices/ │ │ ├── eeg/ │ │ ├── brainMetrics/ +│ │ ├── games/ +│ │ ├── neurostate/ │ │ └── ssvep/ │ ├── features/ │ ├── modules/ │ │ ├── cortex/ +│ │ ├── externalGame/ │ │ ├── miner/ -│ │ └── neuorrun/ +│ │ ├── neuorrun/ +│ │ ├── neurorun/ +│ │ └── neurosniper/ │ └── styles/ │ ├── src-tauri/ @@ -429,9 +553,9 @@ The first build can take significantly longer because Node packages, Rust depend # Linux experimental setup -Linux support is currently experimental and has not been validated end-to-end with the full KNeuron hardware stack. +Linux runs KNeuron in development mode (`npm run tauri:dev`). It has been checked on Ubuntu 24.04: all four sidecars build, the BrainAccess SDK scans over Bluetooth Low Energy, and the BrainAccess → LSL → neurostate → game path works end to end. Connecting real BrainAccess hardware on Linux still needs to be confirmed. Linux packaging is not set up yet. -The included Linux bootstrap targets **Ubuntu/Debian-family distributions** and is intended primarily for development and platform testing. +The included Linux bootstrap targets **Ubuntu/Debian-family distributions**. ### 1. Clone the repository @@ -461,10 +585,10 @@ Python 3 Rust / Cargo Bluetooth utilities Python virtual environments -all three Python sidecars +all four Python sidecars ``` -The script attempts to prepare all three Python sidecars and checks device permissions used by serial/Bluetooth devices. +The script prepares all four 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. @@ -482,19 +606,21 @@ If system dependencies are already installed: ./setup-and-run.sh --skip-system ``` -### Important Linux note +### Build neurostate from a local checkout -If the script adds the current user to: +The neurostate sidecar bundles the NeuroState package, by default from the commit pinned in `neurostate-sidecar/neurostate.txt`. To build it from a local checkout instead (for example while changing neurostate): -```text -dialout +```bash +NEUROSTATE_SRC=../neurostate ./setup-and-run.sh --skip-system --no-launch ``` -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. +On Windows, set `$env:NEUROSTATE_SRC` before running the bootstrap. neurostate needs Python 3.12 or newer. -log out and log back in before using serial/RFCOMM devices. +### Important Linux notes -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. +If the script adds the current user to the `dialout` group, log out and log back in before using serial/RFCOMM devices. + +BrainAccess on Linux uses the SDK's Bluetooth Low Energy support (`brainaccess==3.6.1` ships Linux wheels). Bluetooth must be on, and BrainAccess Board must not be connected to the headset at the same time. --- @@ -753,6 +879,13 @@ brainlink-sidecar/*.spec brainlink-sidecar/requirements.txt brainlink-sidecar/build.ps1 +neurostate-sidecar/*.py +neurostate-sidecar/*.spec +neurostate-sidecar/requirements.txt +neurostate-sidecar/neurostate.txt +neurostate-sidecar/profiles/ +neurostate-sidecar/build.ps1 + public/modules/cortex/*.glb public/neuorrun/Build/ @@ -894,12 +1027,13 @@ npm run quality # Sidecars -KNeuron uses three production sidecars: +KNeuron uses four production sidecars: ```text -brainaccess-bridge -ssvep-classifier -brainlink-bridge +brainaccess-bridge BrainAccess SDK; raw EEG, republished on LSL while streaming +ssvep-classifier FBCCA classifier for TaaLON Miner +brainlink-bridge BrainLink / ThinkGear metrics +neurostate-bridge neurostate on the BrainAccess LSL stream, for the native games and Signal Check ``` Tauri's `externalBin` configuration references names without a platform target suffix: @@ -908,7 +1042,8 @@ Tauri's `externalBin` configuration references names without a platform target s "externalBin": [ "binaries/brainaccess-bridge", "binaries/ssvep-classifier", - "binaries/brainlink-bridge" + "binaries/brainlink-bridge", + "binaries/neurostate-bridge" ] ``` @@ -918,6 +1053,7 @@ On Windows, generated files use names similar to: brainaccess-bridge-x86_64-pc-windows-msvc.exe ssvep-classifier-x86_64-pc-windows-msvc.exe brainlink-bridge-x86_64-pc-windows-msvc.exe +neurostate-bridge-x86_64-pc-windows-msvc.exe ``` On a typical x86_64 Linux machine: @@ -926,6 +1062,7 @@ On a typical x86_64 Linux machine: brainaccess-bridge-x86_64-unknown-linux-gnu ssvep-classifier-x86_64-unknown-linux-gnu brainlink-bridge-x86_64-unknown-linux-gnu +neurostate-bridge-x86_64-unknown-linux-gnu ``` The bootstrap scripts create/copy these locally into: @@ -1180,7 +1317,9 @@ Replace `COM7` with the actual outgoing Bluetooth COM port. ## BrainLink serial access fails on Linux -Verify membership: +Enter the headset's Bluetooth MAC address on the Device page. A direct RFCOMM link does not need a serial device or `dialout` membership. + +If you connect through an existing `/dev/rfcomm*` device instead, verify membership: ```bash groups @@ -1203,6 +1342,40 @@ systemctl status bluetooth --- +## A native game does not react to the cap + +Open the game's module in KNeuron and check the BRAIN SIGNAL panel: + +- `NO CAP`: connect the BrainAccess cap on the Device page. +- `BASELINE`: neurostate is measuring your resting baseline; sit still with your eyes open for about a minute. +- `POOR SIGNAL` / `NO SIGNAL`: adjust the electrodes, or bring the headset closer. +- `ERROR` mentioning `json_tcp` or an address in use: another neurostate is already running (for example one started with `uv run neurostate run`). Stop it and press RETRY. + +The games read `127.0.0.1:13855`; they reconnect on their own when neurostate restarts. + +--- + +## A game won't download + +The GAME panel shows the reason. Common ones: + +- `Could not reach GitHub`: no internet access. PLAY starts the installed version if there is one. +- `GitHub's rate limit was reached`: wait up to an hour, or use LOCAL BUILD... meanwhile. +- `has no build for this platform`: the latest release lacks a `-linux-x86_64-….tar.gz` (or `-windows-x64-….zip`) asset. +- `is corrupted`: the download didn't match GitHub's SHA-256 digest; press PLAY to try again. + +--- + +## A local build is "not executable" on Linux + +Archives extracted with some tools lose the executable bit: + +```bash +chmod +x /path/to/Neurosniper/Neurosniper.x86_64 +``` + +--- + ## Neuorrun reports `UNITY BUILD REQUIRED` Verify: @@ -2817,6 +2990,8 @@ There should be one canonical production registration path for each type of exte - [x] Windows sidecar packaging - [x] Windows bootstrap script - [x] Linux bootstrap script +- [x] BrainAccess MIDI support +- [x] neurostate sidecar and native game launcher (Neurosniper, Neurorun) - [x] developer extension guide - [x] device/module/sidecar implementation guide - [x] lifecycle/cleanup rules and extension checklists diff --git a/brainaccess-sidecar/brainaccess-bridge.spec b/brainaccess-sidecar/brainaccess-bridge.spec index 67edb91..ad038b5 100644 --- a/brainaccess-sidecar/brainaccess-bridge.spec +++ b/brainaccess-sidecar/brainaccess-bridge.spec @@ -3,6 +3,10 @@ from PyInstaller.utils.hooks import collect_all datas, binaries, hiddenimports = collect_all("brainaccess") +# pylsl loads liblsl from its own lib/ folder at runtime. +for collected, target in zip(collect_all("pylsl"), (datas, binaries, hiddenimports)): + target += collected + a = Analysis( ["bridge.py"], pathex=[], diff --git a/brainaccess-sidecar/bridge.py b/brainaccess-sidecar/bridge.py index 70da33d..b4346c8 100644 --- a/brainaccess-sidecar/bridge.py +++ b/brainaccess-sidecar/bridge.py @@ -7,6 +7,10 @@ This process owns the BrainAccess SDK lifecycle. React and KNeuron modules never import the BrainAccess Python package directly. + +While streaming, the EEG can also be republished as a Lab Streaming Layer (LSL) +stream, so neurostate can turn it into attention/relaxation values for the +native games and any LSL tool can record it. """ from __future__ import annotations @@ -31,6 +35,10 @@ PROTOCOL_VERSION = 1 TARGET_BATCH_SAMPLES = 10 +# Re-anchor LSL timestamps to the local clock when the device's sample count +# drifts this far from it (clock drift, or the sample counter restarting). +LSL_MAX_DRIFT_S = 1.0 + _write_lock = threading.Lock() @@ -48,17 +56,97 @@ def log(message: str) -> None: print(message, file=sys.stderr, flush=True) +def device_model_name(manager: Any) -> str | None: + """The connected headset's model ("MIDI", "MAXI", ...), or None if the SDK can't tell.""" + try: + return str(manager.get_device_info().device_model.name) + except Exception: + return None + + +class LslPublisher: + """Republishes the EEG stream on LSL. + + Timestamps come from the device's sample numbers, so they stay regular despite + Bluetooth jitter, and samples lost over Bluetooth show up as gaps. + """ + + def __init__( + self, + name: str, + source_id: str, + labels: list[str], + sample_rate_hz: float, + device_name: str, + ) -> None: + # Imported here so the bridge, and its tests, work without liblsl. + import pylsl + + self._clock: Callable[[], float] = pylsl.local_clock + + info = pylsl.StreamInfo( + name, "EEG", len(labels), sample_rate_hz, pylsl.cf_float32, source_id + ) + desc = info.desc() + desc.append_child_value("manufacturer", "BrainAccess") + desc.append_child_value("device", device_name) + channels = desc.append_child("channels") + for label in labels: + channel = channels.append_child("channel") + channel.append_child_value("label", label) + channel.append_child_value("unit", "microvolts") + channel.append_child_value("type", "EEG") + + self._outlet = pylsl.StreamOutlet(info) + self._period = 1.0 / sample_rate_hz + self._channel_count = len(labels) + + # (sample number, LSL time) of a reference sample. + self._anchor: tuple[float, float] | None = None + self._last_number: float | None = None + + def push(self, eeg: np.ndarray, sample_numbers: np.ndarray) -> None: + """Push channels x samples of EEG, with the device's sample number for each sample.""" + if eeg.shape[0] != self._channel_count: + raise RuntimeError( + f"LSL stream has {self._channel_count} channels, got {eeg.shape[0]}." + ) + + samples = np.ascontiguousarray(eeg.T, dtype=np.float32) + now = self._clock() + + if sample_numbers.size != eeg.shape[1] or not np.all(np.isfinite(sample_numbers)): + # No usable sample numbers: LSL derives the timestamps from the push time. + self._outlet.push_chunk(samples) + return + + last = float(sample_numbers[-1]) + restarted = self._last_number is not None and float(sample_numbers[0]) <= self._last_number + if self._anchor is None or restarted: + self._anchor = (last, now) + + stamps = self._anchor[1] + (sample_numbers - self._anchor[0]) * self._period + if abs(float(stamps[-1]) - now) > LSL_MAX_DRIFT_S: + self._anchor = (last, now) + stamps = now + (sample_numbers - last) * self._period + + self._last_number = last + self._outlet.push_chunk(samples, stamps.tolist()) + + class BrainAccessSession: def __init__(self) -> None: self._core_initialized = False self._manager: EEGManager | None = None self._device_name: str | None = None + self._model: str | None = None self._sample_rate_hz: float | None = None self._eeg_count = 0 self._streaming = False self._sequence_cursor = 0 + self._lsl: LslPublisher | None = None self._acquisition_lock = threading.Lock() self._pending_eeg: np.ndarray | None = None @@ -88,7 +176,11 @@ def scan(self) -> list[dict[str, str]]: for device in devices ] - def connect(self, device_name: str) -> dict[str, Any]: + def connect( + self, + device_name: str, + bias_electrodes: list[int] | None = None, + ) -> dict[str, Any]: if self._manager is not None: if self._device_name == device_name: return self.connection_info() @@ -126,12 +218,20 @@ def connect(self, device_name: str) -> dict[str, Any]: manager.set_channel_enabled(channel, True) manager.set_channel_gain(channel, GainMode.X8) - # This follows the BrainAccess SDK minimal acquisition example. - # The exact montage/bias semantics must still be verified for the physical cap. - manager.set_channel_bias( - eeg_channel.ELECTRODE_MEASUREMENT + eeg_count - 1, - True, - ) + # Electrodes whose signal drives the bias feedback. Without a request this + # follows the BrainAccess SDK minimal acquisition example (last electrode); + # the adapter passes the cap's own choice, e.g. pin 0 like Board's MIDI template. + for electrode in bias_electrodes or [eeg_count - 1]: + if not 0 <= electrode < eeg_count: + raise RuntimeError( + f"Bias electrode {electrode} is outside the device's " + f"{eeg_count} electrodes." + ) + + manager.set_channel_bias( + eeg_channel.ELECTRODE_MEASUREMENT + electrode, + True, + ) # Keep the native sample number for packet-loss accounting. manager.set_channel_enabled(eeg_channel.SAMPLE_NUMBER, True) @@ -145,6 +245,7 @@ def connect(self, device_name: str) -> dict[str, Any]: self._manager = manager self._device_name = device_name + self._model = device_model_name(manager) self._sample_rate_hz = sample_rate_hz self._eeg_count = eeg_count self._reset_pending() @@ -179,39 +280,61 @@ def connection_info(self) -> dict[str, Any]: return { "deviceName": self._device_name, + "model": self._model, "sampleRateHz": self._sample_rate_hz, "channelCount": self._eeg_count, } - def start_stream(self) -> dict[str, Any]: + def start_stream(self, lsl: dict[str, Any] | None = None) -> dict[str, Any]: + """Start acquisition, optionally republishing it on LSL. + + ``lsl`` holds the stream's ``name``, ``sourceId`` and one ``channelLabels`` + entry per EEG electrode. + """ manager = self._require_manager() if self._streaming: - return {"streaming": True} + return {"streaming": True, "lsl": self._lsl is not None, "lslError": None} + + # Republishing is best-effort: modules reading the bridge's own sample events + # keep working even when LSL can't start. + lsl_error: str | None = None + if lsl is not None: + try: + self._lsl = self._create_lsl_publisher(lsl) + except Exception as exc: + lsl_error = str(exc) + log(f"LSL republishing unavailable: {exc}") self._reset_pending() self._sequence_cursor = 0 - manager.set_callback_chunk(self._on_chunk) - manager.load_config() - manager.start_stream() + try: + manager.set_callback_chunk(self._on_chunk) + manager.load_config() + manager.start_stream() + except Exception: + self._lsl = None + raise self._streaming = True log("BrainAccess EEG stream started.") - return {"streaming": True} + return {"streaming": True, "lsl": self._lsl is not None, "lslError": lsl_error} def stop_stream(self) -> dict[str, Any]: manager = self._manager if manager is None or not self._streaming: self._streaming = False + self._lsl = None self._reset_pending() return {"streaming": False} manager.stop_stream() self._streaming = False + self._lsl = None self._reset_pending() log("BrainAccess EEG stream stopped.") @@ -234,6 +357,8 @@ def disconnect(self) -> dict[str, Any]: finally: self._manager = None self._device_name = None + self._model = None + self._lsl = None self._sample_rate_hz = None self._eeg_count = 0 self._streaming = False @@ -258,6 +383,28 @@ def _require_manager(self) -> EEGManager: return self._manager + def _create_lsl_publisher(self, options: dict[str, Any]) -> LslPublisher: + name = str(options.get("name", "")).strip() + source_id = str(options.get("sourceId", "")).strip() or name + labels = [str(label) for label in options.get("channelLabels") or []] + + if not name: + raise RuntimeError("start_stream lsl options require a name.") + + if len(labels) != self._eeg_count: + raise RuntimeError( + f"start_stream lsl options list {len(labels)} channel labels, " + f"but the device has {self._eeg_count} EEG electrodes." + ) + + assert self._sample_rate_hz is not None and self._device_name is not None + + publisher = LslPublisher( + name, source_id, labels, self._sample_rate_hz, self._device_name + ) + log(f'Publishing EEG on LSL as "{name}".') + return publisher + def _reset_pending(self) -> None: with self._acquisition_lock: self._pending_eeg = None @@ -306,6 +453,7 @@ def _on_chunk(self, chunk: list[Any], chunk_size: int) -> None: sample_numbers = arr[0, :take].copy() eeg = arr[1 : self._eeg_count + 1, :take].copy() + self._publish_lsl(eeg, sample_numbers) self._append_samples(eeg, sample_numbers) except Exception as exc: @@ -316,6 +464,24 @@ def _on_chunk(self, chunk: list[Any], chunk_size: int) -> None: } ) + def _publish_lsl(self, eeg: np.ndarray, sample_numbers: np.ndarray) -> None: + publisher = self._lsl + + if publisher is None: + return + + try: + publisher.push(eeg, sample_numbers) + except Exception as exc: + # Stop republishing rather than report the same failure for every chunk. + self._lsl = None + emit( + { + "type": "bridge-error", + "message": f"LSL republishing stopped: {exc}", + } + ) + def _append_samples( self, eeg: np.ndarray, @@ -423,13 +589,16 @@ def handle_request(request: dict[str, Any]) -> bool: if not device_name: raise RuntimeError("connect requires payload.deviceName.") - response_ok(request_id, SESSION.connect(device_name)) + bias = payload.get("biasElectrodes") + bias_electrodes = [int(electrode) for electrode in bias] if bias else None + + response_ok(request_id, SESSION.connect(device_name, bias_electrodes)) elif command == "stream_info": response_ok(request_id, SESSION.connection_info()) elif command == "start_stream": - response_ok(request_id, SESSION.start_stream()) + response_ok(request_id, SESSION.start_stream(payload.get("lsl"))) elif command == "stop_stream": response_ok(request_id, SESSION.stop_stream()) diff --git a/brainaccess-sidecar/requirements.txt b/brainaccess-sidecar/requirements.txt index 81a7708..9049de7 100644 --- a/brainaccess-sidecar/requirements.txt +++ b/brainaccess-sidecar/requirements.txt @@ -1,3 +1,4 @@ brainaccess==3.6.1 numpy>=2.0,<3 +pylsl>=1.17,<2 pyinstaller>=6,<7 diff --git a/brainaccess-sidecar/tests/test_bridge.py b/brainaccess-sidecar/tests/test_bridge.py index 68bd838..6fb40f1 100644 --- a/brainaccess-sidecar/tests/test_bridge.py +++ b/brainaccess-sidecar/tests/test_bridge.py @@ -90,6 +90,66 @@ def destroy(self) -> None: self.destroy_calls += 1 +class FakeXml: + def __init__(self) -> None: + self.values: dict[str, str] = {} + self.children: list[tuple[str, "FakeXml"]] = [] + + def append_child_value(self, name: str, value: str) -> "FakeXml": + self.values[name] = value + return self + + def append_child(self, name: str) -> "FakeXml": + child = FakeXml() + self.children.append((name, child)) + return child + + +class FakeStreamInfo: + def __init__(self, name, type_, channel_count, srate, fmt, source_id) -> None: + self.args = (name, type_, channel_count, srate, fmt, source_id) + self._desc = FakeXml() + + def desc(self) -> FakeXml: + return self._desc + + +class FakeStreamOutlet: + instances: list["FakeStreamOutlet"] = [] + + def __init__(self, info: FakeStreamInfo) -> None: + self.info = info + self.pushed: list[tuple[np.ndarray, object]] = [] + type(self).instances.append(self) + + def push_chunk(self, samples, timestamp=0.0) -> None: + self.pushed.append((np.asarray(samples), timestamp)) + + +def make_fake_pylsl(clock: list[float]) -> types.ModuleType: + module = types.ModuleType("pylsl") + module.StreamInfo = FakeStreamInfo + module.StreamOutlet = FakeStreamOutlet + module.cf_float32 = 1 + module.local_clock = lambda: clock[0] + return module + + +def make_chunk(first_number: int, n: int, channels: int = 4) -> np.ndarray: + chunk = np.zeros((channels + 2, n), dtype=float) + chunk[0] = np.arange(first_number, first_number + n) + chunk[1 : channels + 1] = np.arange(channels * n, dtype=float).reshape(channels, n) + chunk[channels + 1] = 1 + return chunk + + +LSL_OPTIONS = { + "name": "KNeuron BA MIDI 026", + "sourceId": "kneuron-BA MIDI 026", + "channelLabels": ["P8", "O2", "P4", "C4"], +} + + class BrainAccessBridgeTests(unittest.TestCase): MODULE_NAMES = ( "brainaccess", @@ -191,6 +251,7 @@ def test_connect_configures_channels_and_returns_connection_info(self) -> None: info, { "deviceName": "BA MAXI 009", + "model": None, "sampleRateHz": 250.0, "channelCount": 4, "battery": 87, @@ -209,6 +270,130 @@ def test_connect_configures_channels_and_returns_connection_info(self) -> None: self.assertEqual(second["deviceName"], "BA MAXI 009") self.assertEqual(len(FakeEEGManager.instances), 1) + def test_connect_uses_requested_bias_electrodes(self) -> None: + session = self.bridge.BrainAccessSession() + + session.connect("BA MIDI 026", [0]) + + self.assertEqual(FakeEEGManager.instances[-1].biases, [(100, True)]) + + def test_connect_rejects_bias_electrode_outside_the_device(self) -> None: + session = self.bridge.BrainAccessSession() + + with self.assertRaisesRegex(RuntimeError, "Bias electrode 7"): + session.connect("BA MIDI 026", [7]) + + self.assertEqual(FakeEEGManager.instances[-1].destroy_calls, 1) + + def test_connect_reports_the_device_model(self) -> None: + class MidiManager(FakeEEGManager): + def get_device_info(self): + model = types.SimpleNamespace(name="MIDI") + return types.SimpleNamespace(device_model=model) + + session = self.bridge.BrainAccessSession() + with patch.object(self.bridge, "EEGManager", MidiManager): + info = session.connect("BA MIDI 026") + + self.assertEqual(info["model"], "MIDI") + + def test_handle_request_passes_bias_and_lsl_options(self) -> None: + session = Mock() + session.connect.return_value = {} + session.start_stream.return_value = {} + + with ( + patch.object(self.bridge, "SESSION", session), + patch.object(self.bridge, "emit"), + ): + self.bridge.handle_request( + { + "id": "1", + "command": "connect", + "payload": {"deviceName": "BA MIDI 026", "biasElectrodes": [0]}, + } + ) + self.bridge.handle_request( + {"id": "2", "command": "start_stream", "payload": {"lsl": LSL_OPTIONS}} + ) + + session.connect.assert_called_once_with("BA MIDI 026", [0]) + session.start_stream.assert_called_once_with(LSL_OPTIONS) + + def test_stream_republishes_on_lsl_with_regular_timestamps(self) -> None: + clock = [1000.0] + FakeStreamOutlet.instances.clear() + session = self.bridge.BrainAccessSession() + session.connect("BA MIDI 026") + + with patch.dict(sys.modules, {"pylsl": make_fake_pylsl(clock)}): + started = session.start_stream(LSL_OPTIONS) + + self.assertEqual(started, {"streaming": True, "lsl": True, "lslError": None}) + outlet = FakeStreamOutlet.instances[-1] + name, type_, count, srate, _, source_id = outlet.info.args + self.assertEqual((name, type_, count, srate), ("KNeuron BA MIDI 026", "EEG", 4, 250.0)) + self.assertEqual(source_id, "kneuron-BA MIDI 026") + channels = outlet.info.desc().children[0][1].children + self.assertEqual( + [channel.values for _, channel in channels][0], + {"label": "P8", "unit": "microvolts", "type": "EEG"}, + ) + + with patch.object(self.bridge, "emit"): + session._on_chunk(make_chunk(0, 5), 5) + # Bluetooth jitter: the next chunk arrives late, but its samples follow on. + clock[0] = 1000.5 + session._on_chunk(make_chunk(5, 5), 5) + + (first, first_stamps), (second, second_stamps) = outlet.pushed + self.assertEqual(first.shape, (5, 4)) + self.assertEqual(first.dtype, np.float32) + np.testing.assert_allclose(first_stamps, 1000.0 + (np.arange(5) - 4) / 250.0) + np.testing.assert_allclose(second_stamps, 1000.0 + (np.arange(5, 10) - 4) / 250.0) + + # Stopping ends the republishing; a stream started without options has none. + session.stop_stream() + self.assertFalse(session.start_stream()["lsl"]) + + def test_lsl_timestamps_reanchor_after_large_drift(self) -> None: + clock = [10.0] + FakeStreamOutlet.instances.clear() + session = self.bridge.BrainAccessSession() + session.connect("BA MIDI 026") + + with patch.dict(sys.modules, {"pylsl": make_fake_pylsl(clock)}): + session.start_stream(LSL_OPTIONS) + + with patch.object(self.bridge, "emit"): + session._on_chunk(make_chunk(0, 5), 5) + clock[0] = 20.0 + session._on_chunk(make_chunk(5, 5), 5) + + _, stamps = FakeStreamOutlet.instances[-1].pushed[-1] + self.assertAlmostEqual(stamps[-1], 20.0) + + def test_lsl_failure_does_not_stop_the_eeg_stream(self) -> None: + session = self.bridge.BrainAccessSession() + session.connect("BA MIDI 026") + + with patch.dict(sys.modules, {"pylsl": None}): + started = session.start_stream(LSL_OPTIONS) + + self.assertTrue(started["streaming"]) + self.assertFalse(started["lsl"]) + self.assertIn("pylsl", started["lslError"]) + + def test_lsl_rejects_mismatched_channel_labels(self) -> None: + session = self.bridge.BrainAccessSession() + session.connect("BA MIDI 026") + + with patch.dict(sys.modules, {"pylsl": make_fake_pylsl([0.0])}): + started = session.start_stream({**LSL_OPTIONS, "channelLabels": ["O1"]}) + + self.assertFalse(started["lsl"]) + self.assertIn("1 channel labels", started["lslError"]) + def test_connect_failure_destroys_temporary_manager(self) -> None: FakeEEGManager.connect_status = 5 session = self.bridge.BrainAccessSession() @@ -234,8 +419,9 @@ def test_stream_start_stop_disconnect_lifecycle(self) -> None: session.connect("BA MAXI 009") manager = FakeEEGManager.instances[-1] - self.assertEqual(session.start_stream(), {"streaming": True}) - self.assertEqual(session.start_stream(), {"streaming": True}) + started = {"streaming": True, "lsl": False, "lslError": None} + self.assertEqual(session.start_stream(), started) + self.assertEqual(session.start_stream(), started) self.assertEqual(manager.start_stream_calls, 1) self.assertEqual(manager.load_config_calls, 1) self.assertTrue(callable(manager.chunk_callback)) diff --git a/brainlink-sidecar/bridge.py b/brainlink-sidecar/bridge.py index 72613dc..13fbfbb 100644 --- a/brainlink-sidecar/bridge.py +++ b/brainlink-sidecar/bridge.py @@ -2,6 +2,8 @@ import json import os +import re +import socket import sys import threading import time @@ -18,6 +20,10 @@ BAUD_RATE = 57600 PROBE_SECONDS = 2.5 READ_TIMEOUT_SECONDS = 0.20 +RFCOMM_CONNECT_TIMEOUT_SECONDS = 15.0 +DEFAULT_RFCOMM_CHANNEL = 1 + +_BLUETOOTH_ADDRESS = re.compile(r"[0-9A-F]{2}(?::[0-9A-F]{2}){5}") _stdout_lock = threading.Lock() @@ -48,9 +54,55 @@ def to_json(self) -> dict[str, Any]: } +def normalize_bluetooth_address(address: str) -> str: + normalized = address.strip().replace("-", ":").upper() + if not _BLUETOOTH_ADDRESS.fullmatch(normalized): + raise ValueError(f"Invalid Bluetooth MAC address: {address!r}") + return normalized + + +class RfcommTransport: + """Bluetooth RFCOMM socket exposing the subset of serial.Serial the session uses.""" + + def __init__(self, address: str, channel: int) -> None: + if not hasattr(socket, "AF_BLUETOOTH") or not hasattr(socket, "BTPROTO_RFCOMM"): + raise RuntimeError("This Python build has no Bluetooth RFCOMM socket support.") + + self.port = address + self.is_open = False + self._socket = socket.socket( + socket.AF_BLUETOOTH, socket.SOCK_STREAM, socket.BTPROTO_RFCOMM + ) + + try: + self._socket.settimeout(RFCOMM_CONNECT_TIMEOUT_SECONDS) + self._socket.connect((address, channel)) + self._socket.settimeout(READ_TIMEOUT_SECONDS) + except Exception: + self._socket.close() + raise + + self.is_open = True + + def read(self, size: int) -> bytes: + try: + chunk = self._socket.recv(size) + except TimeoutError: + return b"" + + # Unlike a serial read timeout, an empty recv means the headset hung up. + if not chunk: + raise ConnectionError("Bluetooth RFCOMM link closed by the device.") + return chunk + + def close(self) -> None: + self.is_open = False + self._socket.close() + + class BrainLinkSession: def __init__(self) -> None: - self._serial: serial.Serial | None = None + self._serial: serial.Serial | RfcommTransport | None = None self._reader_thread: threading.Thread | None = None self._stop_event = threading.Event() self._lock = threading.Lock() @@ -71,14 +123,20 @@ def scan_ports(self) -> list[PortInfo]: ) return ports - def connect(self, requested_port: str | None = None) -> dict[str, Any]: + def connect( + self, + requested_port: str | None = None, + bluetooth_address: str | None = None, + ) -> dict[str, Any]: with self._lock: if self._serial and self._serial.is_open: return self._connection_info() override_port = requested_port or os.environ.get("KNEURON_BRAINLINK_PORT") - if override_port: + if bluetooth_address: + serial_port, parser = self._open_and_verify_rfcomm(bluetooth_address) + elif override_port: serial_port, parser = self._open_and_verify_port(override_port) else: serial_port, parser = self._autodetect_port() @@ -139,11 +197,12 @@ def _connection_info(self) -> dict[str, Any]: "baudRate": BAUD_RATE, } - def _autodetect_port(self) -> tuple[serial.Serial, ThinkGearParser]: + def _autodetect_port(self) -> tuple[serial.Serial | RfcommTransport, ThinkGearParser]: ports = self.scan_ports() if not ports: raise RuntimeError( - "No serial ports are available. Pair BrainLink Lite in Windows Bluetooth first." + "No serial ports are available. Pair BrainLink Lite in your system's Bluetooth " + "settings first. On Linux, also enter its Bluetooth MAC address." ) def priority(port: PortInfo) -> tuple[int, str]: @@ -172,17 +231,42 @@ def priority(port: PortInfo) -> tuple[int, str]: details = "; ".join(failures[:8]) raise RuntimeError( "BrainLink Lite was not detected on any serial port at 57600 baud. " - "Make sure it is powered on, paired with Windows, and not connected to another device. " + "Make sure it is powered on, paired with this computer, " + "and not connected to another device. " f"Probe results: {details}" ) - def _open_and_verify_port(self, port_name: str) -> tuple[serial.Serial, ThinkGearParser]: + def _open_and_verify_port( + self, port_name: str + ) -> tuple[serial.Serial | RfcommTransport, ThinkGearParser]: serial_port = serial.Serial( port=port_name, baudrate=BAUD_RATE, timeout=READ_TIMEOUT_SECONDS, write_timeout=1.0, ) + return self._verify_stream(serial_port) + + def _open_and_verify_rfcomm( + self, address: str + ) -> tuple[serial.Serial | RfcommTransport, ThinkGearParser]: + normalized = normalize_bluetooth_address(address) + channel = int(os.environ.get("KNEURON_BRAINLINK_RFCOMM_CHANNEL", DEFAULT_RFCOMM_CHANNEL)) + + try: + transport = RfcommTransport(normalized, channel) + except OSError as error: + raise RuntimeError( + f"Could not open a Bluetooth RFCOMM link to {normalized} on channel {channel}. " + "Make sure it is powered on, paired with this computer, and not connected " + f"to another device. ({error_text(error)})" + ) from error + + return self._verify_stream(transport) + + def _verify_stream( + self, serial_port: serial.Serial | RfcommTransport + ) -> tuple[serial.Serial | RfcommTransport, ThinkGearParser]: parser = ThinkGearParser() deadline = time.monotonic() + PROBE_SECONDS @@ -356,7 +440,10 @@ def handle_request(request: dict[str, Any]) -> bool: requested_port = payload.get("port") if requested_port is not None: requested_port = str(requested_port) - respond(request_id, session.connect(requested_port)) + bluetooth_address = payload.get("bluetoothAddress") + if bluetooth_address is not None: + bluetooth_address = str(bluetooth_address) + respond(request_id, session.connect(requested_port, bluetooth_address)) return True if command == "get_metrics": diff --git a/brainlink-sidecar/tests/test_bridge.py b/brainlink-sidecar/tests/test_bridge.py index b38e2c5..7a9397a 100644 --- a/brainlink-sidecar/tests/test_bridge.py +++ b/brainlink-sidecar/tests/test_bridge.py @@ -41,6 +41,31 @@ def close(self) -> None: self.is_open = False +class FakeSocket: + def __init__(self, reads=None, connect_error=None) -> None: + self.reads = list(reads or []) + self.connect_error = connect_error + self.connected_to = None + self.closed = False + + def settimeout(self, _timeout) -> None: + pass + + def connect(self, address) -> None: + if self.connect_error: + raise self.connect_error + self.connected_to = address + + def recv(self, _size: int) -> bytes: + item = self.reads.pop(0) + if isinstance(item, BaseException): + raise item + return item + + def close(self) -> None: + self.closed = True + + class FakeThread: def __init__(self, *, target, args, name, daemon) -> None: self.target = target @@ -102,6 +127,7 @@ def tearDownClass(cls) -> None: def tearDown(self) -> None: os.environ.pop("KNEURON_BRAINLINK_PORT", None) + os.environ.pop("KNEURON_BRAINLINK_RFCOMM_CHANNEL", None) def test_scan_ports_normalizes_pyserial_metadata(self) -> None: self.list_ports_mod.comports = lambda: [ @@ -205,6 +231,124 @@ def test_connect_honors_environment_override(self) -> None: session._open_and_verify_port.assert_called_once_with("COM12") session.disconnect() + def test_normalize_bluetooth_address(self) -> None: + self.assertEqual( + self.bridge.normalize_bluetooth_address(" 42-22-22-25-ac-97 "), + "42:22:22:25:AC:97", + ) + for invalid in ("", "42:22:22:25:AC", "42:22:22:25:AC:9G", "/dev/rfcomm0"): + with self.assertRaisesRegex(ValueError, "Invalid Bluetooth MAC address"): + self.bridge.normalize_bluetooth_address(invalid) + + def test_connect_with_bluetooth_address_opens_rfcomm_socket(self) -> None: + from thinkgear import build_packet + + fake = FakeSocket([build_packet([0x04, 66])]) + session = self.bridge.BrainLinkSession() + session.scan_ports = Mock() + + with ( + patch.object(self.bridge.socket, "socket", return_value=fake), + patch.object(self.bridge.threading, "Thread", FakeThread), + ): + info = session.connect(None, "42:22:22:25:ac:97") + + self.assertEqual(fake.connected_to, ("42:22:22:25:AC:97", 1)) + self.assertEqual(info["port"], "42:22:22:25:AC:97") + session.scan_ports.assert_not_called() + + session.disconnect() + self.assertTrue(fake.closed) + + def test_bluetooth_address_takes_priority_over_port_override(self) -> None: + os.environ["KNEURON_BRAINLINK_PORT"] = "COM12" + session = self.bridge.BrainLinkSession() + session._open_and_verify_port = Mock() + session._open_and_verify_rfcomm = Mock( + return_value=(FakeSerialPort("42:22:22:25:AC:97"), self.bridge.ThinkGearParser()) + ) + + with patch.object(self.bridge.threading, "Thread", FakeThread): + session.connect(None, "42:22:22:25:AC:97") + + session._open_and_verify_port.assert_not_called() + session.disconnect() + + def test_rfcomm_channel_can_be_overridden(self) -> None: + from thinkgear import build_packet + + os.environ["KNEURON_BRAINLINK_RFCOMM_CHANNEL"] = "3" + fake = FakeSocket([build_packet([0x04, 66])]) + + with patch.object(self.bridge.socket, "socket", return_value=fake): + self.bridge.BrainLinkSession()._open_and_verify_rfcomm("42:22:22:25:AC:97") + + self.assertEqual(fake.connected_to, ("42:22:22:25:AC:97", 3)) + + def test_rfcomm_connect_failure_is_explained_and_closes_socket(self) -> None: + fake = FakeSocket(connect_error=OSError(112, "Host is down")) + + with patch.object(self.bridge.socket, "socket", return_value=fake): + with self.assertRaisesRegex(RuntimeError, "42:22:22:25:AC:97.*Host is down"): + self.bridge.BrainLinkSession()._open_and_verify_rfcomm("42:22:22:25:AC:97") + + self.assertTrue(fake.closed) + + def test_rfcomm_probe_failure_closes_socket(self) -> None: + fake = FakeSocket() + + with ( + patch.object(self.bridge.socket, "socket", return_value=fake), + patch.object(self.bridge, "PROBE_SECONDS", -1.0), + ): + with self.assertRaisesRegex(RuntimeError, "no valid ThinkGear packets"): + self.bridge.BrainLinkSession()._open_and_verify_rfcomm("42:22:22:25:AC:97") + + self.assertTrue(fake.closed) + + def test_rfcomm_read_timeout_returns_no_data(self) -> None: + fake = FakeSocket([TimeoutError("timed out"), b"\xaa"]) + + with patch.object(self.bridge.socket, "socket", return_value=fake): + transport = self.bridge.RfcommTransport("42:22:22:25:AC:97", 1) + + self.assertEqual(transport.read(512), b"") + self.assertEqual(transport.read(512), b"\xaa") + + def test_rfcomm_peer_close_emits_disconnect(self) -> None: + fake = FakeSocket([b""]) + with patch.object(self.bridge.socket, "socket", return_value=fake): + transport = self.bridge.RfcommTransport("42:22:22:25:AC:97", 1) + + session = self.bridge.BrainLinkSession() + session._serial = transport + emitted: list[dict] = [] + + with patch.object(self.bridge, "emit", side_effect=emitted.append): + session._reader_loop(self.bridge.ThinkGearParser()) + + self.assertEqual(emitted[0]["type"], "device-disconnected") + self.assertIn("closed by the device", emitted[0]["reason"]) + self.assertTrue(fake.closed) + + def test_connect_request_forwards_bluetooth_address(self) -> None: + stub = Mock() + stub.connect.return_value = {"port": "42:22:22:25:AC:97"} + + with ( + patch.object(self.bridge, "session", stub), + patch.object(self.bridge, "emit"), + ): + self.bridge.handle_request( + { + "id": "1", + "command": "connect", + "payload": {"bluetoothAddress": "42:22:22:25:AC:97"}, + } + ) + + stub.connect.assert_called_once_with(None, "42:22:22:25:AC:97") + def test_handle_packet_emits_metrics_and_signal_quality(self) -> None: session = self.bridge.BrainLinkSession() session._port = "COM7" @@ -278,7 +422,7 @@ def __init__(self) -> None: def scan_ports(self): return [] - def connect(self, requested_port=None): + def connect(self, requested_port=None, bluetooth_address=None): return {"port": requested_port or "COM7"} def latest_metrics(self): diff --git a/index.html b/index.html index b1f881b..0eb7dd7 100644 --- a/index.html +++ b/index.html @@ -3,7 +3,7 @@ - + KNeuron diff --git a/neurostate-sidecar/bridge.py b/neurostate-sidecar/bridge.py new file mode 100644 index 0000000..c8612b2 --- /dev/null +++ b/neurostate-sidecar/bridge.py @@ -0,0 +1,652 @@ +"""neurostate JSON-lines sidecar for KNeuron. + +Runs the neurostate service on the LSL EEG stream of the connected device, so the +native games (Neurosniper, Neurorun) and any other neurostate client receive +attention and relaxation on neurostate's usual outputs: JSON over TCP (13855), +WebSocket (13856), ThinkGear (13854) and LSL. The values are also reported back +to KNeuron on stdout. + +Devices that compute attention themselves and provide no raw EEG (BrainLink) can +use the same outputs: KNeuron pushes their metrics, which are relayed unchanged. + +Protocol: +- stdin: one JSON request per line +- stdout: one JSON response/event per line +- stderr: diagnostic logs only + +It can also run neurostate's signal check, the headless equivalent of +`neurostate check`: filtered signals, per-channel status (flat, noisy, mains +hum), gaps and the spectrum over the back of the head, for fitting the cap. + +Requests: ping, start {stream, profile}, start_relay, +push_metrics {attention, meditation, poorSignalLevel}, stop, +start_check {stream, profile}, stop_check, shutdown. +Events: ready, service-status {status, message, outputs}, brain-state {state}, +check-state {state, message}, check-info {...}, check-signals {values}, +check-status {...}. +""" + +from __future__ import annotations + +import asyncio +import contextlib +import json +import math +import re +import sys +import threading +import time +from collections.abc import Callable, Coroutine +from pathlib import Path +from typing import Any, TextIO + +import numpy as np +import pylsl +from scipy.signal import welch + +from neurostate.acquisition.lsl_source import LslSource, find_streams +from neurostate.config import Config, load_config +from neurostate.estimators.eeg import EegEstimator +from neurostate.montage import REGIONS, Montage, load_montage +from neurostate.outputs.thinkgear_server import poor_signal_level +from neurostate.pipeline import OutputStartError, run_service +from neurostate.preprocessing.channels import ChannelStatus, assess_channels +from neurostate.preprocessing.filters import CausalFilter +from neurostate.schema import BrainState, State + +PROTOCOL_VERSION = 1 + +# How long each LSL lookup waits, between checks for a stop request. +STREAM_LOOKUP_S = 1.0 + +# Signal check timing and spectrum, as in neurostate's own check window. +CHECK_PULL_S = 0.05 +CHECK_ASSESS_S = 0.5 +CHECK_SPECTRUM_S = 4.0 +CHECK_SPECTRUM_MAX_HZ = 60.0 + +RELAY_SOURCE = "device metrics" + +# How long a device may stay silent before its last metrics count as no signal. +RELAY_STALE_S = 3.0 + +PROFILE_NAME = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$") + +# Profiles are neurostate config files tuned for a cap, bundled next to this script. +# "standard" means neurostate's defaults (any cap with 10-20 labels). +PROFILES_DIR = Path(getattr(sys, "_MEIPASS", Path(__file__).resolve().parent)) / "profiles" + + +_write_lock = threading.Lock() + +# The real stdout. main() points sys.stdout at stderr, so a stray print from a +# library can never corrupt the protocol. +_protocol_out: TextIO | None = None + + +def emit(message: dict[str, Any]) -> None: + """Write exactly one protocol message to stdout.""" + payload = json.dumps(message, separators=(",", ":"), allow_nan=False) + out = _protocol_out or sys.stdout + + with _write_lock: + out.write(payload + "\n") + out.flush() + + +def log(message: str) -> None: + print(message, file=sys.stderr, flush=True) + + +def load_profile(profile: str) -> Config: + """The neurostate config for a cap profile.""" + if not PROFILE_NAME.match(profile): + raise RuntimeError(f'Invalid neurostate profile name "{profile}".') + + if profile == "standard": + return load_config(None) + + path = PROFILES_DIR / f"{profile}.yaml" + if not path.is_file(): + raise RuntimeError(f'Unknown neurostate profile "{profile}".') + + return load_config(path) + + +def brain_state_event(state: BrainState) -> dict[str, Any]: + return { + "type": "brain-state", + "state": { + "t": state.t, + "attention": round(state.attention, 1), + "relaxation": round(state.relaxation, 1), + "quality": round(state.quality, 1), + "state": state.state.value, + "calibrated": state.calibrated, + "poorSignalLevel": poor_signal_level(state), + }, + } + + +def status_event(status: str, message: str, outputs: list[str] | None = None) -> dict[str, Any]: + return { + "type": "service-status", + "status": status, + "message": message, + "outputs": outputs or [], + } + + +def find_stream( + stream: str, stream_type: str, stop: threading.Event +) -> pylsl.StreamInfo | None: + """Wait for the named LSL stream; None if ``stop`` is set first.""" + while not stop.is_set(): + found = find_streams(stream, stream_type, STREAM_LOOKUP_S) + if found: + return found[0] + return None + + +def finite(value: float, digits: int) -> float | None: + """A rounded number, or None where JSON can't carry it (infinity, NaN).""" + return round(value, digits) if math.isfinite(value) else None + + +class SignalCheck: + """neurostate's signal check without its window. + + It applies exactly what ``neurostate check`` does, with the cap profile's + settings: the causal display filter, the per-channel assessment (flat, noisy, + mains hum) and the spectrum over the back of the head. + """ + + def __init__(self, source: LslSource, montage: Montage, config: Config): + self._source = source + self._config = config + fs = source.sampling_rate + self._filter = CausalFilter(config.filters, fs, len(source.labels)) + self._regions = montage.region_indices(source.labels) + self._spectrum_channels = self._regions["occipital"] or list(range(len(source.labels))) + + def info_event(self) -> dict[str, Any]: + labels = self._source.labels + return { + "type": "check-info", + "stream": self._source.description.name, + "samplingRateHz": self._source.sampling_rate, + "labels": labels, + "regions": { + region: [labels[i] for i in self._regions[region]] for region in REGIONS + }, + "spectrumChannels": [labels[i] for i in self._spectrum_channels], + "alphaHz": list(self._config.bands.alpha), + "thresholds": { + "flatUv": self._config.bad_channels.flat_uv, + "noisyUv": self._config.bad_channels.noisy_uv, + "mainsRatio": self._config.bad_channels.mains_ratio, + }, + } + + def signals_event(self) -> dict[str, Any] | None: + """Newly arrived samples, filtered for display (channels x samples, µV).""" + n_new = self._source.pull() + if not n_new: + return None + samples, _ = self._source.buffer.latest(n_new) + filtered = self._filter(samples) + return { + "type": "check-signals", + "values": np.round(filtered.T, 2).tolist(), + } + + def status_event(self) -> dict[str, Any]: + """Per-channel status, regions, stream health and the spectrum.""" + source = self._source + fs = source.sampling_rate + gaps = source.gaps + idle = source.seconds_since_data() + + event: dict[str, Any] = { + "type": "check-status", + "stream": { + "nominalRateHz": fs, + "receivedRateHz": finite(gaps.effective_rate_hz or math.nan, 2), + "gaps": gaps.n_gaps, + "missingSamples": gaps.n_missing, + "secondsSinceData": finite(idle, 2) if idle is not None else None, + }, + "channels": [], + "regions": {}, + "spectrum": None, + } + + raw, _ = source.buffer.latest(round(self._config.processing.window_s * fs)) + if len(raw) < fs / 2: + return event + + reports = assess_channels( + raw, fs, self._config.filters.notch_hz, self._config.bad_channels + ) + event["channels"] = [ + { + "label": label, + "status": report.status.value, + "rmsUv": finite(report.rms_uv, 2), + "mainsRatio": finite(report.mains_ratio, 2), + } + for label, report in zip(source.labels, reports, strict=True) + ] + event["regions"] = { + region: { + "good": sum(reports[i].status is ChannelStatus.OK for i in positions), + "total": len(positions), + } + for region, positions in self._regions.items() + } + + raw, _ = source.buffer.latest(round(CHECK_SPECTRUM_S * fs)) + freqs, psd = welch( + raw[:, self._spectrum_channels], + fs=fs, + nperseg=min(len(raw), round(fs)), + detrend="linear", + axis=0, + ) + shown = (freqs >= 1) & (freqs <= min(CHECK_SPECTRUM_MAX_HZ, fs / 2)) + # A floor keeps flat channels from producing log(0) in the plot. + power = np.maximum(psd[shown].mean(axis=1), 1e-6) + event["spectrum"] = { + "hz": np.round(freqs[shown], 2).tolist(), + "psd": [float(f"{value:.4g}") for value in power], + } + return event + + +def check_state_event(state: str, message: str) -> dict[str, Any]: + return {"type": "check-state", "state": state, "message": message} + + +class CheckSession: + """Runs at most one signal check at a time, on a background thread.""" + + def __init__(self) -> None: + self._lock = threading.Lock() + self._thread: threading.Thread | None = None + self._stop = threading.Event() + self._stream: str | None = None + + @property + def running(self) -> bool: + return self._thread is not None and self._thread.is_alive() + + def start(self, stream: str, config: Config) -> dict[str, Any]: + with self._lock: + if self.running: + if self._stream == stream: + return {"started": False, "stream": stream} + raise RuntimeError( + f'The signal check is already running on "{self._stream}". Stop it first.' + ) + + self._stop.clear() + self._stream = stream + self._thread = threading.Thread( + target=self._run, args=(stream, config), name="signal-check", daemon=True + ) + self._thread.start() + + return {"started": True, "stream": stream} + + def stop(self, timeout_s: float = 10.0) -> dict[str, Any]: + with self._lock: + thread = self._thread + self._stop.set() + + if thread is not None: + thread.join(timeout_s) + if thread.is_alive(): + raise RuntimeError("The signal check did not stop in time.") + + with self._lock: + self._thread = None + self._stream = None + + return {"running": False} + + def _run(self, stream: str, config: Config) -> None: + try: + self._check(stream, config) + emit(check_state_event("stopped", "Signal check stopped.")) + except Exception as exc: + log(f"Signal check failed: {exc!r}") + emit(check_state_event("error", f"Signal check failed: {exc}")) + + def _check(self, stream: str, config: Config) -> None: + montage = load_montage(config.input.montage) + + emit(check_state_event("waiting-for-stream", f'Waiting for the EEG stream "{stream}"...')) + info = find_stream(stream, config.input.stream_type, self._stop) + if info is None: + return + + source = LslSource(info, montage) + try: + check = SignalCheck(source, montage, config) + emit(check.info_event()) + emit(check_state_event("running", f'Checking "{stream}".')) + + next_status = 0.0 + while not self._stop.wait(CHECK_PULL_S): + signals = check.signals_event() + if signals is not None: + emit(signals) + + if time.monotonic() >= next_status: + next_status = time.monotonic() + CHECK_ASSESS_S + emit(check.status_event()) + finally: + source.close() + + +def relayed_state( + t: float, + attention: float | None, + meditation: float | None, + poor_signal: float | None, +) -> BrainState: + """Metrics in ThinkGear's scales as neurostate's values, with the same signal meaning.""" + if poor_signal is None or poor_signal >= 200: + state, quality = State.NO_SIGNAL, 0.0 + elif poor_signal > 0: + state, quality = State.POOR_SIGNAL, 100 * (1 - poor_signal / 200) + else: + state, quality = State.UNCALIBRATED, 100.0 + + def score(value: float | None) -> float: + usable = state is State.UNCALIBRATED and value is not None + return max(0.0, min(100.0, value)) if usable else 0.0 + + return BrainState( + t=t, + attention=score(attention), + relaxation=score(meditation), + quality=quality, + state=state, + calibrated=False, + ) + + +class MetricsRelay: + """Holds the metrics KNeuron last pushed, as an estimator for neurostate's outputs.""" + + def __init__(self) -> None: + self._latest: tuple[float, float | None, float | None, float | None] | None = None + + def update( + self, attention: float | None, meditation: float | None, poor_signal: float | None + ) -> None: + self._latest = (pylsl.local_clock(), attention, meditation, poor_signal) + + def __call__(self, t: float) -> BrainState: + latest = self._latest + if latest is None or t - latest[0] > RELAY_STALE_S: + return relayed_state(t, None, None, None) + return relayed_state(t, *latest[1:]) + + +class NeuroStateSession: + """Runs at most one neurostate service at a time, on a background thread.""" + + def __init__(self) -> None: + self._lock = threading.Lock() + self._thread: threading.Thread | None = None + self._stop_requested = threading.Event() + self._loop: asyncio.AbstractEventLoop | None = None + self._stop_event: asyncio.Event | None = None + self._stream: str | None = None + self._relay: MetricsRelay | None = None + + @property + def running(self) -> bool: + return self._thread is not None and self._thread.is_alive() + + def start(self, stream: str, config: Config) -> dict[str, Any]: + return self._launch(stream, lambda: self._serve(stream, config)) + + def start_relay(self, config: Config) -> dict[str, Any]: + return self._launch(RELAY_SOURCE, lambda: self._serve_relay(config)) + + def push_metrics( + self, attention: float | None, meditation: float | None, poor_signal: float | None + ) -> None: + relay = self._relay + if relay is None: + raise RuntimeError("neurostate is not relaying device metrics.") + relay.update(attention, meditation, poor_signal) + + def _launch( + self, stream: str, serve: Callable[[], Coroutine[Any, Any, None]] + ) -> dict[str, Any]: + with self._lock: + if self.running: + if self._stream == stream: + return {"started": False, "stream": stream} + + raise RuntimeError( + f'neurostate is already running on "{self._stream}". Stop it first.' + ) + + self._stop_requested.clear() + self._loop = None + self._stop_event = None + self._stream = stream + self._relay = MetricsRelay() if stream == RELAY_SOURCE else None + self._thread = threading.Thread( + target=self._run, args=(serve,), name="neurostate", daemon=True + ) + self._thread.start() + + return {"started": True, "stream": stream} + + def stop(self, timeout_s: float = 10.0) -> dict[str, Any]: + with self._lock: + thread = self._thread + self._stop_requested.set() + + loop, stop_event = self._loop, self._stop_event + if thread is not None and loop is not None and stop_event is not None: + # The loop may close between the check and the call when the service + # ends on its own (an error); there is nothing left to stop then. + with contextlib.suppress(RuntimeError): + loop.call_soon_threadsafe(stop_event.set) + + if thread is not None: + thread.join(timeout_s) + + if thread.is_alive(): + raise RuntimeError("neurostate did not stop in time.") + + with self._lock: + self._thread = None + self._stream = None + self._relay = None + self._loop = None + self._stop_event = None + + return {"running": False} + + def _run(self, serve: Callable[[], Coroutine[Any, Any, None]]) -> None: + try: + asyncio.run(serve()) + emit(status_event("stopped", "neurostate stopped.")) + except OutputStartError as exc: + emit(status_event("error", str(exc))) + except Exception as exc: + log(f"neurostate failed: {exc!r}") + emit(status_event("error", f"neurostate failed: {exc}")) + + async def _serve(self, stream: str, config: Config) -> None: + self._loop = asyncio.get_running_loop() + self._stop_event = asyncio.Event() + if self._stop_requested.is_set(): + return + + montage = load_montage(config.input.montage) + + emit(status_event("waiting-for-stream", f'Waiting for the EEG stream "{stream}"...')) + info = await asyncio.to_thread( + find_stream, stream, config.input.stream_type, self._stop_requested + ) + if info is None: + return + + source = await asyncio.to_thread(LslSource, info, montage) + try: + for warning in source.warnings: + log(f"Stream warning: {warning}") + log(f"Reading {len(source.labels)} EEG channels: {' '.join(source.labels)}") + + estimator = EegEstimator(source, montage, config) + + await self._publish(config, estimator, f'Reading "{stream}".') + finally: + source.close() + + async def _serve_relay(self, config: Config) -> None: + self._loop = asyncio.get_running_loop() + self._stop_event = asyncio.Event() + if self._stop_requested.is_set(): + return + + relay = self._relay + if relay is None: + raise RuntimeError("neurostate is not relaying device metrics.") + + await self._publish(config, relay, "Relaying the device's own metrics.") + + async def _publish( + self, config: Config, estimator: Callable[[float], BrainState], message: str + ) -> None: + # Report every update to KNeuron as well as to neurostate's own outputs. + def estimate(t: float) -> BrainState: + state = estimator(t) + emit(brain_state_event(state)) + return state + + def started(outputs: list[Any]) -> None: + descriptions = [output.description for output in outputs] + for description in descriptions: + log(f"Publishing on {description}") + emit(status_event("running", message, descriptions)) + + await run_service(config.outputs, estimate, on_started=started, stop=self._stop_event) + + +SESSION = NeuroStateSession() +CHECK = CheckSession() + + +def stream_and_profile(payload: dict[str, Any], command: str) -> tuple[str, Config]: + stream = str(payload.get("stream", "")).strip() + if not stream: + raise RuntimeError(f"{command} requires payload.stream.") + + return stream, load_profile(str(payload.get("profile") or "standard")) + + +def response_ok(request_id: str, result: Any = None) -> None: + emit({"type": "response", "id": request_id, "ok": True, "result": result}) + + +def response_error(request_id: str, exc: Exception) -> None: + emit({"type": "response", "id": request_id, "ok": False, "error": str(exc)}) + + +def handle_request(request: dict[str, Any]) -> bool: + request_id = str(request.get("id", "")) + command = str(request.get("command", "")) + payload = request.get("payload") or {} + + try: + if command == "ping": + response_ok(request_id, {"protocolVersion": PROTOCOL_VERSION}) + + elif command == "start": + response_ok(request_id, SESSION.start(*stream_and_profile(payload, command))) + + elif command == "start_relay": + response_ok(request_id, SESSION.start_relay(load_profile("standard"))) + + elif command == "push_metrics": + SESSION.push_metrics( + payload.get("attention"), + payload.get("meditation"), + payload.get("poorSignalLevel"), + ) + response_ok(request_id) + + elif command == "stop": + response_ok(request_id, SESSION.stop()) + + elif command == "start_check": + response_ok(request_id, CHECK.start(*stream_and_profile(payload, command))) + + elif command == "stop_check": + response_ok(request_id, CHECK.stop()) + + elif command == "shutdown": + SESSION.stop() + CHECK.stop() + response_ok(request_id, {"shutdown": True}) + return False + + else: + raise RuntimeError(f'Unknown neurostate bridge command "{command}".') + + except Exception as exc: + response_error(request_id, exc) + + return True + + +def main() -> None: + global _protocol_out + _protocol_out = sys.stdout + sys.stdout = sys.stderr + + try: + emit({"type": "ready", "protocolVersion": PROTOCOL_VERSION}) + + for raw_line in sys.stdin: + line = raw_line.strip() + + if not line: + continue + + try: + request = json.loads(line) + + if not isinstance(request, dict): + raise RuntimeError("Protocol request must be a JSON object.") + + except Exception as exc: + emit({"type": "bridge-error", "message": f"Invalid JSON request: {exc}"}) + continue + + if not handle_request(request): + break + + finally: + for session in (SESSION, CHECK): + try: + session.stop() + except Exception as exc: + log(f"neurostate shutdown warning: {exc}") + + sys.stdout = _protocol_out + _protocol_out = None + + +if __name__ == "__main__": + main() diff --git a/neurostate-sidecar/build.ps1 b/neurostate-sidecar/build.ps1 new file mode 100644 index 0000000..d11c983 --- /dev/null +++ b/neurostate-sidecar/build.ps1 @@ -0,0 +1,68 @@ +$ErrorActionPreference = "Stop" + +$ProjectRoot = Resolve-Path (Join-Path $PSScriptRoot "..") +$VenvPath = Join-Path $PSScriptRoot ".venv" +$PythonPath = Join-Path $VenvPath "Scripts\python.exe" + +if (-not (Test-Path $PythonPath)) { + Write-Host "Creating neurostate bridge virtual environment (needs Python 3.12 or newer)..." + py -3 -m venv $VenvPath +} + +Write-Host "Installing neurostate bridge dependencies..." +& $PythonPath -m pip install --upgrade pip +& $PythonPath -m pip install -r (Join-Path $PSScriptRoot "requirements.txt") + +# NEUROSTATE_SRC points at a local NeuroState checkout to build from instead of +# the pinned GitHub commit. +if ($env:NEUROSTATE_SRC) { + Write-Host "Installing neurostate from $env:NEUROSTATE_SRC..." + & $PythonPath -m pip install --upgrade $env:NEUROSTATE_SRC +} +else { + Write-Host "Installing neurostate from GitHub..." + & $PythonPath -m pip install -r (Join-Path $PSScriptRoot "neurostate.txt") +} + +if ($LASTEXITCODE -ne 0) { + throw "Installing neurostate failed." +} + +Write-Host "Building neurostate sidecar..." +Push-Location $PSScriptRoot +try { + & $PythonPath -m PyInstaller --clean --noconfirm "neurostate-bridge.spec" +} +finally { + Pop-Location +} + +$TargetTriple = "" +try { + $TargetTriple = (& rustc --print host-tuple).Trim() +} +catch { + $HostLine = (& rustc -Vv | Select-String "host:").Line + $TargetTriple = ($HostLine -split "\s+")[1].Trim() +} + +if ([string]::IsNullOrWhiteSpace($TargetTriple)) { + throw "Could not determine the Rust target triple." +} + +$SourceExe = Join-Path $PSScriptRoot "dist\neurostate-bridge.exe" +if (-not (Test-Path $SourceExe)) { + throw "PyInstaller did not create $SourceExe" +} + +$BinariesDir = Join-Path $ProjectRoot "src-tauri\binaries" +New-Item -ItemType Directory -Force -Path $BinariesDir | Out-Null + +$DestinationExe = Join-Path $BinariesDir "neurostate-bridge-$TargetTriple.exe" +Copy-Item -Force $SourceExe $DestinationExe + +Write-Host "" +Write-Host "neurostate sidecar built successfully:" +Write-Host $DestinationExe +Write-Host "" +Write-Host "Tauri externalBin entry remains: binaries/neurostate-bridge" diff --git a/neurostate-sidecar/neurostate-bridge.spec b/neurostate-sidecar/neurostate-bridge.spec new file mode 100644 index 0000000..4e1e308 --- /dev/null +++ b/neurostate-sidecar/neurostate-bridge.spec @@ -0,0 +1,38 @@ +# -*- mode: python ; coding: utf-8 -*- +from PyInstaller.utils.hooks import collect_all + +datas, binaries, hiddenimports = [("profiles/*.yaml", "profiles")], [], [] + +# neurostate loads its montages as package data; pylsl loads liblsl from its lib/ folder. +for package in ("neurostate", "pylsl"): + for collected, target in zip(collect_all(package), (datas, binaries, hiddenimports)): + target += collected + +a = Analysis( + ["bridge.py"], + pathex=[], + binaries=binaries, + datas=datas, + hiddenimports=hiddenimports, + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=["pyqtgraph", "PySide6", "matplotlib", "tkinter"], + noarchive=False, +) + +pyz = PYZ(a.pure) + +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.datas, + [], + name="neurostate-bridge", + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=False, + console=True, +) diff --git a/neurostate-sidecar/neurostate.txt b/neurostate-sidecar/neurostate.txt new file mode 100644 index 0000000..94176b6 --- /dev/null +++ b/neurostate-sidecar/neurostate.txt @@ -0,0 +1 @@ +neurostate @ git+https://github.com/KN-Neuron/NeuroState.git@08f0ab28cf90182bf2d0fb0d9dfec81fef388dce diff --git a/neurostate-sidecar/profiles/brainaccess-midi.yaml b/neurostate-sidecar/profiles/brainaccess-midi.yaml new file mode 100644 index 0000000..9988606 --- /dev/null +++ b/neurostate-sidecar/profiles/brainaccess-midi.yaml @@ -0,0 +1,7 @@ +# neurostate settings for the BrainAccess MIDI (16 dry electrodes). +# Mirrors config/brainaccess-midi.yaml in the NeuroState repository. +input: + montage: brainaccess-midi +bad_channels: + # Dry electrodes pick up much more mains hum than gel ones. + mains_ratio: 10 diff --git a/neurostate-sidecar/requirements.txt b/neurostate-sidecar/requirements.txt new file mode 100644 index 0000000..87d4b3c --- /dev/null +++ b/neurostate-sidecar/requirements.txt @@ -0,0 +1,3 @@ +# neurostate itself is installed by the bootstrap scripts: from NEUROSTATE_SRC when +# set (a local checkout), otherwise from the pinned GitHub commit in neurostate.txt. +pyinstaller>=6,<7 diff --git a/neurostate-sidecar/tests/test_bridge.py b/neurostate-sidecar/tests/test_bridge.py new file mode 100644 index 0000000..90d7590 --- /dev/null +++ b/neurostate-sidecar/tests/test_bridge.py @@ -0,0 +1,405 @@ +from __future__ import annotations + +import importlib.util +import io +import json +import socket +import sys +import threading +import time +import unittest +import uuid +from pathlib import Path +from unittest.mock import patch + +from neurostate.acquisition.mock_source import run_mock +from neurostate.config import Config, MockConfig +from neurostate.schema import BrainState, State + +SIDECAR_DIR = Path(__file__).resolve().parents[1] +BRIDGE_PATH = SIDECAR_DIR / "bridge.py" + + +def load_bridge(): + spec = importlib.util.spec_from_file_location("k_neuron_neurostate_bridge_test", BRIDGE_PATH) + if spec is None or spec.loader is None: + raise RuntimeError("Could not load neurostate-sidecar/bridge.py") + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + return module + + +def brain_state(state: State, quality: float = 90.0) -> BrainState: + return BrainState( + t=12.5, + attention=61.26, + relaxation=40.04, + quality=quality, + state=state, + calibrated=False, + ) + + +class Events: + """Collects emitted protocol messages from any thread.""" + + def __init__(self) -> None: + self.messages: list[dict] = [] + self._changed = threading.Condition() + + def append(self, message: dict) -> None: + with self._changed: + self.messages.append(message) + self._changed.notify_all() + + def wait_for(self, predicate, timeout_s: float) -> dict: + deadline = time.monotonic() + timeout_s + with self._changed: + while True: + for message in self.messages: + if predicate(message): + return message + remaining = deadline - time.monotonic() + if remaining <= 0: + raise AssertionError(f"No matching event; got {self.messages[-5:]}") + self._changed.wait(remaining) + + +class NeuroStateBridgeTests(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + cls.bridge = load_bridge() + + @classmethod + def tearDownClass(cls) -> None: + sys.modules.pop("k_neuron_neurostate_bridge_test", None) + + def test_emit_writes_one_compact_json_line(self) -> None: + stdout = io.StringIO() + with patch.object(sys, "stdout", stdout): + self.bridge.emit({"type": "ready", "protocolVersion": 1}) + + self.assertEqual(stdout.getvalue(), '{"type":"ready","protocolVersion":1}\n') + + def test_standard_profile_uses_neurostate_defaults(self) -> None: + config = self.bridge.load_profile("standard") + + self.assertEqual(config.input.montage, "standard-1020") + self.assertEqual(config.outputs.json_tcp.port, 13855) + + def test_midi_profile_tunes_for_dry_electrodes(self) -> None: + config = self.bridge.load_profile("brainaccess-midi") + + self.assertEqual(config.input.montage, "brainaccess-midi") + self.assertEqual(config.bad_channels.mains_ratio, 10) + + def test_profile_names_cannot_escape_the_profiles_folder(self) -> None: + with self.assertRaisesRegex(RuntimeError, "Invalid neurostate profile"): + self.bridge.load_profile("../secrets") + + with self.assertRaisesRegex(RuntimeError, "Unknown neurostate profile"): + self.bridge.load_profile("brainaccess-nope") + + def test_brain_state_event_carries_thinkgear_signal_level(self) -> None: + usable = self.bridge.brain_state_event(brain_state(State.UNCALIBRATED)) + poor = self.bridge.brain_state_event(brain_state(State.POOR_SIGNAL, quality=25.0)) + none = self.bridge.brain_state_event(brain_state(State.NO_SIGNAL, quality=0.0)) + + self.assertEqual( + usable, + { + "type": "brain-state", + "state": { + "t": 12.5, + "attention": 61.3, + "relaxation": 40.0, + "quality": 90.0, + "state": "uncalibrated", + "calibrated": False, + "poorSignalLevel": 0, + }, + }, + ) + self.assertEqual(poor["state"]["poorSignalLevel"], 150) + self.assertEqual(none["state"]["poorSignalLevel"], 200) + + def test_start_requires_a_stream_name(self) -> None: + events = Events() + with patch.object(self.bridge, "emit", side_effect=events.append): + keep_running = self.bridge.handle_request({"id": "1", "command": "start"}) + + self.assertTrue(keep_running) + self.assertFalse(events.messages[0]["ok"]) + self.assertIn("payload.stream", events.messages[0]["error"]) + + def test_unknown_command_is_a_response_error(self) -> None: + events = Events() + with patch.object(self.bridge, "emit", side_effect=events.append): + self.bridge.handle_request({"id": "x", "command": "nope"}) + + self.assertFalse(events.messages[0]["ok"]) + self.assertIn("Unknown neurostate bridge command", events.messages[0]["error"]) + + def test_main_keeps_stdout_pure(self) -> None: + stdin = io.StringIO( + "not-json\n" + '{"id":"1","command":"ping"}\n' + '{"id":"2","command":"stop"}\n' + '{"id":"3","command":"shutdown"}\n' + ) + stdout = io.StringIO() + stderr = io.StringIO() + + def noisy_ping(request): + print("a library talking on stdout") + return original(request) + + original = self.bridge.handle_request + with ( + patch.object(sys, "stdin", stdin), + patch.object(sys, "stdout", stdout), + patch.object(sys, "stderr", stderr), + patch.object(self.bridge, "handle_request", side_effect=noisy_ping), + ): + self.bridge.main() + restored = sys.stdout + + messages = [json.loads(line) for line in stdout.getvalue().splitlines()] + self.assertEqual(messages[0], {"type": "ready", "protocolVersion": 1}) + self.assertEqual(messages[1]["type"], "bridge-error") + self.assertEqual(messages[2]["result"], {"protocolVersion": 1}) + self.assertEqual(messages[3]["result"], {"running": False}) + self.assertEqual(messages[4]["result"], {"shutdown": True}) + self.assertIn("a library talking on stdout", stderr.getvalue()) + self.assertIs(restored, stdout) + + def test_service_reads_an_lsl_stream_and_publishes_values(self) -> None: + mock = MockConfig(stream_name=f"kneuron-test-{uuid.uuid4().hex[:8]}") + stop_mock = threading.Event() + mock_thread = threading.Thread( + target=run_mock, args=(mock,), kwargs={"stop": stop_mock}, daemon=True + ) + mock_thread.start() + self.addCleanup(mock_thread.join, 5) + self.addCleanup(stop_mock.set) + + # Any free port, and only the JSON output, so the test can't clash with a + # neurostate already running on this machine. + config = Config.model_validate( + { + "outputs": { + "json_tcp": {"port": 0}, + "lsl": {"enabled": False}, + "websocket": {"enabled": False}, + "thinkgear": {"enabled": False}, + } + } + ) + + session = self.bridge.NeuroStateSession() + events = Events() + with patch.object(self.bridge, "emit", side_effect=events.append): + self.assertEqual( + session.start(mock.stream_name, config), + {"started": True, "stream": mock.stream_name}, + ) + self.assertEqual(session.start(mock.stream_name, config)["started"], False) + + running = events.wait_for(lambda m: m.get("status") == "running", 30) + update = events.wait_for(lambda m: m["type"] == "brain-state", 10) + + # neurostate's own JSON output carries the same values for the games. + address = running["outputs"][0].rsplit(" ", 1)[-1] + host, port = address.rsplit(":", 1) + with socket.create_connection((host, int(port)), timeout=5) as client: + line = client.makefile().readline() + self.assertEqual(json.loads(line)["v"], 1) + + self.assertEqual(session.stop(), {"running": False}) + events.wait_for(lambda m: m.get("status") == "stopped", 10) + + self.assertIn(update["state"]["state"], {"calibrating", "poor_signal", "no_signal"}) + self.assertFalse(session.running) + + def test_relayed_state_maps_thinkgear_signal_levels(self) -> None: + usable = self.bridge.relayed_state(1.0, 73, 44, 0) + self.assertEqual(usable.state, State.UNCALIBRATED) + self.assertEqual((usable.attention, usable.relaxation, usable.quality), (73, 44, 100)) + self.assertFalse(usable.calibrated) + + poor = self.bridge.relayed_state(1.0, 73, 44, 50) + self.assertEqual(poor.state, State.POOR_SIGNAL) + self.assertEqual((poor.attention, poor.relaxation, poor.quality), (0, 0, 75)) + + for level in (200, None): + absent = self.bridge.relayed_state(1.0, 73, 44, level) + self.assertEqual(absent.state, State.NO_SIGNAL) + self.assertEqual(absent.quality, 0) + + # eSense values may not have arrived yet, or be out of range. + partial = self.bridge.relayed_state(1.0, None, 150, 0) + self.assertEqual((partial.attention, partial.relaxation), (0, 100)) + + def test_relay_reports_no_signal_without_recent_metrics(self) -> None: + relay = self.bridge.MetricsRelay() + now = self.bridge.pylsl.local_clock() + self.assertEqual(relay(now).state, State.NO_SIGNAL) + + relay.update(73, 44, 0) + now = self.bridge.pylsl.local_clock() + self.assertEqual(relay(now).state, State.UNCALIBRATED) + self.assertEqual(relay(now + self.bridge.RELAY_STALE_S + 1).state, State.NO_SIGNAL) + + def test_push_metrics_requires_a_running_relay(self) -> None: + session = self.bridge.NeuroStateSession() + with self.assertRaisesRegex(RuntimeError, "not relaying"): + session.push_metrics(73, 44, 0) + + def test_relay_publishes_pushed_metrics_on_the_json_output(self) -> None: + config = Config.model_validate( + { + "outputs": { + "json_tcp": {"port": 0}, + "lsl": {"enabled": False}, + "websocket": {"enabled": False}, + "thinkgear": {"enabled": False}, + } + } + ) + + session = self.bridge.NeuroStateSession() + events = Events() + with patch.object(self.bridge, "emit", side_effect=events.append): + self.assertTrue(session.start_relay(config)["started"]) + self.assertFalse(session.start_relay(config)["started"]) + with self.assertRaisesRegex(RuntimeError, "already running"): + session.start("Some Cap", config) + + running = events.wait_for(lambda m: m.get("status") == "running", 10) + session.push_metrics(73, 44, 0) + + host, port = running["outputs"][0].rsplit(" ", 1)[-1].rsplit(":", 1) + with socket.create_connection((host, int(port)), timeout=5) as client: + lines = client.makefile() + message = json.loads(lines.readline()) + while message["state"] == "no_signal": + message = json.loads(lines.readline()) + + update = events.wait_for( + lambda m: m["type"] == "brain-state" and m["state"]["attention"] == 73, 5 + ) + self.assertEqual(session.stop(), {"running": False}) + events.wait_for(lambda m: m.get("status") == "stopped", 10) + + self.assertEqual(message["attention"], 73) + self.assertEqual(message["relaxation"], 44) + self.assertEqual(message["state"], "uncalibrated") + self.assertEqual(update["state"]["poorSignalLevel"], 0) + + def start_mock(self, **settings) -> MockConfig: + mock = MockConfig(stream_name=f"kneuron-test-{uuid.uuid4().hex[:8]}", **settings) + stop_mock = threading.Event() + thread = threading.Thread( + target=run_mock, args=(mock,), kwargs={"stop": stop_mock}, daemon=True + ) + thread.start() + self.addCleanup(thread.join, 5) + self.addCleanup(stop_mock.set) + return mock + + def run_check(self, mock: MockConfig, config: Config, until) -> Events: + session = self.bridge.CheckSession() + events = Events() + with patch.object(self.bridge, "emit", side_effect=events.append): + self.assertTrue(session.start(mock.stream_name, config)["started"]) + try: + events.wait_for(until, 30) + finally: + self.assertEqual(session.stop(), {"running": False}) + return events + + def test_signal_check_reports_channels_signals_and_spectrum(self) -> None: + midi = "P8,O2,P4,C4,F8,F4,Oz,Cz,Fz,Pz,F3,O1,P7,C3,P3,F7".split(",") + mock = self.start_mock(channels=midi, alpha_uv=20.0) + + events = self.run_check( + mock, + self.bridge.load_profile("brainaccess-midi"), + lambda m: m["type"] == "check-status" and m["spectrum"] is not None, + ) + + info = next(m for m in events.messages if m["type"] == "check-info") + self.assertEqual(info["labels"], midi) + # Regions list channels in the montage's order. + self.assertEqual(info["spectrumChannels"], ["O1", "Oz", "O2"]) + self.assertEqual(info["regions"]["frontal"], ["Fz", "F3", "F4"]) + self.assertEqual(info["thresholds"]["mainsRatio"], 10) + + signals = next(m for m in events.messages if m["type"] == "check-signals") + self.assertEqual(len(signals["values"]), 16) + + status = [m for m in events.messages if m["type"] == "check-status"][-1] + self.assertEqual([channel["label"] for channel in status["channels"]], midi) + self.assertTrue(all(channel["status"] == "ok" for channel in status["channels"])) + self.assertEqual(status["regions"]["occipital"], {"good": 3, "total": 3}) + self.assertEqual(status["stream"]["nominalRateHz"], 250.0) + + # The mock's alpha rhythm shows as the spectrum's peak over the back of the head. + spectrum = status["spectrum"] + peak_hz = spectrum["hz"][spectrum["psd"].index(max(spectrum["psd"]))] + self.assertTrue(8 <= peak_hz <= 13, peak_hz) + self.assertEqual(events.messages[-1], { + "type": "check-state", "state": "stopped", "message": "Signal check stopped." + }) + + def test_signal_check_flags_mains_hum(self) -> None: + mock = self.start_mock(line_noise_uv=40.0) + + events = self.run_check( + mock, + Config(), + lambda m: m["type"] == "check-status" and m["channels"], + ) + + status = next(m for m in events.messages if m["type"] == "check-status" and m["channels"]) + self.assertIn("mains", {channel["status"] for channel in status["channels"]}) + + def test_numbers_json_cannot_carry_become_null(self) -> None: + self.assertEqual(self.bridge.finite(1.234, 1), 1.2) + self.assertIsNone(self.bridge.finite(float("inf"), 1)) + self.assertIsNone(self.bridge.finite(float("nan"), 1)) + + def test_start_check_requires_a_stream_name(self) -> None: + events = Events() + with patch.object(self.bridge, "emit", side_effect=events.append): + self.bridge.handle_request({"id": "1", "command": "start_check"}) + + self.assertIn("start_check requires payload.stream", events.messages[0]["error"]) + + def test_stop_check_while_waiting_for_a_stream(self) -> None: + session = self.bridge.CheckSession() + events = Events() + with patch.object(self.bridge, "emit", side_effect=events.append): + session.start(f"kneuron-missing-{uuid.uuid4().hex[:8]}", Config()) + events.wait_for(lambda m: m.get("state") == "waiting-for-stream", 10) + self.assertEqual(session.stop(), {"running": False}) + + self.assertEqual(events.messages[-1]["state"], "stopped") + + def test_stop_while_waiting_for_a_stream(self) -> None: + session = self.bridge.NeuroStateSession() + events = Events() + with patch.object(self.bridge, "emit", side_effect=events.append): + session.start(f"kneuron-missing-{uuid.uuid4().hex[:8]}", Config()) + events.wait_for(lambda m: m.get("status") == "waiting-for-stream", 10) + + self.assertEqual(session.stop(), {"running": False}) + # Stopping again, as shutdown does at exit, is harmless. + self.assertEqual(session.stop(), {"running": False}) + + self.assertEqual(events.messages[-1]["status"], "stopped") + + +if __name__ == "__main__": + unittest.main() diff --git a/package-lock.json b/package-lock.json index ea6c6de..7631e90 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,10 @@ "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-dialog": "^2.8.3", "@tauri-apps/plugin-shell": "^2.4.0", "lucide-react": "^0.468.0", "react": "^18.3.1", @@ -1173,6 +1176,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", @@ -1914,6 +1935,15 @@ "node": ">= 10" } }, + "node_modules/@tauri-apps/plugin-dialog": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.8.3.tgz", + "integrity": "sha512-Hd/yBfk2R5v2MFrz/0VArSXeh8xKLLXGunqSiTtcHKn1zNZwaKujuA0VSwUvNfwC4hDoF1PveS+gBDNe3nHspw==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.12.0" + } + }, "node_modules/@tauri-apps/plugin-shell": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-shell/-/plugin-shell-2.4.0.tgz", diff --git a/package.json b/package.json index c141132..d1e23d7 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,10 @@ "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-dialog": "^2.8.3", "@tauri-apps/plugin-shell": "^2.4.0", "lucide-react": "^0.468.0", "react": "^18.3.1", diff --git a/public/brain-logo.png b/public/brain-logo.png new file mode 100644 index 0000000..8c2a61c Binary files /dev/null and b/public/brain-logo.png differ diff --git a/public/devices/brainaccess-maxi.png b/public/devices/brainaccess-maxi.png new file mode 100644 index 0000000..46c8623 Binary files /dev/null and b/public/devices/brainaccess-maxi.png differ diff --git a/public/devices/brainlink-lite.png b/public/devices/brainlink-lite.png new file mode 100644 index 0000000..71fa3f9 Binary files /dev/null and b/public/devices/brainlink-lite.png differ diff --git a/public/modules/cortex/thumbnail.png b/public/modules/cortex/thumbnail.png new file mode 100644 index 0000000..7c584ab Binary files /dev/null and b/public/modules/cortex/thumbnail.png differ diff --git a/public/modules/miner/thumbnail.png b/public/modules/miner/thumbnail.png new file mode 100644 index 0000000..8f14962 Binary files /dev/null and b/public/modules/miner/thumbnail.png differ diff --git a/public/modules/neuorrun/thumbnail.png b/public/modules/neuorrun/thumbnail.png new file mode 100644 index 0000000..4ea0458 Binary files /dev/null and b/public/modules/neuorrun/thumbnail.png differ diff --git a/public/modules/neurorun/thumbnail.png b/public/modules/neurorun/thumbnail.png new file mode 100644 index 0000000..4ea0458 Binary files /dev/null and b/public/modules/neurorun/thumbnail.png differ diff --git a/public/modules/neurosniper/thumbnail.png b/public/modules/neurosniper/thumbnail.png new file mode 100644 index 0000000..232e5c1 Binary files /dev/null and b/public/modules/neurosniper/thumbnail.png differ diff --git a/public/modules/signal-check/thumbnail.png b/public/modules/signal-check/thumbnail.png new file mode 100644 index 0000000..f85a2d9 Binary files /dev/null and b/public/modules/signal-check/thumbnail.png differ diff --git a/setup-and-run.ps1 b/setup-and-run.ps1 index a6a8523..6f0d6c6 100644 --- a/setup-and-run.ps1 +++ b/setup-and-run.ps1 @@ -263,12 +263,17 @@ Build-Sidecar ` -Name "BrainLink bridge" ` -Directory (Join-Path $root "brainlink-sidecar") +Build-Sidecar ` + -Name "neurostate bridge" ` + -Directory (Join-Path $root "neurostate-sidecar") + Step "Verifying sidecars" $expectedBinaries = @( "brainaccess-bridge-x86_64-pc-windows-msvc.exe", "ssvep-classifier-x86_64-pc-windows-msvc.exe", - "brainlink-bridge-x86_64-pc-windows-msvc.exe" + "brainlink-bridge-x86_64-pc-windows-msvc.exe", + "neurostate-bridge-x86_64-pc-windows-msvc.exe" ) foreach ($binaryName in $expectedBinaries) { diff --git a/setup-and-run.sh b/setup-and-run.sh index 2edf237..6204781 100644 --- a/setup-and-run.sh +++ b/setup-and-run.sh @@ -20,6 +20,10 @@ Usage: Options: --no-launch Prepare everything but do not start KNeuron. --skip-system Do not install OS-level packages; only verify/use what exists. + +Environment: + NEUROSTATE_SRC Local NeuroState checkout to build the neurostate sidecar from, + instead of the commit pinned in neurostate-sidecar/neurostate.txt. EOF exit 0 ;; @@ -173,9 +177,10 @@ ensure_python() { python3 - <<'PY' import sys -if sys.version_info < (3, 10): +# neurostate, which the neurostate sidecar bundles, needs Python 3.12. +if sys.version_info < (3, 12): raise SystemExit( - f"Python >= 3.10 is required; found {sys.version.split()[0]}" + f"Python >= 3.12 is required; found {sys.version.split()[0]}" ) print(f"[OK] Python {sys.version.split()[0]}") PY @@ -201,6 +206,17 @@ build_sidecar() { "$venv/bin/python" -m pip install -r "$ROOT/$dir/requirements.txt" "$venv/bin/python" -m pip install --upgrade pyinstaller + # The neurostate sidecar bundles the NeuroState package. + if [[ -f "$ROOT/$dir/neurostate.txt" ]]; then + if [[ -n "${NEUROSTATE_SRC:-}" ]]; then + [[ -d "$NEUROSTATE_SRC" ]] || die "NEUROSTATE_SRC is not a directory: $NEUROSTATE_SRC" + ok "Using neurostate from $NEUROSTATE_SRC" + "$venv/bin/python" -m pip install --upgrade "$NEUROSTATE_SRC" + else + "$venv/bin/python" -m pip install -r "$ROOT/$dir/neurostate.txt" + fi + fi + ( cd "$ROOT/$dir" "$venv/bin/python" -m PyInstaller \ @@ -300,6 +316,11 @@ build_sidecar \ "brainlink-bridge" \ "brainlink-bridge.spec" +build_sidecar \ + "neurostate-sidecar" \ + "neurostate-bridge" \ + "neurostate-bridge.spec" + step "Verifying runtime assets" [[ -d "$ROOT/public/modules/cortex" ]] \ @@ -319,7 +340,8 @@ HOST_TARGET="$(rustc -vV | awk '/^host:/ { print $2 }')" for name in \ brainaccess-bridge \ ssvep-classifier \ - brainlink-bridge + brainlink-bridge \ + neurostate-bridge do file="$ROOT/src-tauri/binaries/${name}-${HOST_TARGET}" [[ -x "$file" ]] || die "Missing executable sidecar: $file" diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index fbddf64..89e9d4d 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -139,6 +139,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + [[package]] name = "block2" version = "0.6.2" @@ -316,6 +325,12 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + [[package]] name = "chrono" version = "0.4.45" @@ -338,6 +353,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + [[package]] name = "cookie" version = "0.18.2" @@ -373,7 +394,7 @@ dependencies = [ "bitflags 2.13.2", "core-foundation", "core-graphics-types", - "foreign-types", + "foreign-types 0.5.0", "libc", ] @@ -403,6 +424,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.5.2" @@ -437,6 +467,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + [[package]] name = "cssparser" version = "0.37.0" @@ -578,8 +617,19 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "crypto-common", + "block-buffer 0.10.4", + "crypto-common 0.1.7", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "const-oid", + "crypto-common 0.2.2", ] [[package]] @@ -786,6 +836,16 @@ dependencies = [ "rustc_version", ] +[[package]] +name = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if", + "libc", +] + [[package]] name = "find-msvc-tools" version = "0.1.14" @@ -815,6 +875,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared 0.1.1", +] + [[package]] name = "foreign-types" version = "0.5.0" @@ -822,7 +891,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" dependencies = [ "foreign-types-macros", - "foreign-types-shared", + "foreign-types-shared 0.3.1", ] [[package]] @@ -836,6 +905,12 @@ dependencies = [ "syn 3.0.6", ] +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "foreign-types-shared" version = "0.3.1" @@ -1281,6 +1356,15 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +[[package]] +name = "hybrid-array" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" +dependencies = [ + "typenum", +] + [[package]] name = "hyper" version = "1.11.1" @@ -1301,6 +1385,22 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.21" @@ -1700,11 +1800,18 @@ dependencies = [ name = "kneuron-shell" version = "0.2.7" dependencies = [ + "flate2", + "libc", + "reqwest", "serde", "serde_json", + "sha2 0.11.1", + "tar", "tauri", "tauri-build", + "tauri-plugin-dialog", "tauri-plugin-shell", + "zip", ] [[package]] @@ -1765,6 +1872,12 @@ dependencies = [ "libc", ] +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + [[package]] name = "litemap" version = "0.8.3" @@ -1876,6 +1989,23 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d" +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "ndk" version = "0.9.0" @@ -1912,6 +2042,18 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "nix" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" +dependencies = [ + "bitflags 2.13.2", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "num-conv" version = "0.2.2" @@ -2073,6 +2215,7 @@ checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ "bitflags 2.13.2", "block2", + "libc", "objc2", "objc2-core-foundation", ] @@ -2162,6 +2305,49 @@ dependencies = [ "libc", ] +[[package]] +name = "openssl" +version = "0.10.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45" +dependencies = [ + "bitflags 2.13.2", + "cfg-if", + "foreign-types 0.3.2", + "libc", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-sys" +version = "0.9.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "option-ext" version = "0.2.0" @@ -2552,15 +2738,19 @@ dependencies = [ "http-body", "http-body-util", "hyper", + "hyper-tls", "hyper-util", "js-sys", "log", + "native-tls", "percent-encoding", "pin-project-lite", + "rustls-pki-types", "serde", "serde_json", "sync_wrapper", "tokio", + "tokio-native-tls", "tokio-util", "tower", "tower-http", @@ -2572,6 +2762,30 @@ dependencies = [ "web-sys", ] +[[package]] +name = "rfd" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672" +dependencies = [ + "block2", + "dispatch2", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.60.2", +] + [[package]] name = "rustc-hash" version = "2.1.3" @@ -2587,6 +2801,28 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" +dependencies = [ + "bitflags 2.13.2", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "zeroize", +] + [[package]] name = "rustversion" version = "1.0.23" @@ -2602,6 +2838,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cb23c424d8ee841bb4d813677ca237e2a610067d594fec130f87d6093495ab5" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "schemars" version = "0.8.22" @@ -2659,6 +2904,29 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.13.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "selectors" version = "0.38.0" @@ -2854,8 +3122,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d7069beb7d6ac7b9acd1039986e73443f24234f41074da099d6f994ac9ad19" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "digest 0.11.3", ] [[package]] @@ -3157,6 +3436,17 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "tar" +version = "0.4.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "target-lexicon" version = "0.12.16" @@ -3252,7 +3542,7 @@ dependencies = [ "semver", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "syn 2.0.119", "tauri-utils", "thiserror 2.0.21", @@ -3292,6 +3582,48 @@ dependencies = [ "walkdir", ] +[[package]] +name = "tauri-plugin-dialog" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7527c5fd1678032d116c1a82e5d609c67370e56f858ae6cfd376bbca37fe0490" +dependencies = [ + "log", + "raw-window-handle", + "rfd", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "thiserror 2.0.21", + "url", +] + +[[package]] +name = "tauri-plugin-fs" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "466eb32466fe5021b6728b06862f9ff14a9f7e1af890047654e03a96d7c0e1bb" +dependencies = [ + "anyhow", + "dunce", + "glob", + "log", + "objc2-foundation", + "percent-encoding", + "schemars 0.8.22", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.21", + "toml 1.1.6+spec-1.1.0", + "url", +] + [[package]] name = "tauri-plugin-shell" version = "2.4.0" @@ -3413,6 +3745,19 @@ dependencies = [ "toml 1.1.6+spec-1.1.0", ] +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "tendril" version = "0.5.1" @@ -3522,6 +3867,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.19" @@ -3723,6 +4078,12 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "typed-path" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e" + [[package]] name = "typeid" version = "1.0.3" @@ -3790,6 +4151,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version-compare" version = "0.2.1" @@ -4201,6 +4568,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -4234,13 +4610,30 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + [[package]] name = "windows-threading" version = "0.2.1" @@ -4271,6 +4664,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -4283,6 +4682,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[package]] name = "windows_i686_gnu" version = "0.42.2" @@ -4295,12 +4700,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[package]] name = "windows_i686_msvc" version = "0.42.2" @@ -4313,6 +4730,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[package]] name = "windows_x86_64_gnu" version = "0.42.2" @@ -4325,6 +4748,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -4337,6 +4766,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + [[package]] name = "windows_x86_64_msvc" version = "0.42.2" @@ -4349,6 +4784,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "winnow" version = "0.5.40" @@ -4419,7 +4860,7 @@ dependencies = [ "once_cell", "percent-encoding", "raw-window-handle", - "sha2", + "sha2 0.10.9", "soup3", "tao-macros", "thiserror 2.0.21", @@ -4454,6 +4895,16 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + [[package]] name = "yoke" version = "0.8.3" @@ -4498,6 +4949,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879" + [[package]] name = "zerotrie" version = "0.2.5" @@ -4531,6 +4988,20 @@ dependencies = [ "syn 3.0.6", ] +[[package]] +name = "zip" +version = "9.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e04976da629d3b04204ba8105b396a193af8ad2583b2c79bfb3ab8af580a11e" +dependencies = [ + "crc32fast", + "flate2", + "indexmap 2.14.2", + "memchr", + "nix", + "typed-path", +] + [[package]] name = "zlib-rs" version = "0.6.8" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index f5441e9..8125986 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -17,3 +17,12 @@ tauri = { version = "2", features = [] } serde = { version = "1", features = ["derive"] } serde_json = "1" tauri-plugin-shell = "2" +tauri-plugin-dialog = "2" +reqwest = { version = "0.13", default-features = false, features = ["native-tls", "json"] } +zip = { version = "9", default-features = false, features = ["deflate-flate2"] } +tar = "0.4" +flate2 = "1" +sha2 = "0.11" + +[target.'cfg(unix)'.dependencies] +libc = "0.2" diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index f14c0ba..7b2f809 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -1,13 +1,14 @@ { "$schema": "../gen/schemas/desktop-schema.json", "identifier": "main-capability", - "description": "Minimal permissions required by the KNeuron custom title bar and approved sidecars.", + "description": "Minimal permissions required by the KNeuron custom title bar, approved sidecars and choosing game executables.", "windows": ["main"], "permissions": [ "core:window:allow-close", "core:window:allow-minimize", "core:window:allow-toggle-maximize", "core:window:allow-start-dragging", + "dialog:allow-open", "shell:allow-stdin-write", "shell:allow-kill", { @@ -24,6 +25,10 @@ { "name": "binaries/brainlink-bridge", "sidecar": true + }, + { + "name": "binaries/neurostate-bridge", + "sidecar": true } ] } diff --git a/src-tauri/gen/schemas/acl-manifests.json b/src-tauri/gen/schemas/acl-manifests.json index 00e6fc6..0864b9d 100644 --- a/src-tauri/gen/schemas/acl-manifests.json +++ b/src-tauri/gen/schemas/acl-manifests.json @@ -1,2009 +1 @@ -{ - "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" - } - } -} +{"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},"dialog":{"default_permission":{"identifier":"default","description":"This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n","permissions":["allow-message","allow-save","allow-open"]},"permissions":{"allow-ask":{"identifier":"allow-ask","description":"Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)","commands":{"allow":["message"],"deny":[]}},"allow-confirm":{"identifier":"allow-confirm","description":"Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)","commands":{"allow":["message"],"deny":[]}},"allow-message":{"identifier":"allow-message","description":"Enables the message command without any pre-configured scope.","commands":{"allow":["message"],"deny":[]}},"allow-open":{"identifier":"allow-open","description":"Enables the open command without any pre-configured scope.","commands":{"allow":["open"],"deny":[]}},"allow-save":{"identifier":"allow-save","description":"Enables the save command without any pre-configured scope.","commands":{"allow":["save"],"deny":[]}},"deny-ask":{"identifier":"deny-ask","description":"Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)","commands":{"allow":[],"deny":["message"]}},"deny-confirm":{"identifier":"deny-confirm","description":"Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)","commands":{"allow":[],"deny":["message"]}},"deny-message":{"identifier":"deny-message","description":"Denies the message command without any pre-configured scope.","commands":{"allow":[],"deny":["message"]}},"deny-open":{"identifier":"deny-open","description":"Denies the open command without any pre-configured scope.","commands":{"allow":[],"deny":["open"]}},"deny-save":{"identifier":"deny-save","description":"Denies the save command without any pre-configured scope.","commands":{"allow":[],"deny":["save"]}}},"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 diff --git a/src-tauri/gen/schemas/capabilities.json b/src-tauri/gen/schemas/capabilities.json index 09e9c2d..f493f3a 100644 --- a/src-tauri/gen/schemas/capabilities.json +++ b/src-tauri/gen/schemas/capabilities.json @@ -1,24 +1 @@ -{ - "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 } - ] - } - ] - } -} +{"main-capability":{"identifier":"main-capability","description":"Minimal permissions required by the KNeuron custom title bar, approved sidecars and choosing game executables.","local":true,"windows":["main"],"permissions":["core:window:allow-close","core:window:allow-minimize","core:window:allow-toggle-maximize","core:window:allow-start-dragging","dialog:allow-open","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},{"name":"binaries/neurostate-bridge","sidecar":true}]}]}} \ No newline at end of file diff --git a/src-tauri/gen/schemas/desktop-schema.json b/src-tauri/gen/schemas/desktop-schema.json index ad4f1d7..40e4ae5 100644 --- a/src-tauri/gen/schemas/desktop-schema.json +++ b/src-tauri/gen/schemas/desktop-schema.json @@ -21,7 +21,9 @@ { "description": "A list of capabilities.", "type": "object", - "required": ["capabilities"], + "required": [ + "capabilities" + ], "properties": { "capabilities": { "description": "The list of capabilities.", @@ -37,7 +39,10 @@ "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`", @@ -88,7 +93,10 @@ }, "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" } @@ -98,7 +106,9 @@ "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", @@ -208,7 +218,10 @@ "anyOf": [ { "type": "object", - "required": ["cmd", "name"], + "required": [ + "cmd", + "name" + ], "properties": { "args": { "description": "The allowed arguments for the command execution.", @@ -231,7 +244,10 @@ }, { "type": "object", - "required": ["name", "sidecar"], + "required": [ + "name", + "sidecar" + ], "properties": { "args": { "description": "The allowed arguments for the command execution.", @@ -262,7 +278,10 @@ "anyOf": [ { "type": "object", - "required": ["cmd", "name"], + "required": [ + "cmd", + "name" + ], "properties": { "args": { "description": "The allowed arguments for the command execution.", @@ -285,7 +304,10 @@ }, { "type": "object", - "required": ["name", "sidecar"], + "required": [ + "name", + "sidecar" + ], "properties": { "args": { "description": "The allowed arguments for the command execution.", @@ -334,14 +356,20 @@ }, "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" } @@ -349,7 +377,9 @@ } } ], - "required": ["identifier"] + "required": [ + "identifier" + ] } ] }, @@ -2396,6 +2426,72 @@ "const": "core:window:deny-unminimize", "markdownDescription": "Denies the unminimize command without any pre-configured scope." }, + { + "description": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`", + "type": "string", + "const": "dialog:default", + "markdownDescription": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`" + }, + { + "description": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)", + "type": "string", + "const": "dialog:allow-ask", + "markdownDescription": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)" + }, + { + "description": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)", + "type": "string", + "const": "dialog:allow-confirm", + "markdownDescription": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)" + }, + { + "description": "Enables the message command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-message", + "markdownDescription": "Enables the message command without any pre-configured scope." + }, + { + "description": "Enables the open command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-open", + "markdownDescription": "Enables the open command without any pre-configured scope." + }, + { + "description": "Enables the save command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-save", + "markdownDescription": "Enables the save command without any pre-configured scope." + }, + { + "description": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)", + "type": "string", + "const": "dialog:deny-ask", + "markdownDescription": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)" + }, + { + "description": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)", + "type": "string", + "const": "dialog:deny-confirm", + "markdownDescription": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)" + }, + { + "description": "Denies the message command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-message", + "markdownDescription": "Denies the message command without any pre-configured scope." + }, + { + "description": "Denies the open command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-open", + "markdownDescription": "Denies the open command without any pre-configured scope." + }, + { + "description": "Denies the save command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-save", + "markdownDescription": "Denies the save command without any pre-configured scope." + }, { "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\n#### This default permission set includes:\n\n- `allow-open`", "type": "string", @@ -2524,27 +2620,37 @@ { "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" + ] } ] }, @@ -2558,7 +2664,9 @@ { "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.", @@ -2591,4 +2699,4 @@ ] } } -} +} \ No newline at end of file diff --git a/src-tauri/gen/schemas/linux-schema.json b/src-tauri/gen/schemas/linux-schema.json new file mode 100644 index 0000000..40e4ae5 --- /dev/null +++ b/src-tauri/gen/schemas/linux-schema.json @@ -0,0 +1,2702 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "CapabilityFile", + "description": "Capability formats accepted in a capability file.", + "anyOf": [ + { + "description": "A single capability.", + "allOf": [ + { + "$ref": "#/definitions/Capability" + } + ] + }, + { + "description": "A list of capabilities.", + "type": "array", + "items": { + "$ref": "#/definitions/Capability" + } + }, + { + "description": "A list of capabilities.", + "type": "object", + "required": [ + "capabilities" + ], + "properties": { + "capabilities": { + "description": "The list of capabilities.", + "type": "array", + "items": { + "$ref": "#/definitions/Capability" + } + } + } + } + ], + "definitions": { + "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" + ], + "properties": { + "identifier": { + "description": "Identifier of the capability.\n\n## Example\n\n`main-user-files-write`", + "type": "string" + }, + "description": { + "description": "Description of what the capability is intended to allow on associated windows.\n\nIt should contain a description of what the grouped permissions should allow.\n\n## Example\n\nThis capability allows the `main` window access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.", + "default": "", + "type": "string" + }, + "remote": { + "description": "Configure remote URLs that can use the capability permissions.\n\nThis setting is optional and defaults to not being set, as our default use case is that the content is served from our local application.\n\n:::caution Make sure you understand the security implications of providing remote sources with local system access. :::\n\n## Example\n\n```json { \"urls\": [\"https://*.mydomain.dev\"] } ```", + "anyOf": [ + { + "$ref": "#/definitions/CapabilityRemote" + }, + { + "type": "null" + } + ] + }, + "local": { + "description": "Whether this capability is enabled for local app URLs or not. Defaults to `true`.", + "default": true, + "type": "boolean" + }, + "windows": { + "description": "List of windows that are affected by this capability. Can be a glob pattern.\n\nIf a window label matches any of the patterns in this list, the capability will be enabled on all the webviews of that window, regardless of the value of [`Self::webviews`].\n\nOn multiwebview windows, prefer specifying [`Self::webviews`] and omitting [`Self::windows`] for a fine grained access control.\n\n## Example\n\n`[\"main\"]`", + "type": "array", + "items": { + "type": "string" + } + }, + "webviews": { + "description": "List of webviews that are affected by this capability. Can be a glob pattern.\n\nThe capability will be enabled on all the webviews whose label matches any of the patterns in this list, regardless of whether the webview's window label matches a pattern in [`Self::windows`].\n\n## Example\n\n`[\"sub-webview-one\", \"sub-webview-two\"]`", + "type": "array", + "items": { + "type": "string" + } + }, + "permissions": { + "description": "List of permissions attached to this capability.\n\nMust include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`. For commands directly implemented in the application itself only `${permission-name}` is required.\n\n## Example\n\n```json [ \"core:default\", \"shell:allow-open\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] } ] ```", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionEntry" + }, + "uniqueItems": true + }, + "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" + ], + "items": { + "$ref": "#/definitions/Target" + } + } + } + }, + "CapabilityRemote": { + "description": "Configuration for remote URLs that are associated with the capability.", + "type": "object", + "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", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "PermissionEntry": { + "description": "An entry for a permission value in a [`Capability`] can be either a raw permission [`Identifier`] or an object that references a permission and extends its scope.", + "anyOf": [ + { + "description": "Reference a permission or permission set by identifier.", + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ] + }, + { + "description": "Reference a permission or permission set by identifier and extends its scope.", + "type": "object", + "allOf": [ + { + "if": { + "properties": { + "identifier": { + "anyOf": [ + { + "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\n#### This default permission set includes:\n\n- `allow-open`", + "type": "string", + "const": "shell:default", + "markdownDescription": "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\n#### This default permission set includes:\n\n- `allow-open`" + }, + { + "description": "Enables the execute command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-execute", + "markdownDescription": "Enables the execute command without any pre-configured scope." + }, + { + "description": "Enables the kill command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-kill", + "markdownDescription": "Enables the kill command without any pre-configured scope." + }, + { + "description": "Enables the open command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-open", + "markdownDescription": "Enables the open command without any pre-configured scope." + }, + { + "description": "Enables the spawn command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-spawn", + "markdownDescription": "Enables the spawn command without any pre-configured scope." + }, + { + "description": "Enables the stdin_write command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-stdin-write", + "markdownDescription": "Enables the stdin_write command without any pre-configured scope." + }, + { + "description": "Denies the execute command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-execute", + "markdownDescription": "Denies the execute command without any pre-configured scope." + }, + { + "description": "Denies the kill command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-kill", + "markdownDescription": "Denies the kill command without any pre-configured scope." + }, + { + "description": "Denies the open command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-open", + "markdownDescription": "Denies the open command without any pre-configured scope." + }, + { + "description": "Denies the spawn command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-spawn", + "markdownDescription": "Denies the spawn command without any pre-configured scope." + }, + { + "description": "Denies the stdin_write command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-stdin-write", + "markdownDescription": "Denies the stdin_write command without any pre-configured scope." + } + ] + } + } + }, + "then": { + "properties": { + "allow": { + "items": { + "title": "ShellScopeEntry", + "description": "Shell scope entry.", + "anyOf": [ + { + "type": "object", + "required": [ + "cmd", + "name" + ], + "properties": { + "args": { + "description": "The allowed arguments for the command execution.", + "allOf": [ + { + "$ref": "#/definitions/ShellScopeEntryAllowedArgs" + } + ] + }, + "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" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "name", + "sidecar" + ], + "properties": { + "args": { + "description": "The allowed arguments for the command execution.", + "allOf": [ + { + "$ref": "#/definitions/ShellScopeEntryAllowedArgs" + } + ] + }, + "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" + } + }, + "additionalProperties": false + } + ] + } + }, + "deny": { + "items": { + "title": "ShellScopeEntry", + "description": "Shell scope entry.", + "anyOf": [ + { + "type": "object", + "required": [ + "cmd", + "name" + ], + "properties": { + "args": { + "description": "The allowed arguments for the command execution.", + "allOf": [ + { + "$ref": "#/definitions/ShellScopeEntryAllowedArgs" + } + ] + }, + "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" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "name", + "sidecar" + ], + "properties": { + "args": { + "description": "The allowed arguments for the command execution.", + "allOf": [ + { + "$ref": "#/definitions/ShellScopeEntryAllowedArgs" + } + ] + }, + "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" + } + }, + "additionalProperties": false + } + ] + } + } + } + }, + "properties": { + "identifier": { + "description": "Identifier of the permission or permission set.", + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ] + } + } + }, + { + "properties": { + "identifier": { + "description": "Identifier of the permission or permission set.", + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ] + }, + "allow": { + "description": "Data that defines what is allowed by the scope.", + "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" + ], + "items": { + "$ref": "#/definitions/Value" + } + } + } + } + ], + "required": [ + "identifier" + ] + } + ] + }, + "Identifier": { + "description": "Permission identifier", + "oneOf": [ + { + "description": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`", + "type": "string", + "const": "core:default", + "markdownDescription": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`" + }, + { + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-supports-multiple-windows`", + "type": "string", + "const": "core:app:default", + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-supports-multiple-windows`" + }, + { + "description": "Enables the app_hide command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-app-hide", + "markdownDescription": "Enables the app_hide command without any pre-configured scope." + }, + { + "description": "Enables the app_show command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-app-show", + "markdownDescription": "Enables the app_show command without any pre-configured scope." + }, + { + "description": "Enables the bundle_type command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-bundle-type", + "markdownDescription": "Enables the bundle_type command without any pre-configured scope." + }, + { + "description": "Enables the default_window_icon command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-default-window-icon", + "markdownDescription": "Enables the default_window_icon command without any pre-configured scope." + }, + { + "description": "Enables the exit command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-exit", + "markdownDescription": "Enables the exit command without any pre-configured scope." + }, + { + "description": "Enables the fetch_data_store_identifiers command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-fetch-data-store-identifiers", + "markdownDescription": "Enables the fetch_data_store_identifiers command without any pre-configured scope." + }, + { + "description": "Enables the identifier command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-identifier", + "markdownDescription": "Enables the identifier command without any pre-configured scope." + }, + { + "description": "Enables the name command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-name", + "markdownDescription": "Enables the name command without any pre-configured scope." + }, + { + "description": "Enables the register_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-register-listener", + "markdownDescription": "Enables the register_listener command without any pre-configured scope." + }, + { + "description": "Enables the remove_data_store command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-remove-data-store", + "markdownDescription": "Enables the remove_data_store command without any pre-configured scope." + }, + { + "description": "Enables the remove_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-remove-listener", + "markdownDescription": "Enables the remove_listener command without any pre-configured scope." + }, + { + "description": "Enables the set_app_theme command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-set-app-theme", + "markdownDescription": "Enables the set_app_theme command without any pre-configured scope." + }, + { + "description": "Enables the set_dock_visibility command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-set-dock-visibility", + "markdownDescription": "Enables the set_dock_visibility command without any pre-configured scope." + }, + { + "description": "Enables the supports_multiple_windows command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-supports-multiple-windows", + "markdownDescription": "Enables the supports_multiple_windows command without any pre-configured scope." + }, + { + "description": "Enables the tauri_version command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-tauri-version", + "markdownDescription": "Enables the tauri_version command without any pre-configured scope." + }, + { + "description": "Enables the version command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-version", + "markdownDescription": "Enables the version command without any pre-configured scope." + }, + { + "description": "Denies the app_hide command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-app-hide", + "markdownDescription": "Denies the app_hide command without any pre-configured scope." + }, + { + "description": "Denies the app_show command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-app-show", + "markdownDescription": "Denies the app_show command without any pre-configured scope." + }, + { + "description": "Denies the bundle_type command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-bundle-type", + "markdownDescription": "Denies the bundle_type command without any pre-configured scope." + }, + { + "description": "Denies the default_window_icon command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-default-window-icon", + "markdownDescription": "Denies the default_window_icon command without any pre-configured scope." + }, + { + "description": "Denies the exit command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-exit", + "markdownDescription": "Denies the exit command without any pre-configured scope." + }, + { + "description": "Denies the fetch_data_store_identifiers command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-fetch-data-store-identifiers", + "markdownDescription": "Denies the fetch_data_store_identifiers command without any pre-configured scope." + }, + { + "description": "Denies the identifier command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-identifier", + "markdownDescription": "Denies the identifier command without any pre-configured scope." + }, + { + "description": "Denies the name command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-name", + "markdownDescription": "Denies the name command without any pre-configured scope." + }, + { + "description": "Denies the register_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-register-listener", + "markdownDescription": "Denies the register_listener command without any pre-configured scope." + }, + { + "description": "Denies the remove_data_store command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-remove-data-store", + "markdownDescription": "Denies the remove_data_store command without any pre-configured scope." + }, + { + "description": "Denies the remove_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-remove-listener", + "markdownDescription": "Denies the remove_listener command without any pre-configured scope." + }, + { + "description": "Denies the set_app_theme command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-set-app-theme", + "markdownDescription": "Denies the set_app_theme command without any pre-configured scope." + }, + { + "description": "Denies the set_dock_visibility command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-set-dock-visibility", + "markdownDescription": "Denies the set_dock_visibility command without any pre-configured scope." + }, + { + "description": "Denies the supports_multiple_windows command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-supports-multiple-windows", + "markdownDescription": "Denies the supports_multiple_windows command without any pre-configured scope." + }, + { + "description": "Denies the tauri_version command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-tauri-version", + "markdownDescription": "Denies the tauri_version command without any pre-configured scope." + }, + { + "description": "Denies the version command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-version", + "markdownDescription": "Denies the version command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-listen`\n- `allow-unlisten`\n- `allow-emit`\n- `allow-emit-to`", + "type": "string", + "const": "core:event:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-listen`\n- `allow-unlisten`\n- `allow-emit`\n- `allow-emit-to`" + }, + { + "description": "Enables the emit command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-emit", + "markdownDescription": "Enables the emit command without any pre-configured scope." + }, + { + "description": "Enables the emit_to command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-emit-to", + "markdownDescription": "Enables the emit_to command without any pre-configured scope." + }, + { + "description": "Enables the listen command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-listen", + "markdownDescription": "Enables the listen command without any pre-configured scope." + }, + { + "description": "Enables the unlisten command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-unlisten", + "markdownDescription": "Enables the unlisten command without any pre-configured scope." + }, + { + "description": "Denies the emit command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-emit", + "markdownDescription": "Denies the emit command without any pre-configured scope." + }, + { + "description": "Denies the emit_to command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-emit-to", + "markdownDescription": "Denies the emit_to command without any pre-configured scope." + }, + { + "description": "Denies the listen command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-listen", + "markdownDescription": "Denies the listen command without any pre-configured scope." + }, + { + "description": "Denies the unlisten command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-unlisten", + "markdownDescription": "Denies the unlisten command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-from-bytes`\n- `allow-from-path`\n- `allow-rgba`\n- `allow-size`", + "type": "string", + "const": "core:image:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-from-bytes`\n- `allow-from-path`\n- `allow-rgba`\n- `allow-size`" + }, + { + "description": "Enables the from_bytes command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-from-bytes", + "markdownDescription": "Enables the from_bytes command without any pre-configured scope." + }, + { + "description": "Enables the from_path command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-from-path", + "markdownDescription": "Enables the from_path command without any pre-configured scope." + }, + { + "description": "Enables the new command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-new", + "markdownDescription": "Enables the new command without any pre-configured scope." + }, + { + "description": "Enables the rgba command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-rgba", + "markdownDescription": "Enables the rgba command without any pre-configured scope." + }, + { + "description": "Enables the size command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-size", + "markdownDescription": "Enables the size command without any pre-configured scope." + }, + { + "description": "Denies the from_bytes command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-from-bytes", + "markdownDescription": "Denies the from_bytes command without any pre-configured scope." + }, + { + "description": "Denies the from_path command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-from-path", + "markdownDescription": "Denies the from_path command without any pre-configured scope." + }, + { + "description": "Denies the new command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-new", + "markdownDescription": "Denies the new command without any pre-configured scope." + }, + { + "description": "Denies the rgba command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-rgba", + "markdownDescription": "Denies the rgba command without any pre-configured scope." + }, + { + "description": "Denies the size command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-size", + "markdownDescription": "Denies the size command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-append`\n- `allow-prepend`\n- `allow-insert`\n- `allow-remove`\n- `allow-remove-at`\n- `allow-items`\n- `allow-get`\n- `allow-popup`\n- `allow-create-default`\n- `allow-set-as-app-menu`\n- `allow-set-as-window-menu`\n- `allow-text`\n- `allow-set-text`\n- `allow-is-enabled`\n- `allow-set-enabled`\n- `allow-set-accelerator`\n- `allow-set-as-windows-menu-for-nsapp`\n- `allow-set-as-help-menu-for-nsapp`\n- `allow-is-checked`\n- `allow-set-checked`\n- `allow-set-icon`", + "type": "string", + "const": "core:menu:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-append`\n- `allow-prepend`\n- `allow-insert`\n- `allow-remove`\n- `allow-remove-at`\n- `allow-items`\n- `allow-get`\n- `allow-popup`\n- `allow-create-default`\n- `allow-set-as-app-menu`\n- `allow-set-as-window-menu`\n- `allow-text`\n- `allow-set-text`\n- `allow-is-enabled`\n- `allow-set-enabled`\n- `allow-set-accelerator`\n- `allow-set-as-windows-menu-for-nsapp`\n- `allow-set-as-help-menu-for-nsapp`\n- `allow-is-checked`\n- `allow-set-checked`\n- `allow-set-icon`" + }, + { + "description": "Enables the append command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-append", + "markdownDescription": "Enables the append command without any pre-configured scope." + }, + { + "description": "Enables the create_default command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-create-default", + "markdownDescription": "Enables the create_default command without any pre-configured scope." + }, + { + "description": "Enables the get command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-get", + "markdownDescription": "Enables the get command without any pre-configured scope." + }, + { + "description": "Enables the insert command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-insert", + "markdownDescription": "Enables the insert command without any pre-configured scope." + }, + { + "description": "Enables the is_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-is-checked", + "markdownDescription": "Enables the is_checked command without any pre-configured scope." + }, + { + "description": "Enables the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-is-enabled", + "markdownDescription": "Enables the is_enabled command without any pre-configured scope." + }, + { + "description": "Enables the items command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-items", + "markdownDescription": "Enables the items command without any pre-configured scope." + }, + { + "description": "Enables the new command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-new", + "markdownDescription": "Enables the new command without any pre-configured scope." + }, + { + "description": "Enables the popup command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-popup", + "markdownDescription": "Enables the popup command without any pre-configured scope." + }, + { + "description": "Enables the prepend command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-prepend", + "markdownDescription": "Enables the prepend command without any pre-configured scope." + }, + { + "description": "Enables the remove command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-remove", + "markdownDescription": "Enables the remove command without any pre-configured scope." + }, + { + "description": "Enables the remove_at command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-remove-at", + "markdownDescription": "Enables the remove_at command without any pre-configured scope." + }, + { + "description": "Enables the set_accelerator command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-accelerator", + "markdownDescription": "Enables the set_accelerator command without any pre-configured scope." + }, + { + "description": "Enables the set_as_app_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-app-menu", + "markdownDescription": "Enables the set_as_app_menu command without any pre-configured scope." + }, + { + "description": "Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-help-menu-for-nsapp", + "markdownDescription": "Enables the set_as_help_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Enables the set_as_window_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-window-menu", + "markdownDescription": "Enables the set_as_window_menu command without any pre-configured scope." + }, + { + "description": "Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-windows-menu-for-nsapp", + "markdownDescription": "Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Enables the set_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-checked", + "markdownDescription": "Enables the set_checked command without any pre-configured scope." + }, + { + "description": "Enables the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-enabled", + "markdownDescription": "Enables the set_enabled command without any pre-configured scope." + }, + { + "description": "Enables the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-icon", + "markdownDescription": "Enables the set_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-text", + "markdownDescription": "Enables the set_text command without any pre-configured scope." + }, + { + "description": "Enables the text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-text", + "markdownDescription": "Enables the text command without any pre-configured scope." + }, + { + "description": "Denies the append command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-append", + "markdownDescription": "Denies the append command without any pre-configured scope." + }, + { + "description": "Denies the create_default command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-create-default", + "markdownDescription": "Denies the create_default command without any pre-configured scope." + }, + { + "description": "Denies the get command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-get", + "markdownDescription": "Denies the get command without any pre-configured scope." + }, + { + "description": "Denies the insert command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-insert", + "markdownDescription": "Denies the insert command without any pre-configured scope." + }, + { + "description": "Denies the is_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-is-checked", + "markdownDescription": "Denies the is_checked command without any pre-configured scope." + }, + { + "description": "Denies the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-is-enabled", + "markdownDescription": "Denies the is_enabled command without any pre-configured scope." + }, + { + "description": "Denies the items command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-items", + "markdownDescription": "Denies the items command without any pre-configured scope." + }, + { + "description": "Denies the new command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-new", + "markdownDescription": "Denies the new command without any pre-configured scope." + }, + { + "description": "Denies the popup command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-popup", + "markdownDescription": "Denies the popup command without any pre-configured scope." + }, + { + "description": "Denies the prepend command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-prepend", + "markdownDescription": "Denies the prepend command without any pre-configured scope." + }, + { + "description": "Denies the remove command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-remove", + "markdownDescription": "Denies the remove command without any pre-configured scope." + }, + { + "description": "Denies the remove_at command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-remove-at", + "markdownDescription": "Denies the remove_at command without any pre-configured scope." + }, + { + "description": "Denies the set_accelerator command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-accelerator", + "markdownDescription": "Denies the set_accelerator command without any pre-configured scope." + }, + { + "description": "Denies the set_as_app_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-app-menu", + "markdownDescription": "Denies the set_as_app_menu command without any pre-configured scope." + }, + { + "description": "Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-help-menu-for-nsapp", + "markdownDescription": "Denies the set_as_help_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Denies the set_as_window_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-window-menu", + "markdownDescription": "Denies the set_as_window_menu command without any pre-configured scope." + }, + { + "description": "Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-windows-menu-for-nsapp", + "markdownDescription": "Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Denies the set_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-checked", + "markdownDescription": "Denies the set_checked command without any pre-configured scope." + }, + { + "description": "Denies the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-enabled", + "markdownDescription": "Denies the set_enabled command without any pre-configured scope." + }, + { + "description": "Denies the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-icon", + "markdownDescription": "Denies the set_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-text", + "markdownDescription": "Denies the set_text command without any pre-configured scope." + }, + { + "description": "Denies the text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-text", + "markdownDescription": "Denies the text command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-resolve-directory`\n- `allow-resolve`\n- `allow-normalize`\n- `allow-join`\n- `allow-dirname`\n- `allow-extname`\n- `allow-basename`\n- `allow-is-absolute`", + "type": "string", + "const": "core:path:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-resolve-directory`\n- `allow-resolve`\n- `allow-normalize`\n- `allow-join`\n- `allow-dirname`\n- `allow-extname`\n- `allow-basename`\n- `allow-is-absolute`" + }, + { + "description": "Enables the basename command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-basename", + "markdownDescription": "Enables the basename command without any pre-configured scope." + }, + { + "description": "Enables the dirname command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-dirname", + "markdownDescription": "Enables the dirname command without any pre-configured scope." + }, + { + "description": "Enables the extname command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-extname", + "markdownDescription": "Enables the extname command without any pre-configured scope." + }, + { + "description": "Enables the is_absolute command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-is-absolute", + "markdownDescription": "Enables the is_absolute command without any pre-configured scope." + }, + { + "description": "Enables the join command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-join", + "markdownDescription": "Enables the join command without any pre-configured scope." + }, + { + "description": "Enables the normalize command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-normalize", + "markdownDescription": "Enables the normalize command without any pre-configured scope." + }, + { + "description": "Enables the resolve command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-resolve", + "markdownDescription": "Enables the resolve command without any pre-configured scope." + }, + { + "description": "Enables the resolve_directory command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-resolve-directory", + "markdownDescription": "Enables the resolve_directory command without any pre-configured scope." + }, + { + "description": "Denies the basename command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-basename", + "markdownDescription": "Denies the basename command without any pre-configured scope." + }, + { + "description": "Denies the dirname command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-dirname", + "markdownDescription": "Denies the dirname command without any pre-configured scope." + }, + { + "description": "Denies the extname command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-extname", + "markdownDescription": "Denies the extname command without any pre-configured scope." + }, + { + "description": "Denies the is_absolute command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-is-absolute", + "markdownDescription": "Denies the is_absolute command without any pre-configured scope." + }, + { + "description": "Denies the join command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-join", + "markdownDescription": "Denies the join command without any pre-configured scope." + }, + { + "description": "Denies the normalize command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-normalize", + "markdownDescription": "Denies the normalize command without any pre-configured scope." + }, + { + "description": "Denies the resolve command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-resolve", + "markdownDescription": "Denies the resolve command without any pre-configured scope." + }, + { + "description": "Denies the resolve_directory command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-resolve-directory", + "markdownDescription": "Denies the resolve_directory command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-close`", + "type": "string", + "const": "core:resources:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-close`" + }, + { + "description": "Enables the close command without any pre-configured scope.", + "type": "string", + "const": "core:resources:allow-close", + "markdownDescription": "Enables the close command without any pre-configured scope." + }, + { + "description": "Denies the close command without any pre-configured scope.", + "type": "string", + "const": "core:resources:deny-close", + "markdownDescription": "Denies the close command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-icon-with-as-template`\n- `allow-set-show-menu-on-left-click`", + "type": "string", + "const": "core:tray:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-icon-with-as-template`\n- `allow-set-show-menu-on-left-click`" + }, + { + "description": "Enables the get_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-get-by-id", + "markdownDescription": "Enables the get_by_id command without any pre-configured scope." + }, + { + "description": "Enables the new command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-new", + "markdownDescription": "Enables the new command without any pre-configured scope." + }, + { + "description": "Enables the remove_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-remove-by-id", + "markdownDescription": "Enables the remove_by_id command without any pre-configured scope." + }, + { + "description": "Enables the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-icon", + "markdownDescription": "Enables the set_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_icon_as_template command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-icon-as-template", + "markdownDescription": "Enables the set_icon_as_template command without any pre-configured scope." + }, + { + "description": "Enables the set_icon_with_as_template command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-icon-with-as-template", + "markdownDescription": "Enables the set_icon_with_as_template command without any pre-configured scope." + }, + { + "description": "Enables the set_menu command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-menu", + "markdownDescription": "Enables the set_menu command without any pre-configured scope." + }, + { + "description": "Enables the set_show_menu_on_left_click command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-show-menu-on-left-click", + "markdownDescription": "Enables the set_show_menu_on_left_click command without any pre-configured scope." + }, + { + "description": "Enables the set_temp_dir_path command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-temp-dir-path", + "markdownDescription": "Enables the set_temp_dir_path command without any pre-configured scope." + }, + { + "description": "Enables the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-title", + "markdownDescription": "Enables the set_title command without any pre-configured scope." + }, + { + "description": "Enables the set_tooltip command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-tooltip", + "markdownDescription": "Enables the set_tooltip command without any pre-configured scope." + }, + { + "description": "Enables the set_visible command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-visible", + "markdownDescription": "Enables the set_visible command without any pre-configured scope." + }, + { + "description": "Denies the get_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-get-by-id", + "markdownDescription": "Denies the get_by_id command without any pre-configured scope." + }, + { + "description": "Denies the new command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-new", + "markdownDescription": "Denies the new command without any pre-configured scope." + }, + { + "description": "Denies the remove_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-remove-by-id", + "markdownDescription": "Denies the remove_by_id command without any pre-configured scope." + }, + { + "description": "Denies the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-icon", + "markdownDescription": "Denies the set_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_icon_as_template command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-icon-as-template", + "markdownDescription": "Denies the set_icon_as_template command without any pre-configured scope." + }, + { + "description": "Denies the set_icon_with_as_template command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-icon-with-as-template", + "markdownDescription": "Denies the set_icon_with_as_template command without any pre-configured scope." + }, + { + "description": "Denies the set_menu command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-menu", + "markdownDescription": "Denies the set_menu command without any pre-configured scope." + }, + { + "description": "Denies the set_show_menu_on_left_click command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-show-menu-on-left-click", + "markdownDescription": "Denies the set_show_menu_on_left_click command without any pre-configured scope." + }, + { + "description": "Denies the set_temp_dir_path command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-temp-dir-path", + "markdownDescription": "Denies the set_temp_dir_path command without any pre-configured scope." + }, + { + "description": "Denies the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-title", + "markdownDescription": "Denies the set_title command without any pre-configured scope." + }, + { + "description": "Denies the set_tooltip command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-tooltip", + "markdownDescription": "Denies the set_tooltip command without any pre-configured scope." + }, + { + "description": "Denies the set_visible command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-visible", + "markdownDescription": "Denies the set_visible command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-webviews`\n- `allow-webview-position`\n- `allow-webview-size`\n- `allow-internal-toggle-devtools`", + "type": "string", + "const": "core:webview:default", + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-webviews`\n- `allow-webview-position`\n- `allow-webview-size`\n- `allow-internal-toggle-devtools`" + }, + { + "description": "Enables the clear_all_browsing_data command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-clear-all-browsing-data", + "markdownDescription": "Enables the clear_all_browsing_data command without any pre-configured scope." + }, + { + "description": "Enables the create_webview command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-create-webview", + "markdownDescription": "Enables the create_webview command without any pre-configured scope." + }, + { + "description": "Enables the create_webview_window command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-create-webview-window", + "markdownDescription": "Enables the create_webview_window command without any pre-configured scope." + }, + { + "description": "Enables the get_all_webviews command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-get-all-webviews", + "markdownDescription": "Enables the get_all_webviews command without any pre-configured scope." + }, + { + "description": "Enables the internal_toggle_devtools command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-internal-toggle-devtools", + "markdownDescription": "Enables the internal_toggle_devtools command without any pre-configured scope." + }, + { + "description": "Enables the print command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-print", + "markdownDescription": "Enables the print command without any pre-configured scope." + }, + { + "description": "Enables the reparent command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-reparent", + "markdownDescription": "Enables the reparent command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_auto_resize command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-auto-resize", + "markdownDescription": "Enables the set_webview_auto_resize command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-background-color", + "markdownDescription": "Enables the set_webview_background_color command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_focus command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-focus", + "markdownDescription": "Enables the set_webview_focus command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-position", + "markdownDescription": "Enables the set_webview_position command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-size", + "markdownDescription": "Enables the set_webview_size command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_zoom command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-zoom", + "markdownDescription": "Enables the set_webview_zoom command without any pre-configured scope." + }, + { + "description": "Enables the webview_close command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-close", + "markdownDescription": "Enables the webview_close command without any pre-configured scope." + }, + { + "description": "Enables the webview_hide command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-hide", + "markdownDescription": "Enables the webview_hide command without any pre-configured scope." + }, + { + "description": "Enables the webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-position", + "markdownDescription": "Enables the webview_position command without any pre-configured scope." + }, + { + "description": "Enables the webview_show command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-show", + "markdownDescription": "Enables the webview_show command without any pre-configured scope." + }, + { + "description": "Enables the webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-size", + "markdownDescription": "Enables the webview_size command without any pre-configured scope." + }, + { + "description": "Denies the clear_all_browsing_data command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-clear-all-browsing-data", + "markdownDescription": "Denies the clear_all_browsing_data command without any pre-configured scope." + }, + { + "description": "Denies the create_webview command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-create-webview", + "markdownDescription": "Denies the create_webview command without any pre-configured scope." + }, + { + "description": "Denies the create_webview_window command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-create-webview-window", + "markdownDescription": "Denies the create_webview_window command without any pre-configured scope." + }, + { + "description": "Denies the get_all_webviews command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-get-all-webviews", + "markdownDescription": "Denies the get_all_webviews command without any pre-configured scope." + }, + { + "description": "Denies the internal_toggle_devtools command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-internal-toggle-devtools", + "markdownDescription": "Denies the internal_toggle_devtools command without any pre-configured scope." + }, + { + "description": "Denies the print command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-print", + "markdownDescription": "Denies the print command without any pre-configured scope." + }, + { + "description": "Denies the reparent command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-reparent", + "markdownDescription": "Denies the reparent command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_auto_resize command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-auto-resize", + "markdownDescription": "Denies the set_webview_auto_resize command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-background-color", + "markdownDescription": "Denies the set_webview_background_color command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_focus command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-focus", + "markdownDescription": "Denies the set_webview_focus command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-position", + "markdownDescription": "Denies the set_webview_position command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-size", + "markdownDescription": "Denies the set_webview_size command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_zoom command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-zoom", + "markdownDescription": "Denies the set_webview_zoom command without any pre-configured scope." + }, + { + "description": "Denies the webview_close command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-close", + "markdownDescription": "Denies the webview_close command without any pre-configured scope." + }, + { + "description": "Denies the webview_hide command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-hide", + "markdownDescription": "Denies the webview_hide command without any pre-configured scope." + }, + { + "description": "Denies the webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-position", + "markdownDescription": "Denies the webview_position command without any pre-configured scope." + }, + { + "description": "Denies the webview_show command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-show", + "markdownDescription": "Denies the webview_show command without any pre-configured scope." + }, + { + "description": "Denies the webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-size", + "markdownDescription": "Denies the webview_size command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-activity-name`\n- `allow-scene-identifier`\n- `allow-internal-toggle-maximize`", + "type": "string", + "const": "core:window:default", + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-activity-name`\n- `allow-scene-identifier`\n- `allow-internal-toggle-maximize`" + }, + { + "description": "Enables the activity_name command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-activity-name", + "markdownDescription": "Enables the activity_name command without any pre-configured scope." + }, + { + "description": "Enables the available_monitors command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-available-monitors", + "markdownDescription": "Enables the available_monitors command without any pre-configured scope." + }, + { + "description": "Enables the center command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-center", + "markdownDescription": "Enables the center command without any pre-configured scope." + }, + { + "description": "Enables the close command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-close", + "markdownDescription": "Enables the close command without any pre-configured scope." + }, + { + "description": "Enables the create command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-create", + "markdownDescription": "Enables the create command without any pre-configured scope." + }, + { + "description": "Enables the current_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-current-monitor", + "markdownDescription": "Enables the current_monitor command without any pre-configured scope." + }, + { + "description": "Enables the cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-cursor-position", + "markdownDescription": "Enables the cursor_position command without any pre-configured scope." + }, + { + "description": "Enables the destroy command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-destroy", + "markdownDescription": "Enables the destroy command without any pre-configured scope." + }, + { + "description": "Enables the get_all_windows command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-get-all-windows", + "markdownDescription": "Enables the get_all_windows command without any pre-configured scope." + }, + { + "description": "Enables the hide command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-hide", + "markdownDescription": "Enables the hide command without any pre-configured scope." + }, + { + "description": "Enables the inner_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-inner-position", + "markdownDescription": "Enables the inner_position command without any pre-configured scope." + }, + { + "description": "Enables the inner_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-inner-size", + "markdownDescription": "Enables the inner_size command without any pre-configured scope." + }, + { + "description": "Enables the internal_toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-internal-toggle-maximize", + "markdownDescription": "Enables the internal_toggle_maximize command without any pre-configured scope." + }, + { + "description": "Enables the is_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-always-on-top", + "markdownDescription": "Enables the is_always_on_top command without any pre-configured scope." + }, + { + "description": "Enables the is_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-closable", + "markdownDescription": "Enables the is_closable command without any pre-configured scope." + }, + { + "description": "Enables the is_decorated command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-decorated", + "markdownDescription": "Enables the is_decorated command without any pre-configured scope." + }, + { + "description": "Enables the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-enabled", + "markdownDescription": "Enables the is_enabled command without any pre-configured scope." + }, + { + "description": "Enables the is_focused command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-focused", + "markdownDescription": "Enables the is_focused command without any pre-configured scope." + }, + { + "description": "Enables the is_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-fullscreen", + "markdownDescription": "Enables the is_fullscreen command without any pre-configured scope." + }, + { + "description": "Enables the is_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-maximizable", + "markdownDescription": "Enables the is_maximizable command without any pre-configured scope." + }, + { + "description": "Enables the is_maximized command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-maximized", + "markdownDescription": "Enables the is_maximized command without any pre-configured scope." + }, + { + "description": "Enables the is_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-minimizable", + "markdownDescription": "Enables the is_minimizable command without any pre-configured scope." + }, + { + "description": "Enables the is_minimized command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-minimized", + "markdownDescription": "Enables the is_minimized command without any pre-configured scope." + }, + { + "description": "Enables the is_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-resizable", + "markdownDescription": "Enables the is_resizable command without any pre-configured scope." + }, + { + "description": "Enables the is_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-visible", + "markdownDescription": "Enables the is_visible command without any pre-configured scope." + }, + { + "description": "Enables the maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-maximize", + "markdownDescription": "Enables the maximize command without any pre-configured scope." + }, + { + "description": "Enables the minimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-minimize", + "markdownDescription": "Enables the minimize command without any pre-configured scope." + }, + { + "description": "Enables the monitor_from_point command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-monitor-from-point", + "markdownDescription": "Enables the monitor_from_point command without any pre-configured scope." + }, + { + "description": "Enables the outer_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-outer-position", + "markdownDescription": "Enables the outer_position command without any pre-configured scope." + }, + { + "description": "Enables the outer_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-outer-size", + "markdownDescription": "Enables the outer_size command without any pre-configured scope." + }, + { + "description": "Enables the primary_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-primary-monitor", + "markdownDescription": "Enables the primary_monitor command without any pre-configured scope." + }, + { + "description": "Enables the request_user_attention command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-request-user-attention", + "markdownDescription": "Enables the request_user_attention command without any pre-configured scope." + }, + { + "description": "Enables the scale_factor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-scale-factor", + "markdownDescription": "Enables the scale_factor command without any pre-configured scope." + }, + { + "description": "Enables the scene_identifier command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-scene-identifier", + "markdownDescription": "Enables the scene_identifier command without any pre-configured scope." + }, + { + "description": "Enables the set_always_on_bottom command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-always-on-bottom", + "markdownDescription": "Enables the set_always_on_bottom command without any pre-configured scope." + }, + { + "description": "Enables the set_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-always-on-top", + "markdownDescription": "Enables the set_always_on_top command without any pre-configured scope." + }, + { + "description": "Enables the set_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-background-color", + "markdownDescription": "Enables the set_background_color command without any pre-configured scope." + }, + { + "description": "Enables the set_badge_count command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-badge-count", + "markdownDescription": "Enables the set_badge_count command without any pre-configured scope." + }, + { + "description": "Enables the set_badge_label command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-badge-label", + "markdownDescription": "Enables the set_badge_label command without any pre-configured scope." + }, + { + "description": "Enables the set_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-closable", + "markdownDescription": "Enables the set_closable command without any pre-configured scope." + }, + { + "description": "Enables the set_content_protected command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-content-protected", + "markdownDescription": "Enables the set_content_protected command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_grab command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-grab", + "markdownDescription": "Enables the set_cursor_grab command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-icon", + "markdownDescription": "Enables the set_cursor_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-position", + "markdownDescription": "Enables the set_cursor_position command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-visible", + "markdownDescription": "Enables the set_cursor_visible command without any pre-configured scope." + }, + { + "description": "Enables the set_decorations command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-decorations", + "markdownDescription": "Enables the set_decorations command without any pre-configured scope." + }, + { + "description": "Enables the set_effects command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-effects", + "markdownDescription": "Enables the set_effects command without any pre-configured scope." + }, + { + "description": "Enables the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-enabled", + "markdownDescription": "Enables the set_enabled command without any pre-configured scope." + }, + { + "description": "Enables the set_focus command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-focus", + "markdownDescription": "Enables the set_focus command without any pre-configured scope." + }, + { + "description": "Enables the set_focusable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-focusable", + "markdownDescription": "Enables the set_focusable command without any pre-configured scope." + }, + { + "description": "Enables the set_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-fullscreen", + "markdownDescription": "Enables the set_fullscreen command without any pre-configured scope." + }, + { + "description": "Enables the set_fullscreen_on_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-fullscreen-on-monitor", + "markdownDescription": "Enables the set_fullscreen_on_monitor command without any pre-configured scope." + }, + { + "description": "Enables the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-icon", + "markdownDescription": "Enables the set_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_ignore_cursor_events command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-ignore-cursor-events", + "markdownDescription": "Enables the set_ignore_cursor_events command without any pre-configured scope." + }, + { + "description": "Enables the set_max_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-max-size", + "markdownDescription": "Enables the set_max_size command without any pre-configured scope." + }, + { + "description": "Enables the set_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-maximizable", + "markdownDescription": "Enables the set_maximizable command without any pre-configured scope." + }, + { + "description": "Enables the set_min_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-min-size", + "markdownDescription": "Enables the set_min_size command without any pre-configured scope." + }, + { + "description": "Enables the set_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-minimizable", + "markdownDescription": "Enables the set_minimizable command without any pre-configured scope." + }, + { + "description": "Enables the set_overlay_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-overlay-icon", + "markdownDescription": "Enables the set_overlay_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-position", + "markdownDescription": "Enables the set_position command without any pre-configured scope." + }, + { + "description": "Enables the set_progress_bar command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-progress-bar", + "markdownDescription": "Enables the set_progress_bar command without any pre-configured scope." + }, + { + "description": "Enables the set_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-resizable", + "markdownDescription": "Enables the set_resizable command without any pre-configured scope." + }, + { + "description": "Enables the set_shadow command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-shadow", + "markdownDescription": "Enables the set_shadow command without any pre-configured scope." + }, + { + "description": "Enables the set_simple_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-simple-fullscreen", + "markdownDescription": "Enables the set_simple_fullscreen command without any pre-configured scope." + }, + { + "description": "Enables the set_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-size", + "markdownDescription": "Enables the set_size command without any pre-configured scope." + }, + { + "description": "Enables the set_size_constraints command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-size-constraints", + "markdownDescription": "Enables the set_size_constraints command without any pre-configured scope." + }, + { + "description": "Enables the set_skip_taskbar command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-skip-taskbar", + "markdownDescription": "Enables the set_skip_taskbar command without any pre-configured scope." + }, + { + "description": "Enables the set_theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-theme", + "markdownDescription": "Enables the set_theme command without any pre-configured scope." + }, + { + "description": "Enables the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-title", + "markdownDescription": "Enables the set_title command without any pre-configured scope." + }, + { + "description": "Enables the set_title_bar_style command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-title-bar-style", + "markdownDescription": "Enables the set_title_bar_style command without any pre-configured scope." + }, + { + "description": "Enables the set_visible_on_all_workspaces command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-visible-on-all-workspaces", + "markdownDescription": "Enables the set_visible_on_all_workspaces command without any pre-configured scope." + }, + { + "description": "Enables the show command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-show", + "markdownDescription": "Enables the show command without any pre-configured scope." + }, + { + "description": "Enables the start_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-start-dragging", + "markdownDescription": "Enables the start_dragging command without any pre-configured scope." + }, + { + "description": "Enables the start_resize_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-start-resize-dragging", + "markdownDescription": "Enables the start_resize_dragging command without any pre-configured scope." + }, + { + "description": "Enables the theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-theme", + "markdownDescription": "Enables the theme command without any pre-configured scope." + }, + { + "description": "Enables the title command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-title", + "markdownDescription": "Enables the title command without any pre-configured scope." + }, + { + "description": "Enables the toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-toggle-maximize", + "markdownDescription": "Enables the toggle_maximize command without any pre-configured scope." + }, + { + "description": "Enables the unmaximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-unmaximize", + "markdownDescription": "Enables the unmaximize command without any pre-configured scope." + }, + { + "description": "Enables the unminimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-unminimize", + "markdownDescription": "Enables the unminimize command without any pre-configured scope." + }, + { + "description": "Denies the activity_name command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-activity-name", + "markdownDescription": "Denies the activity_name command without any pre-configured scope." + }, + { + "description": "Denies the available_monitors command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-available-monitors", + "markdownDescription": "Denies the available_monitors command without any pre-configured scope." + }, + { + "description": "Denies the center command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-center", + "markdownDescription": "Denies the center command without any pre-configured scope." + }, + { + "description": "Denies the close command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-close", + "markdownDescription": "Denies the close command without any pre-configured scope." + }, + { + "description": "Denies the create command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-create", + "markdownDescription": "Denies the create command without any pre-configured scope." + }, + { + "description": "Denies the current_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-current-monitor", + "markdownDescription": "Denies the current_monitor command without any pre-configured scope." + }, + { + "description": "Denies the cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-cursor-position", + "markdownDescription": "Denies the cursor_position command without any pre-configured scope." + }, + { + "description": "Denies the destroy command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-destroy", + "markdownDescription": "Denies the destroy command without any pre-configured scope." + }, + { + "description": "Denies the get_all_windows command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-get-all-windows", + "markdownDescription": "Denies the get_all_windows command without any pre-configured scope." + }, + { + "description": "Denies the hide command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-hide", + "markdownDescription": "Denies the hide command without any pre-configured scope." + }, + { + "description": "Denies the inner_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-inner-position", + "markdownDescription": "Denies the inner_position command without any pre-configured scope." + }, + { + "description": "Denies the inner_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-inner-size", + "markdownDescription": "Denies the inner_size command without any pre-configured scope." + }, + { + "description": "Denies the internal_toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-internal-toggle-maximize", + "markdownDescription": "Denies the internal_toggle_maximize command without any pre-configured scope." + }, + { + "description": "Denies the is_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-always-on-top", + "markdownDescription": "Denies the is_always_on_top command without any pre-configured scope." + }, + { + "description": "Denies the is_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-closable", + "markdownDescription": "Denies the is_closable command without any pre-configured scope." + }, + { + "description": "Denies the is_decorated command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-decorated", + "markdownDescription": "Denies the is_decorated command without any pre-configured scope." + }, + { + "description": "Denies the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-enabled", + "markdownDescription": "Denies the is_enabled command without any pre-configured scope." + }, + { + "description": "Denies the is_focused command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-focused", + "markdownDescription": "Denies the is_focused command without any pre-configured scope." + }, + { + "description": "Denies the is_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-fullscreen", + "markdownDescription": "Denies the is_fullscreen command without any pre-configured scope." + }, + { + "description": "Denies the is_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-maximizable", + "markdownDescription": "Denies the is_maximizable command without any pre-configured scope." + }, + { + "description": "Denies the is_maximized command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-maximized", + "markdownDescription": "Denies the is_maximized command without any pre-configured scope." + }, + { + "description": "Denies the is_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-minimizable", + "markdownDescription": "Denies the is_minimizable command without any pre-configured scope." + }, + { + "description": "Denies the is_minimized command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-minimized", + "markdownDescription": "Denies the is_minimized command without any pre-configured scope." + }, + { + "description": "Denies the is_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-resizable", + "markdownDescription": "Denies the is_resizable command without any pre-configured scope." + }, + { + "description": "Denies the is_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-visible", + "markdownDescription": "Denies the is_visible command without any pre-configured scope." + }, + { + "description": "Denies the maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-maximize", + "markdownDescription": "Denies the maximize command without any pre-configured scope." + }, + { + "description": "Denies the minimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-minimize", + "markdownDescription": "Denies the minimize command without any pre-configured scope." + }, + { + "description": "Denies the monitor_from_point command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-monitor-from-point", + "markdownDescription": "Denies the monitor_from_point command without any pre-configured scope." + }, + { + "description": "Denies the outer_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-outer-position", + "markdownDescription": "Denies the outer_position command without any pre-configured scope." + }, + { + "description": "Denies the outer_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-outer-size", + "markdownDescription": "Denies the outer_size command without any pre-configured scope." + }, + { + "description": "Denies the primary_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-primary-monitor", + "markdownDescription": "Denies the primary_monitor command without any pre-configured scope." + }, + { + "description": "Denies the request_user_attention command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-request-user-attention", + "markdownDescription": "Denies the request_user_attention command without any pre-configured scope." + }, + { + "description": "Denies the scale_factor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-scale-factor", + "markdownDescription": "Denies the scale_factor command without any pre-configured scope." + }, + { + "description": "Denies the scene_identifier command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-scene-identifier", + "markdownDescription": "Denies the scene_identifier command without any pre-configured scope." + }, + { + "description": "Denies the set_always_on_bottom command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-always-on-bottom", + "markdownDescription": "Denies the set_always_on_bottom command without any pre-configured scope." + }, + { + "description": "Denies the set_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-always-on-top", + "markdownDescription": "Denies the set_always_on_top command without any pre-configured scope." + }, + { + "description": "Denies the set_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-background-color", + "markdownDescription": "Denies the set_background_color command without any pre-configured scope." + }, + { + "description": "Denies the set_badge_count command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-badge-count", + "markdownDescription": "Denies the set_badge_count command without any pre-configured scope." + }, + { + "description": "Denies the set_badge_label command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-badge-label", + "markdownDescription": "Denies the set_badge_label command without any pre-configured scope." + }, + { + "description": "Denies the set_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-closable", + "markdownDescription": "Denies the set_closable command without any pre-configured scope." + }, + { + "description": "Denies the set_content_protected command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-content-protected", + "markdownDescription": "Denies the set_content_protected command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_grab command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-grab", + "markdownDescription": "Denies the set_cursor_grab command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-icon", + "markdownDescription": "Denies the set_cursor_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-position", + "markdownDescription": "Denies the set_cursor_position command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-visible", + "markdownDescription": "Denies the set_cursor_visible command without any pre-configured scope." + }, + { + "description": "Denies the set_decorations command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-decorations", + "markdownDescription": "Denies the set_decorations command without any pre-configured scope." + }, + { + "description": "Denies the set_effects command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-effects", + "markdownDescription": "Denies the set_effects command without any pre-configured scope." + }, + { + "description": "Denies the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-enabled", + "markdownDescription": "Denies the set_enabled command without any pre-configured scope." + }, + { + "description": "Denies the set_focus command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-focus", + "markdownDescription": "Denies the set_focus command without any pre-configured scope." + }, + { + "description": "Denies the set_focusable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-focusable", + "markdownDescription": "Denies the set_focusable command without any pre-configured scope." + }, + { + "description": "Denies the set_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-fullscreen", + "markdownDescription": "Denies the set_fullscreen command without any pre-configured scope." + }, + { + "description": "Denies the set_fullscreen_on_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-fullscreen-on-monitor", + "markdownDescription": "Denies the set_fullscreen_on_monitor command without any pre-configured scope." + }, + { + "description": "Denies the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-icon", + "markdownDescription": "Denies the set_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_ignore_cursor_events command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-ignore-cursor-events", + "markdownDescription": "Denies the set_ignore_cursor_events command without any pre-configured scope." + }, + { + "description": "Denies the set_max_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-max-size", + "markdownDescription": "Denies the set_max_size command without any pre-configured scope." + }, + { + "description": "Denies the set_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-maximizable", + "markdownDescription": "Denies the set_maximizable command without any pre-configured scope." + }, + { + "description": "Denies the set_min_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-min-size", + "markdownDescription": "Denies the set_min_size command without any pre-configured scope." + }, + { + "description": "Denies the set_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-minimizable", + "markdownDescription": "Denies the set_minimizable command without any pre-configured scope." + }, + { + "description": "Denies the set_overlay_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-overlay-icon", + "markdownDescription": "Denies the set_overlay_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-position", + "markdownDescription": "Denies the set_position command without any pre-configured scope." + }, + { + "description": "Denies the set_progress_bar command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-progress-bar", + "markdownDescription": "Denies the set_progress_bar command without any pre-configured scope." + }, + { + "description": "Denies the set_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-resizable", + "markdownDescription": "Denies the set_resizable command without any pre-configured scope." + }, + { + "description": "Denies the set_shadow command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-shadow", + "markdownDescription": "Denies the set_shadow command without any pre-configured scope." + }, + { + "description": "Denies the set_simple_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-simple-fullscreen", + "markdownDescription": "Denies the set_simple_fullscreen command without any pre-configured scope." + }, + { + "description": "Denies the set_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-size", + "markdownDescription": "Denies the set_size command without any pre-configured scope." + }, + { + "description": "Denies the set_size_constraints command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-size-constraints", + "markdownDescription": "Denies the set_size_constraints command without any pre-configured scope." + }, + { + "description": "Denies the set_skip_taskbar command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-skip-taskbar", + "markdownDescription": "Denies the set_skip_taskbar command without any pre-configured scope." + }, + { + "description": "Denies the set_theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-theme", + "markdownDescription": "Denies the set_theme command without any pre-configured scope." + }, + { + "description": "Denies the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-title", + "markdownDescription": "Denies the set_title command without any pre-configured scope." + }, + { + "description": "Denies the set_title_bar_style command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-title-bar-style", + "markdownDescription": "Denies the set_title_bar_style command without any pre-configured scope." + }, + { + "description": "Denies the set_visible_on_all_workspaces command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-visible-on-all-workspaces", + "markdownDescription": "Denies the set_visible_on_all_workspaces command without any pre-configured scope." + }, + { + "description": "Denies the show command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-show", + "markdownDescription": "Denies the show command without any pre-configured scope." + }, + { + "description": "Denies the start_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-start-dragging", + "markdownDescription": "Denies the start_dragging command without any pre-configured scope." + }, + { + "description": "Denies the start_resize_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-start-resize-dragging", + "markdownDescription": "Denies the start_resize_dragging command without any pre-configured scope." + }, + { + "description": "Denies the theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-theme", + "markdownDescription": "Denies the theme command without any pre-configured scope." + }, + { + "description": "Denies the title command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-title", + "markdownDescription": "Denies the title command without any pre-configured scope." + }, + { + "description": "Denies the toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-toggle-maximize", + "markdownDescription": "Denies the toggle_maximize command without any pre-configured scope." + }, + { + "description": "Denies the unmaximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-unmaximize", + "markdownDescription": "Denies the unmaximize command without any pre-configured scope." + }, + { + "description": "Denies the unminimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-unminimize", + "markdownDescription": "Denies the unminimize command without any pre-configured scope." + }, + { + "description": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`", + "type": "string", + "const": "dialog:default", + "markdownDescription": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`" + }, + { + "description": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)", + "type": "string", + "const": "dialog:allow-ask", + "markdownDescription": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)" + }, + { + "description": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)", + "type": "string", + "const": "dialog:allow-confirm", + "markdownDescription": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)" + }, + { + "description": "Enables the message command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-message", + "markdownDescription": "Enables the message command without any pre-configured scope." + }, + { + "description": "Enables the open command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-open", + "markdownDescription": "Enables the open command without any pre-configured scope." + }, + { + "description": "Enables the save command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-save", + "markdownDescription": "Enables the save command without any pre-configured scope." + }, + { + "description": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)", + "type": "string", + "const": "dialog:deny-ask", + "markdownDescription": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)" + }, + { + "description": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)", + "type": "string", + "const": "dialog:deny-confirm", + "markdownDescription": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)" + }, + { + "description": "Denies the message command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-message", + "markdownDescription": "Denies the message command without any pre-configured scope." + }, + { + "description": "Denies the open command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-open", + "markdownDescription": "Denies the open command without any pre-configured scope." + }, + { + "description": "Denies the save command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-save", + "markdownDescription": "Denies the save command without any pre-configured scope." + }, + { + "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\n#### This default permission set includes:\n\n- `allow-open`", + "type": "string", + "const": "shell:default", + "markdownDescription": "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\n#### This default permission set includes:\n\n- `allow-open`" + }, + { + "description": "Enables the execute command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-execute", + "markdownDescription": "Enables the execute command without any pre-configured scope." + }, + { + "description": "Enables the kill command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-kill", + "markdownDescription": "Enables the kill command without any pre-configured scope." + }, + { + "description": "Enables the open command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-open", + "markdownDescription": "Enables the open command without any pre-configured scope." + }, + { + "description": "Enables the spawn command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-spawn", + "markdownDescription": "Enables the spawn command without any pre-configured scope." + }, + { + "description": "Enables the stdin_write command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-stdin-write", + "markdownDescription": "Enables the stdin_write command without any pre-configured scope." + }, + { + "description": "Denies the execute command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-execute", + "markdownDescription": "Denies the execute command without any pre-configured scope." + }, + { + "description": "Denies the kill command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-kill", + "markdownDescription": "Denies the kill command without any pre-configured scope." + }, + { + "description": "Denies the open command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-open", + "markdownDescription": "Denies the open command without any pre-configured scope." + }, + { + "description": "Denies the spawn command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-spawn", + "markdownDescription": "Denies the spawn command without any pre-configured scope." + }, + { + "description": "Denies the stdin_write command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-stdin-write", + "markdownDescription": "Denies the stdin_write command without any pre-configured scope." + } + ] + }, + "Value": { + "description": "All supported ACL values.", + "anyOf": [ + { + "description": "Represents a null JSON value.", + "type": "null" + }, + { + "description": "Represents a [`bool`].", + "type": "boolean" + }, + { + "description": "Represents a valid ACL [`Number`].", + "allOf": [ + { + "$ref": "#/definitions/Number" + } + ] + }, + { + "description": "Represents a [`String`].", + "type": "string" + }, + { + "description": "Represents a list of other [`Value`]s.", + "type": "array", + "items": { + "$ref": "#/definitions/Value" + } + }, + { + "description": "Represents a map of [`String`] keys to [`Value`]s.", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Value" + } + } + ] + }, + "Number": { + "description": "A valid ACL number.", + "anyOf": [ + { + "description": "Represents an [`i64`].", + "type": "integer", + "format": "int64" + }, + { + "description": "Represents a [`f64`].", + "type": "number", + "format": "double" + } + ] + }, + "Target": { + "description": "Platform target.", + "oneOf": [ + { + "description": "MacOS.", + "type": "string", + "enum": [ + "macOS" + ] + }, + { + "description": "Windows.", + "type": "string", + "enum": [ + "windows" + ] + }, + { + "description": "Linux.", + "type": "string", + "enum": [ + "linux" + ] + }, + { + "description": "Android.", + "type": "string", + "enum": [ + "android" + ] + }, + { + "description": "iOS.", + "type": "string", + "enum": [ + "iOS" + ] + } + ] + }, + "ShellScopeEntryAllowedArg": { + "description": "A command argument allowed to be executed by the webview API.", + "anyOf": [ + { + "description": "A non-configurable argument that is passed to the command in the order it was specified.", + "type": "string" + }, + { + "description": "A variable that is set while calling the command from the webview API.", + "type": "object", + "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.", + "default": false, + "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" + } + }, + "additionalProperties": false + } + ] + }, + "ShellScopeEntryAllowedArgs": { + "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.", + "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.", + "type": "array", + "items": { + "$ref": "#/definitions/ShellScopeEntryAllowedArg" + } + } + ] + } + } +} \ No newline at end of file diff --git a/src-tauri/src/games.rs b/src-tauri/src/games.rs new file mode 100644 index 0000000..0b8a555 --- /dev/null +++ b/src-tauri/src/games.rs @@ -0,0 +1,404 @@ +//! Native game processes launched from KNeuron. +//! +//! The games (Unity builds of Neurosniper and Neurorun) read attention and +//! relaxation from neurostate on their own, so KNeuron only starts and stops +//! them. Only known games can be launched, and only from an executable named +//! like the game, so the webview cannot start arbitrary programs. + +use std::collections::HashMap; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::{Child, Command, Stdio}; +use std::sync::{Arc, Mutex}; +use std::thread; +use std::time::Duration; + +use serde::Serialize; +use tauri::{AppHandle, Emitter, State}; + +/// A game KNeuron can install and launch. +pub struct GameSpec { + pub id: &'static str, + + /// GitHub repository whose releases publish the game's builds. + pub repository: &'static str, + + /// File names (without extension, lowercase) of the player executable in + /// the game's Unity builds. + pub executable_names: &'static [&'static str], +} + +pub const GAMES: &[GameSpec] = &[ + GameSpec { + id: "neurosniper", + repository: "KN-Neuron/Neurosniper", + executable_names: &["neurosniper"], + }, + GameSpec { + id: "neurorun", + repository: "KN-Neuron/Neurorun", + // The Windows build is named after the Unity project: Neuorrun.exe. + executable_names: &["neurorun", "neuorrun"], + }, +]; + +/// Extension of a Unity player executable on this platform. +#[cfg(windows)] +const EXECUTABLE_EXTENSION: &str = "exe"; +#[cfg(not(windows))] +const EXECUTABLE_EXTENSION: &str = "x86_64"; + +pub fn game_spec(game_id: &str) -> Result<&'static GameSpec, String> { + GAMES + .iter() + .find(|spec| spec.id == game_id) + .ok_or_else(|| format!("Unknown game \"{game_id}\".")) +} + +/// Whether `path` is named like the game's player executable on this platform. +pub fn is_game_executable(spec: &GameSpec, path: &Path) -> bool { + let lowercase = |part: Option<&std::ffi::OsStr>| { + part.and_then(|value| value.to_str()) + .unwrap_or_default() + .to_lowercase() + }; + + spec.executable_names + .contains(&lowercase(path.file_stem()).as_str()) + && lowercase(path.extension()) == EXECUTABLE_EXTENSION +} + +/// How often the watcher checks whether a game has exited. +const WATCH_INTERVAL: Duration = Duration::from_millis(500); + +/// How long a game gets to quit after being asked, before it is killed. +const QUIT_TIMEOUT: Duration = Duration::from_secs(5); + +/// Event sent to the webview when a launched game exits. +pub const GAME_EXITED_EVENT: &str = "game-exited"; + +type ProcessTable = Arc>>; + +/// Running games, by game id. +#[derive(Default)] +pub struct GameProcesses(ProcessTable); + +#[derive(Clone, Serialize)] +#[serde(rename_all = "camelCase")] +struct GameExited { + game_id: String, + code: Option, +} + +/// Checks that `executable` is the given game's player executable. +fn validate_executable(game_id: &str, executable: &str) -> Result { + let spec = game_spec(game_id)?; + let path = PathBuf::from(executable); + + if !path.is_absolute() { + return Err("The game executable path must be absolute.".into()); + } + + if !is_game_executable(spec, &path) { + let names: Vec = spec + .executable_names + .iter() + .map(|name| format!("{name}.{EXECUTABLE_EXTENSION}")) + .collect(); + + return Err(format!( + "Choose the game's {} file (any capitalisation).", + names.join(" or ") + )); + } + + let metadata = + fs::metadata(&path).map_err(|_| format!("{} does not exist.", path.display()))?; + + if !metadata.is_file() { + return Err(format!("{} is not a file.", path.display())); + } + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + + if metadata.permissions().mode() & 0o111 == 0 { + return Err(format!( + "{} is not executable. Make it executable with: chmod +x \"{}\"", + path.display(), + path.display() + )); + } + } + + Ok(path) +} + +fn lock(table: &ProcessTable) -> Result>, String> { + table + .lock() + .map_err(|_| "The game process table is unavailable.".to_string()) +} + +/// Starts a game and returns its process id. +#[tauri::command] +pub fn launch_game( + app: AppHandle, + processes: State<'_, GameProcesses>, + game_id: String, + executable: String, +) -> Result { + let path = validate_executable(&game_id, &executable)?; + let mut games = lock(&processes.0)?; + + if let Some(child) = games.get_mut(&game_id) { + if matches!(child.try_wait(), Ok(None)) { + return Err(format!("{game_id} is already running.")); + } + } + + let child = Command::new(&path) + // Unity finds its data folder next to the executable, whatever the working directory. + .current_dir(path.parent().unwrap_or_else(|| Path::new("."))) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn() + .map_err(|error| format!("Could not start {}: {error}", path.display()))?; + + let pid = child.id(); + games.insert(game_id.clone(), child); + drop(games); + + watch_game(app, processes.0.clone(), game_id, pid); + + Ok(pid) +} + +/// Reports the game's exit to the webview, then forgets it. +fn watch_game(app: AppHandle, table: ProcessTable, game_id: String, pid: u32) { + thread::spawn(move || loop { + thread::sleep(WATCH_INTERVAL); + + let code = { + let Ok(mut games) = table.lock() else { + return; + }; + + let Some(child) = games.get_mut(&game_id) else { + return; + }; + + // A newer launch of the same game has its own watcher. + if child.id() != pid { + return; + } + + match child.try_wait() { + Ok(None) => continue, + Ok(Some(status)) => { + games.remove(&game_id); + status.code() + } + Err(_) => { + games.remove(&game_id); + None + } + } + }; + + let _ = app.emit( + GAME_EXITED_EVENT, + GameExited { + game_id: game_id.clone(), + code, + }, + ); + return; + }); +} + +/// Asks the game to quit, as closing its window would, and kills it if it +/// hasn't after a few seconds. Its exit is reported by the watcher. +#[tauri::command] +pub fn stop_game(processes: State<'_, GameProcesses>, game_id: String) -> Result<(), String> { + let pid = { + let mut games = lock(&processes.0)?; + + let Some(child) = games.get_mut(&game_id) else { + return Ok(()); + }; + + request_quit(child); + child.id() + }; + + let table = processes.0.clone(); + + thread::spawn(move || { + thread::sleep(QUIT_TIMEOUT); + + if let Ok(mut games) = table.lock() { + if let Some(child) = games.get_mut(&game_id) { + if child.id() == pid && matches!(child.try_wait(), Ok(None)) { + let _ = child.kill(); + } + } + } + }); + + Ok(()) +} + +#[cfg(unix)] +fn request_quit(child: &mut Child) { + // SIGTERM lets Unity quit normally, saving PlayerPrefs such as Neurorun's + // attention threshold. + if let Ok(pid) = i32::try_from(child.id()) { + // SAFETY: kill() only sends a signal to the game process we started. + unsafe { + libc::kill(pid, libc::SIGTERM); + } + } +} + +#[cfg(not(unix))] +fn request_quit(child: &mut Child) { + let _ = child.kill(); +} + +#[tauri::command] +pub fn is_game_running(processes: State<'_, GameProcesses>, game_id: String) -> bool { + let Ok(mut games) = lock(&processes.0) else { + return false; + }; + + games + .get_mut(&game_id) + .is_some_and(|child| matches!(child.try_wait(), Ok(None))) +} + +/// Kills every game KNeuron started; called when KNeuron exits. +pub fn stop_all(processes: &GameProcesses) { + if let Ok(mut games) = processes.0.lock() { + for (_, mut child) in games.drain() { + let _ = child.kill(); + let _ = child.wait(); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn temp_file(name: &str, executable: bool) -> PathBuf { + let dir = std::env::temp_dir().join(format!("kneuron-games-test-{}", std::process::id())); + fs::create_dir_all(&dir).unwrap(); + let path = dir.join(name); + fs::write(&path, b"#!/bin/sh\n").unwrap(); + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mode = if executable { 0o755 } else { 0o644 }; + fs::set_permissions(&path, fs::Permissions::from_mode(mode)).unwrap(); + } + #[cfg(not(unix))] + let _ = executable; + + path + } + + fn executable_name(stem: &str) -> String { + format!("{stem}.{EXECUTABLE_EXTENSION}") + } + + #[test] + fn accepts_the_games_player_in_any_case() { + let path = temp_file(&executable_name("NeuroSniper"), true); + + assert_eq!( + validate_executable("neurosniper", path.to_str().unwrap()), + Ok(path) + ); + } + + #[test] + fn accepts_neurorun_under_its_unity_project_name() { + let path = temp_file(&executable_name("Neuorrun"), true); + + assert_eq!( + validate_executable("neurorun", path.to_str().unwrap()), + Ok(path) + ); + } + + #[test] + fn rejects_unknown_games() { + let path = temp_file(&executable_name("Neurosniper"), true); + + let error = validate_executable("solitaire", path.to_str().unwrap()).unwrap_err(); + assert!(error.contains("Unknown game"), "{error}"); + } + + #[test] + fn rejects_other_programs() { + let path = temp_file(&executable_name("bash"), true); + + let error = validate_executable("neurorun", path.to_str().unwrap()).unwrap_err(); + assert!(error.contains("neurorun"), "{error}"); + } + + #[test] + fn rejects_relative_and_missing_paths() { + let relative = validate_executable("neurorun", &executable_name("NeuroRun")).unwrap_err(); + assert!(relative.contains("absolute"), "{relative}"); + + let missing = std::env::temp_dir() + .join("kneuron-games-test-missing") + .join(executable_name("NeuroRun")); + let error = validate_executable("neurorun", missing.to_str().unwrap()).unwrap_err(); + assert!(error.contains("does not exist"), "{error}"); + } + + #[cfg(unix)] + #[test] + fn asks_the_game_to_quit_with_sigterm() { + // Exits with 3 only when it receives SIGTERM. + let mut child = Command::new("sh") + .args(["-c", "trap 'exit 3' TERM; while true; do sleep 0.05; done"]) + .spawn() + .unwrap(); + thread::sleep(Duration::from_millis(200)); + + request_quit(&mut child); + + for _ in 0..100 { + if let Some(status) = child.try_wait().unwrap() { + assert_eq!(status.code(), Some(3)); + return; + } + thread::sleep(Duration::from_millis(50)); + } + let _ = child.kill(); + panic!("the process did not quit on SIGTERM"); + } + + #[cfg(unix)] + #[test] + fn explains_how_to_fix_a_missing_executable_bit() { + let dir = std::env::temp_dir().join(format!("kneuron-games-noexec-{}", std::process::id())); + fs::create_dir_all(&dir).unwrap(); + let path = dir.join(executable_name("NeuroRun")); + fs::write(&path, b"").unwrap(); + { + use std::os::unix::fs::PermissionsExt; + fs::set_permissions(&path, fs::Permissions::from_mode(0o644)).unwrap(); + } + + let error = validate_executable("neurorun", path.to_str().unwrap()).unwrap_err(); + assert!(error.contains("chmod +x"), "{error}"); + } +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 0cd344c..0904199 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,3 +1,8 @@ +mod games; +mod releases; + +use tauri::Manager; + /// Starts the native KNeuron desktop shell. /// /// Keep this layer intentionally small. Device access, EEG processing, @@ -7,6 +12,23 @@ pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_shell::init()) - .run(tauri::generate_context!()) - .expect("error while running KNeuron"); + .plugin(tauri_plugin_dialog::init()) + .manage(games::GameProcesses::default()) + .manage(releases::GameInstalls::default()) + .invoke_handler(tauri::generate_handler![ + games::launch_game, + games::stop_game, + games::is_game_running, + releases::installed_game, + releases::latest_game_release, + releases::install_latest_game + ]) + .build(tauri::generate_context!()) + .expect("error while building KNeuron") + .run(|app, event| { + // Games started from KNeuron end with it, like its sidecars. + if let tauri::RunEvent::Exit = event { + games::stop_all(&app.state::()); + } + }); } diff --git a/src-tauri/src/releases.rs b/src-tauri/src/releases.rs new file mode 100644 index 0000000..763834a --- /dev/null +++ b/src-tauri/src/releases.rs @@ -0,0 +1,709 @@ +//! Installing the games from their GitHub releases. +//! +//! Each game publishes its Unity builds as release assets named +//! `-linux-x86_64-.tar.gz` and `-windows-x64-.zip`. +//! KNeuron downloads the latest one for this platform, checks it against the +//! SHA-256 digest GitHub publishes, and unpacks it into its data folder: +//! +//! ```text +//! /games///... the unpacked build +//! /games//installed.json which version is installed +//! ``` +//! +//! Only the newest version is kept. + +use std::collections::HashSet; +use std::fs::{self, File}; +use std::io::{BufReader, Write}; +use std::path::{Path, PathBuf}; +use std::sync::Mutex; +use std::time::Duration; + +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; +use tauri::{AppHandle, Emitter, Manager, State}; + +use crate::games::{self, GameSpec}; + +/// Part of the asset name, and its extension, for this platform's build. +#[cfg(windows)] +const ASSET_PLATFORM: (&str, &str) = ("-windows-x64-", ".zip"); +#[cfg(not(windows))] +const ASSET_PLATFORM: (&str, &str) = ("-linux-x86_64-", ".tar.gz"); + +/// Event sent to the webview while a game downloads and unpacks. +pub const INSTALL_PROGRESS_EVENT: &str = "game-install-progress"; + +const INSTALLED_FILE: &str = "installed.json"; + +/// How deep in an unpacked build to look for the executable. +const EXECUTABLE_SEARCH_DEPTH: usize = 3; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct InstalledGame { + /// Release tag, e.g. "v1.1.0". + pub version: String, + pub executable: PathBuf, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct LatestRelease { + version: String, + asset_name: String, + /// Download size in bytes. + size: u64, + page_url: String, +} + +#[derive(Debug, Deserialize)] +struct GithubRelease { + tag_name: String, + html_url: String, + assets: Vec, +} + +#[derive(Debug, Clone, Deserialize)] +struct GithubAsset { + name: String, + browser_download_url: String, + size: u64, + /// "sha256:", published by GitHub for release assets. + digest: Option, +} + +#[derive(Clone, Serialize)] +#[serde(rename_all = "camelCase")] +struct InstallProgress<'a> { + game_id: &'a str, + /// "downloading" or "unpacking". + phase: &'static str, + downloaded: u64, + total: u64, +} + +/// Games being installed, so the same game can't be installed twice at once. +#[derive(Default)] +pub struct GameInstalls(Mutex>); + +/// Removes a game from [`GameInstalls`] when its install ends, however it ends. +struct InstallGuard<'a> { + installs: &'a GameInstalls, + game_id: String, +} + +impl<'a> InstallGuard<'a> { + fn acquire(installs: &'a GameInstalls, game_id: &str) -> Result { + let mut active = installs + .0 + .lock() + .map_err(|_| "The game install table is unavailable.".to_string())?; + + if !active.insert(game_id.to_string()) { + return Err(format!("{game_id} is already being installed.")); + } + + Ok(Self { + installs, + game_id: game_id.to_string(), + }) + } +} + +impl Drop for InstallGuard<'_> { + fn drop(&mut self) { + if let Ok(mut active) = self.installs.0.lock() { + active.remove(&self.game_id); + } + } +} + +fn game_dir(app: &AppHandle, spec: &GameSpec) -> Result { + let data = app + .path() + .app_data_dir() + .map_err(|error| format!("KNeuron's data folder is unavailable: {error}"))?; + + Ok(data.join("games").join(spec.id)) +} + +/// The installed version, if its executable is still there. +fn read_installed(dir: &Path) -> Option { + let text = fs::read_to_string(dir.join(INSTALLED_FILE)).ok()?; + let installed: InstalledGame = serde_json::from_str(&text).ok()?; + + installed.executable.is_file().then_some(installed) +} + +fn http_client() -> Result { + reqwest::Client::builder() + .user_agent(concat!("KNeuron/", env!("CARGO_PKG_VERSION"))) + .connect_timeout(Duration::from_secs(15)) + .build() + .map_err(|error| format!("Could not set up the download client: {error}")) +} + +async fn fetch_latest_release( + client: &reqwest::Client, + spec: &GameSpec, +) -> Result { + let url = format!( + "https://api.github.com/repos/{}/releases/latest", + spec.repository + ); + + let response = client + .get(&url) + .header("Accept", "application/vnd.github+json") + .timeout(Duration::from_secs(20)) + .send() + .await + .map_err(|error| format!("Could not reach GitHub: {error}"))?; + + match response.status().as_u16() { + 200 => {} + 404 => return Err(format!("{} has no published release.", spec.repository)), + 403 | 429 => { + return Err("GitHub's rate limit was reached; try again in a while.".to_string()) + } + status => return Err(format!("GitHub answered with HTTP {status}.")), + } + + response + .json::() + .await + .map_err(|error| format!("Unexpected answer from GitHub: {error}")) +} + +/// The release asset holding this platform's build. +fn pick_asset<'a>(release: &'a GithubRelease, spec: &GameSpec) -> Result<&'a GithubAsset, String> { + let (platform, extension) = ASSET_PLATFORM; + // Only download from the game's own release, never from a URL elsewhere. + let allowed_prefix = format!("https://github.com/{}/releases/download/", spec.repository); + + release + .assets + .iter() + .find(|asset| { + asset.name.contains(platform) + && asset.name.ends_with(extension) + && asset.browser_download_url.starts_with(&allowed_prefix) + }) + .ok_or_else(|| { + format!( + "Release {} of {} has no build for this platform (*{platform}*{extension}).", + release.tag_name, spec.repository + ) + }) +} + +/// Lower-case hex of a SHA-256 hash. +fn hex(bytes: &[u8]) -> String { + bytes.iter().map(|byte| format!("{byte:02x}")).collect() +} + +/// Checks a download against the digest GitHub published for it. +fn verify_digest(asset: &GithubAsset, actual_sha256: &str) -> Result<(), String> { + match asset + .digest + .as_deref() + .and_then(|digest| digest.strip_prefix("sha256:")) + { + Some(expected) if !expected.eq_ignore_ascii_case(actual_sha256) => Err(format!( + "{} is corrupted: its SHA-256 does not match the release.", + asset.name + )), + _ => Ok(()), + } +} + +async fn download( + client: &reqwest::Client, + asset: &GithubAsset, + destination: &Path, + mut progress: impl FnMut(u64), +) -> Result<(), String> { + let mut response = client + .get(&asset.browser_download_url) + .send() + .await + .and_then(reqwest::Response::error_for_status) + .map_err(|error| format!("Downloading {} failed: {error}", asset.name))?; + + let mut file = File::create(destination) + .map_err(|error| format!("Could not write {}: {error}", destination.display()))?; + let mut hasher = Sha256::new(); + let mut downloaded = 0u64; + + while let Some(chunk) = response + .chunk() + .await + .map_err(|error| format!("Downloading {} failed: {error}", asset.name))? + { + hasher.update(&chunk); + file.write_all(&chunk) + .map_err(|error| format!("Could not write {}: {error}", destination.display()))?; + downloaded += chunk.len() as u64; + progress(downloaded); + } + + file.flush() + .map_err(|error| format!("Could not write {}: {error}", destination.display()))?; + + if downloaded != asset.size { + return Err(format!( + "Downloading {} stopped early ({downloaded} of {} bytes).", + asset.name, asset.size + )); + } + + verify_digest(asset, &hex(&hasher.finalize())) +} + +/// Unpacks a .tar.gz or .zip build. Both refuse entries that would land +/// outside `destination`. +fn unpack(archive: &Path, destination: &Path) -> Result<(), String> { + let file = File::open(archive) + .map_err(|error| format!("Could not open {}: {error}", archive.display()))?; + let name = archive.to_string_lossy(); + + let result = if name.ends_with(".tar.gz") { + tar::Archive::new(flate2::read::GzDecoder::new(BufReader::new(file))) + .unpack(destination) + .map_err(|error| error.to_string()) + } else if name.ends_with(".zip") { + zip::ZipArchive::new(BufReader::new(file)) + .and_then(|mut zip| zip.extract(destination)) + .map_err(|error| error.to_string()) + } else { + Err("unsupported archive format".to_string()) + }; + + result.map_err(|error| format!("Could not unpack {}: {error}", archive.display())) +} + +/// The game's executable inside an unpacked build. +fn find_executable(dir: &Path, spec: &GameSpec, depth: usize) -> Option { + let mut subdirectories = Vec::new(); + + for entry in fs::read_dir(dir).ok()?.flatten() { + let path = entry.path(); + let file_type = entry.file_type().ok()?; + + if file_type.is_file() && games::is_game_executable(spec, &path) { + return Some(path); + } + + if file_type.is_dir() { + subdirectories.push(path); + } + } + + if depth == 0 { + return None; + } + + subdirectories + .iter() + .find_map(|subdirectory| find_executable(subdirectory, spec, depth - 1)) +} + +#[cfg(unix)] +fn ensure_executable(path: &Path) -> Result<(), String> { + use std::os::unix::fs::PermissionsExt; + + let mut permissions = fs::metadata(path) + .map_err(|error| error.to_string())? + .permissions(); + + if permissions.mode() & 0o111 == 0 { + permissions.set_mode(permissions.mode() | 0o755); + fs::set_permissions(path, permissions).map_err(|error| error.to_string())?; + } + + Ok(()) +} + +#[cfg(not(unix))] +fn ensure_executable(_path: &Path) -> Result<(), String> { + Ok(()) +} + +/// Unpacks a downloaded build as the installed version and removes older ones. +fn install_archive( + archive: &Path, + dir: &Path, + version: &str, + spec: &GameSpec, +) -> Result { + if version.is_empty() || version.contains(['/', '\\']) || version.starts_with('.') { + return Err(format!("Unexpected release tag \"{version}\".")); + } + + let staging = dir.join(format!(".unpacking-{version}")); + let target = dir.join(version); + let io_error = |error: std::io::Error| error.to_string(); + + if staging.exists() { + fs::remove_dir_all(&staging).map_err(io_error)?; + } + fs::create_dir_all(&staging).map_err(io_error)?; + + if let Err(error) = unpack(archive, &staging) { + let _ = fs::remove_dir_all(&staging); + return Err(error); + } + + let Some(staged_executable) = find_executable(&staging, spec, EXECUTABLE_SEARCH_DEPTH) else { + let _ = fs::remove_dir_all(&staging); + return Err(format!( + "The {version} build of {} contains no {} executable.", + spec.repository, + spec.executable_names.join(" or ") + )); + }; + + let relative = staged_executable + .strip_prefix(&staging) + .map_err(|error| error.to_string())? + .to_path_buf(); + + if target.exists() { + fs::remove_dir_all(&target).map_err(io_error)?; + } + fs::rename(&staging, &target).map_err(io_error)?; + + let installed = InstalledGame { + version: version.to_string(), + executable: target.join(relative), + }; + + ensure_executable(&installed.executable)?; + + let record = serde_json::to_string_pretty(&installed).map_err(|error| error.to_string())?; + fs::write(dir.join(INSTALLED_FILE), record).map_err(io_error)?; + + // Older versions. Best effort: on Windows a folder in use can't be removed. + for entry in fs::read_dir(dir).map_err(io_error)?.flatten() { + let path = entry.path(); + + if path.is_dir() && path != target { + let _ = fs::remove_dir_all(&path); + } + } + + Ok(installed) +} + +/// The installed release of a game, if any. +#[tauri::command] +pub fn installed_game(app: AppHandle, game_id: String) -> Result, String> { + let spec = games::game_spec(&game_id)?; + + Ok(read_installed(&game_dir(&app, spec)?)) +} + +/// The game's latest release on GitHub. +#[tauri::command] +pub async fn latest_game_release(game_id: String) -> Result { + let spec = games::game_spec(&game_id)?; + let release = fetch_latest_release(&http_client()?, spec).await?; + let asset = pick_asset(&release, spec)?; + + Ok(LatestRelease { + version: release.tag_name.clone(), + asset_name: asset.name.clone(), + size: asset.size, + page_url: release.html_url.clone(), + }) +} + +/// Makes sure the game's latest release is installed, downloading it if +/// needed, and returns it. +#[tauri::command] +pub async fn install_latest_game( + app: AppHandle, + installs: State<'_, GameInstalls>, + game_id: String, +) -> Result { + let spec = games::game_spec(&game_id)?; + let _guard = InstallGuard::acquire(&installs, spec.id)?; + + let dir = game_dir(&app, spec)?; + let client = http_client()?; + let release = fetch_latest_release(&client, spec).await?; + + if let Some(installed) = read_installed(&dir) { + if installed.version == release.tag_name { + return Ok(installed); + } + } + + let asset = pick_asset(&release, spec)?.clone(); + fs::create_dir_all(&dir).map_err(|error| error.to_string())?; + + let archive = dir.join(format!(".download-{}", asset.name)); + let mut last_percent = None; + + let downloaded = download(&client, &asset, &archive, |downloaded| { + let percent = downloaded * 100 / asset.size.max(1); + + if last_percent != Some(percent) { + last_percent = Some(percent); + let _ = app.emit( + INSTALL_PROGRESS_EVENT, + InstallProgress { + game_id: spec.id, + phase: "downloading", + downloaded, + total: asset.size, + }, + ); + } + }) + .await; + + if let Err(error) = downloaded { + let _ = fs::remove_file(&archive); + return Err(error); + } + + let _ = app.emit( + INSTALL_PROGRESS_EVENT, + InstallProgress { + game_id: spec.id, + phase: "unpacking", + downloaded: asset.size, + total: asset.size, + }, + ); + + let version = release.tag_name.clone(); + let unpack_dir = dir.clone(); + let unpack_archive = archive.clone(); + + let installed = tauri::async_runtime::spawn_blocking(move || { + install_archive(&unpack_archive, &unpack_dir, &version, spec) + }) + .await + .map_err(|error| format!("Unpacking failed: {error}"))?; + + let _ = fs::remove_file(&archive); + + installed +} + +#[cfg(test)] +mod tests { + use super::*; + + fn spec(id: &str) -> &'static GameSpec { + games::game_spec(id).unwrap() + } + + fn temp_dir(name: &str) -> PathBuf { + let dir = + std::env::temp_dir().join(format!("kneuron-releases-{name}-{}", std::process::id())); + let _ = fs::remove_dir_all(&dir); + fs::create_dir_all(&dir).unwrap(); + dir + } + + fn asset(name: &str, url: &str) -> GithubAsset { + GithubAsset { + name: name.into(), + browser_download_url: url.into(), + size: 1, + digest: None, + } + } + + fn release(assets: Vec) -> GithubRelease { + GithubRelease { + tag_name: "v1.1.0".into(), + html_url: "https://github.com/KN-Neuron/Neurosniper/releases/tag/v1.1.0".into(), + assets, + } + } + + const BASE: &str = "https://github.com/KN-Neuron/Neurosniper/releases/download/v1.1.0/"; + + #[test] + fn picks_this_platforms_build() { + let release = release(vec![ + asset( + "Neurosniper-windows-x64-v1.1.0.zip", + &format!("{BASE}Neurosniper-windows-x64-v1.1.0.zip"), + ), + asset( + "Neurosniper-linux-x86_64-v1.1.0.tar.gz", + &format!("{BASE}Neurosniper-linux-x86_64-v1.1.0.tar.gz"), + ), + ]); + + let picked = pick_asset(&release, spec("neurosniper")).unwrap(); + + assert!(picked.name.contains(ASSET_PLATFORM.0)); + assert!(picked.name.ends_with(ASSET_PLATFORM.1)); + } + + #[test] + fn ignores_assets_hosted_outside_the_games_releases() { + let name = format!("Neurosniper{}v1.1.0{}", ASSET_PLATFORM.0, ASSET_PLATFORM.1); + let release = release(vec![asset(&name, &format!("https://example.com/{name}"))]); + + let error = pick_asset(&release, spec("neurosniper")).unwrap_err(); + assert!(error.contains("no build for this platform"), "{error}"); + } + + #[test] + fn rejects_a_download_whose_digest_does_not_match() { + let mut checked = asset("game.tar.gz", BASE); + checked.digest = Some("sha256:abc123".into()); + + assert!(verify_digest(&checked, "ABC123").is_ok()); + assert!(verify_digest(&checked, "ffff") + .unwrap_err() + .contains("corrupted")); + + checked.digest = None; + assert!(verify_digest(&checked, "ffff").is_ok()); + } + + #[test] + fn hashes_as_lowercase_hex() { + assert_eq!( + hex(&Sha256::digest(b"abc")), + "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad" + ); + } + + #[cfg(not(windows))] + #[test] + fn installs_a_tar_build_and_keeps_only_the_newest_version() { + let dir = temp_dir("tar"); + let spec = spec("neurorun"); + + let build = |version: &str| -> PathBuf { + let archive = dir.join(format!("NeuroRun-linux-x86_64-{version}.tar.gz")); + let encoder = flate2::write::GzEncoder::new( + File::create(&archive).unwrap(), + flate2::Compression::fast(), + ); + let mut tar = tar::Builder::new(encoder); + for (path, mode) in [ + ("NeuroRun/NeuroRun.x86_64", 0o755), + ("NeuroRun/UnityPlayer.so", 0o644), + ] { + let mut header = tar::Header::new_gnu(); + header.set_size(4); + header.set_mode(mode); + header.set_cksum(); + tar.append_data(&mut header, path, &b"data"[..]).unwrap(); + } + tar.into_inner().unwrap().finish().unwrap(); + archive + }; + + let first = install_archive(&build("v1.0.0"), &dir, "v1.0.0", spec).unwrap(); + assert_eq!( + first.executable, + dir.join("v1.0.0/NeuroRun/NeuroRun.x86_64") + ); + assert_eq!(read_installed(&dir), Some(first)); + + let second = install_archive(&build("v1.1.0"), &dir, "v1.1.0", spec).unwrap(); + assert_eq!(read_installed(&dir), Some(second.clone())); + assert!(!dir.join("v1.0.0").exists()); + + { + use std::os::unix::fs::PermissionsExt; + let mode = fs::metadata(&second.executable) + .unwrap() + .permissions() + .mode(); + assert_ne!(mode & 0o111, 0); + } + } + + #[test] + fn installs_a_zip_build() { + let dir = temp_dir("zip"); + let archive = dir.join("Neurosniper.zip"); + let executable = format!( + "NeuroSniper.{}", + if cfg!(windows) { "exe" } else { "x86_64" } + ); + + let mut zip = zip::ZipWriter::new(File::create(&archive).unwrap()); + let options = zip::write::SimpleFileOptions::default() + .compression_method(zip::CompressionMethod::Stored); + zip.start_file(format!("Neurosniper/{executable}"), options) + .unwrap(); + zip.write_all(b"data").unwrap(); + zip.finish().unwrap(); + + let installed = install_archive(&archive, &dir, "v1.1.0", spec("neurosniper")).unwrap(); + + assert_eq!( + installed.executable, + dir.join("v1.1.0").join("Neurosniper").join(executable) + ); + } + + #[test] + fn refuses_a_build_without_the_game() { + let dir = temp_dir("empty"); + let archive = dir.join("empty.zip"); + + let mut zip = zip::ZipWriter::new(File::create(&archive).unwrap()); + zip.start_file( + "README.txt", + zip::write::SimpleFileOptions::default() + .compression_method(zip::CompressionMethod::Stored), + ) + .unwrap(); + zip.finish().unwrap(); + + let error = install_archive(&archive, &dir, "v1.0.0", spec("neurorun")).unwrap_err(); + + assert!( + error.contains("no neurorun or neuorrun executable"), + "{error}" + ); + assert!(!dir.join(".unpacking-v1.0.0").exists()); + assert!(read_installed(&dir).is_none()); + } + + #[test] + fn refuses_tags_that_are_paths() { + let dir = temp_dir("tags"); + let error = + install_archive(&dir.join("x.zip"), &dir, "../evil", spec("neurorun")).unwrap_err(); + + assert!(error.contains("Unexpected release tag"), "{error}"); + } + + /// Downloads and installs the real latest Neurorun release: + /// `cargo test --lib -- --ignored real_release` + #[test] + #[ignore = "downloads about 70 MB from GitHub"] + fn real_release_installs() { + let dir = temp_dir("real"); + let spec = spec("neurorun"); + + tauri::async_runtime::block_on(async { + let client = http_client().unwrap(); + let release = fetch_latest_release(&client, spec).await.unwrap(); + let asset = pick_asset(&release, spec).unwrap().clone(); + let archive = dir.join(&asset.name); + + download(&client, &asset, &archive, |_| {}).await.unwrap(); + let installed = install_archive(&archive, &dir, &release.tag_name, spec).unwrap(); + + println!("installed {installed:?}"); + assert!(installed.executable.is_file()); + }); + } +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 7f9d229..155f1bb 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -22,7 +22,7 @@ "decorations": false, "shadow": true, "theme": "Dark", - "backgroundColor": "#030A12" + "backgroundColor": "#090909" } ], "security": { @@ -62,7 +62,8 @@ "externalBin": [ "binaries/brainaccess-bridge", "binaries/ssvep-classifier", - "binaries/brainlink-bridge" + "binaries/brainlink-bridge", + "binaries/neurostate-bridge" ], "category": "DeveloperTool", "shortDescription": "Modular brain-computer interface platform", diff --git a/src/App.tsx b/src/App.tsx index 3a40f86..c083bc7 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -176,7 +176,7 @@ 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"; diff --git a/src/components/BrainLogo.tsx b/src/components/BrainLogo.tsx index 42c7143..4912133 100644 --- a/src/components/BrainLogo.tsx +++ b/src/components/BrainLogo.tsx @@ -1,22 +1,20 @@ -export function BrainLogo({ size = 54 }: { size?: number }) { +interface BrainLogoProps { + size?: number; +} + +export function BrainLogo({ size = 54 }: BrainLogoProps) { return ( - + ); } diff --git a/src/components/DevicePanel.tsx b/src/components/DevicePanel.tsx index 6e6baea..793041a 100644 --- a/src/components/DevicePanel.tsx +++ b/src/components/DevicePanel.tsx @@ -1,5 +1,12 @@ +import { MonitorOff } from "lucide-react"; + import { useDevice } from "../features/device/useDevice"; +import { ModuleThumbnail } from "./ModuleThumbnail"; + +/** + * Right-hand panel with the active device. + */ export function DevicePanel() { const { snapshot, streamInfoByDeviceId } = useDevice(); @@ -7,63 +14,62 @@ export function DevicePanel() { const activeStatus = snapshot.activeStatus; + const state = activeStatus?.state ?? "disconnected"; + const streamInfo = snapshot.activeDeviceId ? streamInfoByDeviceId[snapshot.activeDeviceId] : undefined; return ( - diff --git a/src/components/ModuleThumbnail.tsx b/src/components/ModuleThumbnail.tsx index f91f3eb..06376ce 100644 --- a/src/components/ModuleThumbnail.tsx +++ b/src/components/ModuleThumbnail.tsx @@ -3,32 +3,28 @@ import { useState } from "react"; interface ModuleThumbnailProps { src?: string; name: string; + /** + * Extra class for placement, e.g. on device cards. + */ + className?: string; } /** - * Displays a module thumbnail with a deterministic fallback. + * Displays a module or device thumbnail. * - * A broken or missing thumbnail must never break the launcher layout. + * Renders nothing when no image is set or it fails to load, so the layout + * closes the gap instead of showing a placeholder. */ -export function ModuleThumbnail({ src, name }: ModuleThumbnailProps) { +export function ModuleThumbnail({ src, name, className }: ModuleThumbnailProps) { const [imageFailed, setImageFailed] = useState(false); - const showFallback = !src || imageFailed; - - if (showFallback) { - return ( -
- {name.charAt(0).toUpperCase()} -
- ); + if (!src || imageFailed) { + return null; } return ( {`${name} = [ - { id: "dashboard", label: "Dashboard", icon: LayoutDashboard }, - { id: "device", label: "Device", icon: Cpu }, +const items: Array<{ id: ShellPage; label: string; icon: LucideIcon }> = [ + { id: "dashboard", label: "Dashboard", icon: LayoutGrid }, + { id: "device", label: "Device", icon: Monitor }, { id: "settings", label: "Settings", icon: Settings }, ]; +/** + * Left navigation with the app version at the bottom. + * + * `page` is null while a module is open, so no item is marked current. + */ export function Sidebar({ page, onNavigate, }: { - page: ShellPage; + page: ShellPage | null; onNavigate: (page: ShellPage) => void; }) { return ( @@ -21,28 +28,23 @@ export function Sidebar({ {items.map(({ id, label, icon: Icon }) => ( ))} -
-
- Type - - {formatDeviceKind(device.kind)} -
-
Transport - {formatTransport(device.transport)}
- {streamInfo && ( - <> -
- Sampling rate - - {streamInfo.sampleRateHz} Hz -
- -
- Channels - - {streamInfo.channels.length} -
- + {addressField && ( +
+ + + { + setBluetoothAddress(event.target.value); + }} + /> + + {addressInvalid && Use the format AA:BB:CC:DD:EE:FF.} +
)}
{active && status.message &&

{status.message}

} - {active && status.error &&

{status.error}

}