diff --git a/docs/content/en/api-reference/css-js/page.mdx b/docs/content/en/api-reference/css-js/page.mdx
index dc1fa6b7..312b42c3 100644
--- a/docs/content/en/api-reference/css-js/page.mdx
+++ b/docs/content/en/api-reference/css-js/page.mdx
@@ -9,101 +9,246 @@ Manage custom CSS and JavaScript plugins.
## dmn.css
-### `dmn.css.get(): Promise`
+Global custom CSS state is `{ path: string | null, content: string }`.
-Returns current custom CSS code.
+### `dmn.css.get(): Promise`
+
+Returns the current custom CSS (source file path and content).
```javascript
-const css = await dmn.css.get();
-console.log(css);
+const { path, content } = await dmn.css.get();
```
-### `dmn.css.set(css: string): Promise`
+### `dmn.css.getUse(): Promise`
-Sets custom CSS code.
+Returns whether custom CSS is enabled.
-```javascript
-await dmn.css.set(`
- .key {
- background: linear-gradient(45deg, #ff6b6b, #feca57);
- border-radius: 8px;
- }
-`);
+### `dmn.css.toggle(enabled: boolean): Promise<{ enabled: boolean }>`
+
+Enables or disables custom CSS.
+
+### `dmn.css.load(): Promise`
+
+Opens a file dialog and imports the selected CSS file. The file must be a
+regular `.css` file, UTF-8 encoded, and at most 1 MiB. Successfully imported
+files are added to the history (up to 10 entries).
+
+```typescript
+type CssLoadResult = {
+ success: boolean;
+ error?: string; // error code on failure (see table below)
+ content?: string;
+ path?: string;
+};
```
-### `dmn.css.onChanged(callback): Unsubscribe`
+If the user cancels the dialog, `success` is `false` and `error` is omitted.
+
+### `dmn.css.setContent(content: string): Promise`
+
+Replaces the CSS content directly (the source path is unchanged). Content is
+limited to 1 MiB.
+
+### `dmn.css.reset(): Promise`
-Subscribes to CSS changes.
+Disables custom CSS and clears the content. The history is preserved.
+
+### `dmn.css.onUse(callback): Unsubscribe`
+
+Subscribes to enable/disable changes. The callback receives
+`{ enabled: boolean }`.
+
+### `dmn.css.onContent(callback): Unsubscribe`
+
+Subscribes to content changes (imports, hot reload, direct edits). The
+callback receives `CustomCss`.
```javascript
-const unsub = dmn.css.onChanged((css) => {
- console.log("CSS updated:", css);
+const unsub = dmn.css.onContent(({ path, content }) => {
+ console.log("CSS updated:", path);
});
```
----
+## dmn.css history
-## dmn.js
+The app keeps up to 10 previously imported CSS files. Only paths in this
+history can be re-activated; arbitrary paths are rejected with
+`PATH_NOT_AUTHORIZED`.
+
+### `dmn.css.historyGet(): Promise`
-### `dmn.js.get(): Promise`
+```typescript
+type CustomCssHistoryItem = {
+ path: string;
+ lastUsedAt: number; // unix millis
+ status: "available" | "missing" | "invalid" | "tooLarge";
+};
+```
-Returns current JavaScript plugin code.
+`status` is advisory: it reflects a quick file check at query time, and the
+authoritative validation happens on activation.
-```javascript
-const js = await dmn.js.get();
-console.log(js);
+### `dmn.css.historyActivate(path: string): Promise`
+
+Switches the global custom CSS to a history entry without a file dialog.
+
+```typescript
+type CssActivateResult = {
+ success: boolean;
+ code?: CssHistoryErrorCode; // on failure
+ content?: string;
+ path?: string;
+};
```
-### `dmn.js.set(js: string): Promise`
+### `dmn.css.historyRemove(path: string): Promise`
-Sets JavaScript plugin code.
+Removes an entry from the history (the file itself is not touched) and
+returns the updated list.
-```javascript
-await dmn.js.set(`
- // @id my-plugin
- dmn.plugin.defineElement({
- name: "My Plugin",
- template: (state, settings, { html }) => html\`
Hello
\`,
- });
-`);
+### Error codes
+
+| Code | Meaning |
+| --- | --- |
+| `PATH_NOT_AUTHORIZED` | The path is not in the history |
+| `NOT_FOUND` | File does not exist |
+| `NOT_REGULAR_FILE` | Not a regular file |
+| `INVALID_EXTENSION` | Not a `.css` file |
+| `TOO_LARGE` | Over 1 MiB |
+| `INVALID_UTF8` | Not valid UTF-8 |
+| `IO_ERROR` | Other I/O failure |
+
+## dmn.css.tab
+
+Per-tab CSS overrides the global CSS for a single key tab. A tab override is
+`{ path: string | null, content: string, enabled: boolean }`. Tab CSS is
+rendered only while the global custom CSS toggle is on.
+
+### `dmn.css.tab.getAll(): Promise`
+
+Returns all overrides as a `Record`.
+
+### `dmn.css.tab.get(tabId: string): Promise`
+
+Returns `{ tabId, css: TabCss | null }`.
+
+### `dmn.css.tab.load(tabId: string): Promise`
+
+Opens a file dialog and imports a CSS file for the tab. The same constraints
+as `dmn.css.load()` apply; on failure `error` carries an error code string.
+
+### `dmn.css.tab.activateHistory(tabId: string, path: string): Promise`
+
+Applies a global history entry to the tab. The path must be in the history
+(`PATH_NOT_AUTHORIZED` otherwise).
+
+```typescript
+type TabCssActivateResult = {
+ success: boolean;
+ code?: CssHistoryErrorCode;
+ tabId: string;
+ css?: TabCss;
+};
```
-### `dmn.js.onChanged(callback): Unsubscribe`
+### `dmn.css.tab.export(tabId: string): Promise`
-Subscribes to JS changes.
+Opens a save dialog and writes the tab's registered CSS content to a `.css`
+file.
-```javascript
-const unsub = dmn.js.onChanged((js) => {
- console.log("JS updated:", js);
-});
+```typescript
+type TabCssExportResult = {
+ success: boolean;
+ code?: "NO_TAB_CSS" | "IO_ERROR";
+ error?: string; // details for IO_ERROR
+ path?: string; // saved location on success
+};
```
----
+If the user cancels the dialog, `success` is `false` and `code` is omitted.
-## Example Usage
+### `dmn.css.tab.set(tabId: string, css: TabCss | null): Promise`
+
+Sets or removes (with `null`) the tab override directly. Only paths
+previously authorized through a file dialog or history activation are
+accepted: an unauthorized path is dropped and the override is stored as
+content-only (`path: null` in the stored state and response).
+
+### `dmn.css.tab.toggle(tabId: string, enabled: boolean): Promise`
+
+Enables or disables the tab override without removing it.
+
+### `dmn.css.tab.clear(tabId: string): Promise`
+
+Removes the tab override, falling back to the global CSS.
+
+### `dmn.css.tab.onChanged(callback): Unsubscribe`
+
+Subscribes to tab override changes. The callback receives
+`{ tabId, css: TabCss | null }`.
+
+## dmn.js
+
+JavaScript plugin state is `{ plugins: JsPlugin[] }` where each plugin is
+`{ id, name, path, content, enabled }`.
+
+### `dmn.js.get(): Promise`
+
+Returns the current plugin list.
+
+### `dmn.js.getUse(): Promise`
+
+Returns whether JS plugins are enabled.
+
+### `dmn.js.toggle(enabled: boolean): Promise<{ enabled: boolean }>`
+
+Enables or disables JS plugins globally.
+
+### `dmn.js.load(): Promise`
+
+Opens a file dialog and adds the selected plugin files.
+
+```typescript
+type JsLoadResult = {
+ success: boolean;
+ added: JsPlugin[];
+ errors?: { path: string; error: string }[];
+};
+```
+
+### `dmn.js.reload(): Promise`
+
+Re-reads all plugin files from disk and returns `{ updated, errors? }`.
+
+### `dmn.js.remove(id: string): Promise`
+
+Removes a plugin by id.
+
+### `dmn.js.setPluginEnabled(id: string, enabled: boolean): Promise`
+
+Enables or disables a single plugin.
+
+### `dmn.js.setContent(content: string): Promise`
+
+Replaces the inline JS content.
+
+### `dmn.js.reset(): Promise`
+
+Disables JS plugins and clears the plugin list.
+
+### `dmn.js.onUse(callback): Unsubscribe`
+
+Subscribes to enable/disable changes (`{ enabled: boolean }`).
+
+### `dmn.js.onState(callback): Unsubscribe`
+
+Subscribes to plugin list changes. The callback receives `CustomJs`.
```javascript
-// CSS editor (main window)
-if (dmn.window.type === "main") {
- const cssEditor = document.createElement("textarea");
- cssEditor.style.cssText =
- "width: 100%; height: 200px; font-family: monospace;";
-
- // Load current CSS
- dmn.css.get().then((css) => {
- cssEditor.value = css;
- });
-
- // Watch changes
- dmn.css.onChanged((css) => {
- cssEditor.value = css;
- });
-
- // Save button
- const saveBtn = document.createElement("button");
- saveBtn.textContent = "Save";
- saveBtn.onclick = () => {
- dmn.css.set(cssEditor.value);
- };
-}
+const unsub = dmn.js.onState(({ plugins }) => {
+ console.log(
+ "plugins:",
+ plugins.map((p) => `${p.name}(${p.enabled ? "on" : "off"})`),
+ );
+});
```
diff --git a/docs/content/en/custom-css/page.mdx b/docs/content/en/custom-css/page.mdx
index ec90f103..d721fada 100644
--- a/docs/content/en/custom-css/page.mdx
+++ b/docs/content/en/custom-css/page.mdx
@@ -48,9 +48,20 @@ For counters:
## Applying CSS Files
1. Create a CSS file with your desired styles.
-2. Go to the **Settings** tab.
-3. Click **Select CSS File** in the **Custom CSS** section.
-4. Select your CSS file.
+2. Go to the **Settings** tab and click **Manage CSS**.
+3. Turn on the **Enable** toggle, then click **Import CSS File**.
+4. Select your CSS file (`.css`, up to 1 MiB). Previously imported files stay in the panel list, ready to re-apply with one click.
+
+## Per-Tab CSS
+
+Each key tab can override the global CSS with its own file. Right-click the grid and choose the tab CSS menu to open the popover:
+
+- **Import**: pick a CSS file that applies to the current tab only.
+- **History list**: files already imported in the custom CSS panel appear below the file actions; click **Apply** on an entry to use it for this tab.
+- **Export**: save the CSS currently registered on this tab back to a `.css` file. This is handy when the original file was moved or deleted, since the tab keeps its own copy of the content.
+- The toggle at the top enables or disables the tab override without removing it.
+
+Tab CSS is applied only while the global **Custom CSS** toggle is on.
## Quick Start Example
diff --git a/docs/content/en/custom-css/variables/page.mdx b/docs/content/en/custom-css/variables/page.mdx
index b04091d3..1c2175e7 100644
--- a/docs/content/en/custom-css/variables/page.mdx
+++ b/docs/content/en/custom-css/variables/page.mdx
@@ -11,35 +11,39 @@ This page lists all CSS variables available for styling in DM Note.
CSS variables applied to key elements. Use with `[data-state="inactive"]` or `[data-state="active"]` selectors.
-| Variable | Description | Type | Example |
-| ------------------ | ------------------------ | ---------- | ------------------------ |
-| `--key-radius` | Corner roundness | `` | `8px`, `50%` |
-| `--key-bg` | Background color | `` | `#ff2b80` |
-| `--key-bg-image` | Background image (gradient) | `` | `linear-gradient(90deg, #f0f, #0ff)` |
-| `--key-border` | Border style | `` | `2px solid #fff`, `none` |
-| `--key-text-color` | Text color | `` | `#ffffff` |
-| `--key-offset-x` | X offset in active state | `` | `0px`, `2px` |
-| `--key-offset-y` | Y offset in active state | `` | `4px` |
+| Variable | Description | Type | Example |
+| --------------------- | -------------------------------------- | ---------- | ------------------------------------ |
+| `--key-radius` | Corner roundness | `` | `8px`, `50%` |
+| `--key-bg` | Background color and saved-image reset | `` | `#ff2b80` |
+| `--key-bg-image` | Background image | `` | `linear-gradient(90deg, #f0f, #0ff)` |
+| `--key-border` | Border and saved border-ring reset | `` | `2px solid #fff`, `none` |
+| `--key-border-image` | Replacement image for a saved ring | `` | `linear-gradient(90deg, #f0f, #0ff)` |
+| `--key-padding` | Inner padding | `` | `0px`, `3px` |
+| `--key-text-color` | Text color | `` | `#ffffff` |
+| `--key-shadow` | Idle and shared fallback shadow | `` | `0 4px 10px rgba(0,0,0,.28)`, `none` |
+| `--key-active-shadow` | Active shadow | `` | `0 3px 8px rgba(0,0,0,.32)`, `none` |
+| `--key-offset-x` | X offset in active state | `` | `0px`, `2px` |
+| `--key-offset-y` | Y offset in active state | `` | `4px` |
- `--key-bg` is applied as `background-color`. To use gradients, specify
- `background` or `background-image` directly and set `--key-bg: transparent;`
- if needed. When a key color is set to a gradient in the app, it renders as
- `background-image: var(--key-bg-image, ...)`, so you can override it with
- `--key-bg-image` (solid keys never get an inline `background-image`).
+ With **Inline Styles Priority** disabled, property-panel colors, gradients,
+ borders, typography, and shadows are low-priority defaults only. Setting
+ `--key-bg` automatically removes a saved background gradient. Setting
+ `--key-border` removes the saved gradient border ring and its spacing. Use
+ `--key-bg-image` or the standard `background` property for a custom gradient.
### Usage Example
```css
-[data-state="inactive"] {
+[data-state='inactive'] {
--key-radius: 8px;
--key-bg: #2a2a2a;
--key-border: 2px solid #444;
--key-text-color: #888;
}
-[data-state="active"] {
+[data-state='active'] {
--key-radius: 8px;
--key-bg: #ff2b80;
--key-border: 2px solid #ff2b80;
@@ -53,22 +57,36 @@ CSS variables applied to key elements. Use with `[data-state="inactive"]` or `[d
CSS variables applied to counter elements. Use with `.counter[data-counter-state="..."]` selectors.
-| Variable | Description | Type | Example |
-| ------------------------ | ---------------------- | ---------- | ------------------------ |
-| `--counter-color` | Counter text color | `` | `#ffffff` |
-| `--counter-stroke-color` | Text outline color | `` | `#000000`, `transparent` |
-| `--counter-stroke-width` | Text outline thickness | `` | `1px`, `2px` |
+| Variable | Description | Type | Example |
+| --------------------------- | ------------------------------- | ---------- | ------------------------------------ |
+| `--counter-color` | Text color and saved-fill reset | `` | `#ffffff` |
+| `--counter-fill-image` | Text fill image | `` | `linear-gradient(90deg, #f0f, #0ff)` |
+| `--counter-fill-clip` | Fill clipping area | `` | `text`, `border-box` |
+| `--counter-text-fill-color` | WebKit text fill color | `` | `transparent`, `currentcolor` |
+| `--counter-stroke-color` | Text outline color | `` | `#000000`, `transparent` |
+| `--counter-stroke-width` | Text outline thickness | `` | `1px`, `2px` |
+
+Setting only `--counter-color` also disables a counter fill gradient saved in
+the app. To create a CSS fill gradient, set all three fill variables:
+
+```css
+.counter {
+ --counter-fill-image: linear-gradient(90deg, #f0f, #0ff);
+ --counter-fill-clip: text;
+ --counter-text-fill-color: transparent;
+}
+```
### Usage Example
```css
-.counter[data-counter-state="inactive"] {
+.counter[data-counter-state='inactive'] {
--counter-color: #666;
--counter-stroke-color: transparent;
--counter-stroke-width: 0px;
}
-.counter[data-counter-state="active"] {
+.counter[data-counter-state='active'] {
--counter-color: #ff2b80;
--counter-stroke-color: #fff;
--counter-stroke-width: 1px;
@@ -80,12 +98,14 @@ CSS variables applied to counter elements. Use with `.counter[data-counter-state
CSS variables available for graph elements.
Disable **Inline Styles Priority** on the graph to control these via CSS.
-| Variable | Description | Type | Example |
-| ---------------- | ---------------------- | ---------- | ------------------------ |
-| `--graph-bg` | Graph background | `` | `rgba(17, 17, 20, 0.85)` |
-| `--graph-border` | Graph border | `` | `1px solid #7dd3fc` |
-| `--graph-radius` | Graph corner radius | `` | `10px` |
-| `--graph-color` | Line/bar drawing color | `` | `#86efac` |
+| Variable | Description | Type | Example |
+| ---------------------- | ---------------------------------- | ---------- | ------------------------------------ |
+| `--graph-bg` | Graph background | `` | `rgba(17, 17, 20, 0.85)` |
+| `--graph-border` | Border and saved border-ring reset | `` | `1px solid #7dd3fc`, `none` |
+| `--graph-border-image` | Replacement image for a saved ring | `` | `linear-gradient(90deg, #f0f, #0ff)` |
+| `--graph-padding` | Graph inner padding | `` | `0px`, `3px` |
+| `--graph-radius` | Graph corner radius | `` | `10px` |
+| `--graph-color` | Line/bar drawing color | `` | `#86efac` |
### Usage Example
@@ -98,6 +118,31 @@ Disable **Inline Styles Priority** on the graph to control these via CSS.
}
```
+## Knob Style Variables
+
+Disable **Inline Styles Priority** on a knob to use these variables. The custom
+class is on the position wrapper, so target `.knob-class [data-knob-element]`
+when applying standard properties directly to the visible surface.
+
+| Variable | Description | Type | Example |
+| ---------------------- | ---------------------------------- | ---------- | ------------------------------------ |
+| `--knob-bg` | Knob background | `` | `#222` |
+| `--knob-border` | Border and saved border-ring reset | `` | `2px solid #fff`, `none` |
+| `--knob-border-image` | Replacement image for a saved ring | `` | `linear-gradient(90deg, #f0f, #0ff)` |
+| `--knob-padding` | Knob inner padding | `` | `0px`, `3px` |
+| `--knob-radius` | Corner radius | `` | `50%`, `8px` |
+| `--knob-shadow` | Knob shadow | `` | `0 4px 10px rgba(0,0,0,.3)` |
+| `--knob-active-shadow` | Turning-state shadow | `` | `0 3px 8px rgba(0,0,0,.32)` |
+| `--knob-indicator` | Center rotation indicator color | `` | `#fff` |
+
+```css
+.volume-knob {
+ --knob-bg: #17171b;
+ --knob-border: none;
+ --knob-indicator: #ff2b80;
+}
+```
+
## Selector Reference
### Key Selectors
@@ -120,21 +165,33 @@ Disable **Inline Styles Priority** on the graph to control these via CSS.
### Graph Selectors
-| Selector | Description |
-| ------------------------- | ------------------------------------------------ |
-| `.graphClassName` | Style a specific graph container |
-| `.graphClassName svg` | Style line graph SVG (stroke/fill) |
-| `.graphClassName > div` | Style the inner plot area (line/bar shared) |
-| `.graphClassName > div > div` | Style individual bars in bar mode |
+| Selector | Description |
+| ----------------------------- | ------------------------------------------- |
+| `.graphClassName` | Style a specific graph container |
+| `.graphClassName svg` | Style line graph SVG (stroke/fill) |
+| `.graphClassName > div` | Style the inner plot area (line/bar shared) |
+| `.graphClassName > div > div` | Style individual bars in bar mode |
+
+### Knob Selectors
+
+| Selector | Description |
+| -------------------------------- | ------------------------------- |
+| `[data-knob-element]` | Every visible knob surface |
+| `[data-knob-state="inactive"]` | Idle knob surfaces |
+| `[data-knob-state="active"]` | Active knob surfaces |
+| `.knobClass [data-knob-element]` | A specific class's knob surface |
+| `[data-knob-indicator]` | Center rotation indicator |
## Standard CSS Properties
-In addition to CSS variables, you can freely use the following standard CSS properties:
+With **Inline Styles Priority** disabled, you can directly use standard visual
+properties such as the following. Element positioning remains app-managed; use
+`--key-offset-x`/`--key-offset-y` to move keys, graphs, and knobs.
### Commonly Used Properties
```css
-[data-state="active"] {
+[data-state='active'] {
/* Shadow effect */
box-shadow: 0px 0px 8px #ff2b80;
@@ -147,10 +204,7 @@ In addition to CSS variables, you can freely use the following standard CSS prop
/* Font style */
font-size: 24px;
font-weight: 700;
- font-family: "Roboto", sans-serif;
-
- /* Transform */
- transform: scale(1.05);
+ font-family: 'Roboto', sans-serif;
/* Filter */
filter: brightness(1.2);
@@ -161,18 +215,22 @@ In addition to CSS variables, you can freely use the following standard CSS prop
### Using !important
-Use `!important` if some styles are not being applied:
+Normal declarations lose to inline values only when **Inline Styles Priority**
+is enabled. In that mode, override the rendered CSS property itself with
+`!important`; adding it to a fallback custom property cannot beat an inline
+property declaration:
```css
-[data-state="active"] {
- --key-bg: #ff2b80 !important;
+[data-state='active'] {
+ background: #ff2b80 !important;
box-shadow: 0px 0px 8px #ff2b80 !important;
}
```
### Browser Compatibility
-DM Note runs on Chromium-based WebView2, supporting most modern CSS features:
+DM Note uses WebView2 on Windows and WKWebView on macOS. You can use modern CSS
+features supported by the WebView on the current operating system:
- CSS Variables (Custom Properties) ✅
- CSS Grid / Flexbox ✅
@@ -193,17 +251,17 @@ DM Note runs on Chromium-based WebView2, supporting most modern CSS features:
```css
/* Priority: 1 (lowest) */
-[data-state="active"] {
+[data-state='active'] {
--key-bg: red;
}
/* Priority: 2 */
-.blue[data-state="active"] {
+.blue[data-state='active'] {
--key-bg: blue;
}
/* Priority: 3 (highest) */
-.blue.special[data-state="active"] {
+.blue.special[data-state='active'] {
--key-bg: purple;
}
```
diff --git a/docs/content/en/guide/settings/page.mdx b/docs/content/en/guide/settings/page.mdx
index 7caab6c0..4543913c 100644
--- a/docs/content/en/guide/settings/page.mdx
+++ b/docs/content/en/guide/settings/page.mdx
@@ -70,9 +70,16 @@ Select the reference point when resizing the overlay window:
Load a custom CSS file to customize key and counter styles.
-1. Click the **Select CSS File** button.
-2. Select your CSS file.
-3. Styles are applied immediately.
+1. Click the **Manage CSS** button to open the custom CSS panel.
+2. Turn on the **Enable** toggle at the top of the panel.
+3. Click **Import CSS File** and select your CSS file (`.css`, up to 1 MiB).
+4. Styles are applied immediately.
+
+The list in the panel keeps up to 10 previously imported CSS files.
+
+- Click the **Apply** button on an entry to switch to that file instantly, without a file dialog.
+- Right-click an entry and choose **Remove from list** (the file itself is not deleted).
+- Entries that can no longer be applied show a badge: **Missing** (moved or deleted), **Unusable** (not a regular `.css` file anymore), or **Too large** (over 1 MiB).
For detailed CSS styling information, see the [Custom CSS
@@ -83,9 +90,9 @@ Load a custom CSS file to customize key and counter styles.
Load JavaScript plugins to extend program functionality.
-1. Turn on the **Enable JS Plugins** toggle.
-2. Click the **Manage Plugins** button.
-3. Click **Add JS Plugin** to select a file.
+1. Click the **Manage Plugins** button to open the panel.
+2. Turn on the **Enable** toggle at the top of the panel.
+3. Click **Add Plugins** to select a file.
For detailed plugin development information, see [Getting
diff --git a/docs/content/ko/api-reference/css-js/page.mdx b/docs/content/ko/api-reference/css-js/page.mdx
index de8cbd8d..1ec09f12 100644
--- a/docs/content/ko/api-reference/css-js/page.mdx
+++ b/docs/content/ko/api-reference/css-js/page.mdx
@@ -3,218 +3,250 @@ title: CSS/JS 파일
description: 커스텀 CSS/JS 파일 관리 API
---
-# CSS/JS 파일
+# CSS / JS API
+
+커스텀 CSS와 JavaScript 플러그인을 관리합니다.
## dmn.css
-커스텀 CSS 파일 관리 API입니다.
+전역 커스텀 CSS 상태는 `{ path: string | null, content: string }`입니다.
-### get()
+### `dmn.css.get(): Promise`
-현재 CSS 설정을 조회합니다.
+현재 커스텀 CSS(원본 파일 경로와 콘텐츠)를 반환합니다.
```javascript
-const css = await dmn.css.get();
-console.log("내용:", css.content);
-console.log("경로:", css.path);
+const { path, content } = await dmn.css.get();
```
-### getUse()
+### `dmn.css.getUse(): Promise`
-CSS 사용 여부만 조회합니다. (설정의 `useCustomCSS` 값)
+커스텀 CSS 활성화 여부를 반환합니다.
-```javascript
-const enabled = await dmn.css.getUse();
-```
+### `dmn.css.toggle(enabled: boolean): Promise<{ enabled: boolean }>`
-### toggle(enabled)
+커스텀 CSS를 켜거나 끕니다.
-CSS 사용 여부를 토글합니다.
+### `dmn.css.load(): Promise`
-```javascript
-await dmn.css.toggle(true); // CSS 활성화
-await dmn.css.toggle(false); // CSS 비활성화
+파일 선택 창을 열어 CSS 파일을 불러옵니다. 파일은 일반 `.css` 파일이고
+UTF-8 인코딩이며 1MiB 이하여야 합니다. 성공적으로 불러온 파일은
+히스토리(최대 10개)에 추가됩니다.
+
+```typescript
+type CssLoadResult = {
+ success: boolean;
+ error?: string; // 실패 시 오류 코드 (아래 표 참조)
+ content?: string;
+ path?: string;
+};
```
-### load()
+파일 선택을 취소하면 `success`는 `false`이고 `error`는 생략됩니다.
-외부 CSS 파일을 불러옵니다. 파일 선택 다이얼로그가 열립니다.
+### `dmn.css.setContent(content: string): Promise`
-```javascript
-const result = await dmn.css.load();
-if (result.success) {
- console.log("불러온 경로:", result.path);
- console.log("내용:", result.content);
-}
-```
+CSS 콘텐츠를 직접 교체합니다 (원본 경로는 유지). 콘텐츠는 1MiB로
+제한됩니다.
-
- 커스텀 CSS가 활성화(`useCustomCSS = true`)된 상태에서 파일 경로가 설정돼 있으면, 파일 변경을 자동 감지하여 다시 적용합니다.
-
+### `dmn.css.reset(): Promise`
-### setContent(content)
+커스텀 CSS를 끄고 콘텐츠를 초기화합니다. 히스토리는 보존됩니다.
-CSS 내용을 직접 설정합니다.
+### `dmn.css.onUse(callback): Unsubscribe`
+
+활성화 상태 변경을 구독합니다. 콜백은 `{ enabled: boolean }`을 받습니다.
+
+### `dmn.css.onContent(callback): Unsubscribe`
+
+콘텐츠 변경(불러오기, 핫리로드, 직접 수정)을 구독합니다. 콜백은
+`CustomCss`를 받습니다.
```javascript
-await dmn.css.setContent(`
- [data-state="active"] {
- --key-bg: #ff2b80;
- --key-border: 2px solid #fff;
- }
-`);
+const unsub = dmn.css.onContent(({ path, content }) => {
+ console.log("CSS 갱신:", path);
+});
```
-### reset()
+## dmn.css 히스토리
-CSS를 기본값으로 초기화합니다.
+앱은 이전에 불러온 CSS 파일을 최대 10개까지 기억합니다. 히스토리에 있는
+경로만 재활성화할 수 있으며, 임의 경로는 `PATH_NOT_AUTHORIZED`로
+거부됩니다.
-```javascript
-await dmn.css.reset();
+### `dmn.css.historyGet(): Promise`
+
+```typescript
+type CustomCssHistoryItem = {
+ path: string;
+ lastUsedAt: number; // unix millis
+ status: "available" | "missing" | "invalid" | "tooLarge";
+};
```
-### 이벤트 구독
+`status`는 참고용입니다. 조회 시점의 간단한 파일 검사 결과이며, 최종
+검증은 활성화 시점에 수행됩니다.
-```javascript
-// CSS 활성화 상태 변경
-const unsub1 = dmn.css.onUse(({ enabled }) => {
- console.log("CSS 활성화:", enabled);
-});
+### `dmn.css.historyActivate(path: string): Promise`
-// CSS 내용 변경
-const unsub2 = dmn.css.onContent((css) => {
- console.log("CSS 변경됨:", css.content);
-});
+파일 선택 창 없이 히스토리 항목으로 전역 커스텀 CSS를 전환합니다.
+
+```typescript
+type CssActivateResult = {
+ success: boolean;
+ code?: CssHistoryErrorCode; // 실패 시
+ content?: string;
+ path?: string;
+};
```
-### 탭별 CSS (css.tab)
+### `dmn.css.historyRemove(path: string): Promise`
-모드/탭별로 개별 CSS를 적용할 수 있습니다.
+히스토리에서 항목을 제거하고 (파일 자체는 건드리지 않음) 갱신된 목록을
+반환합니다.
-```javascript
-// 모든 탭 CSS 조회
-const allTabCss = await dmn.css.tab.getAll();
+### 오류 코드
-// 특정 탭 CSS 조회
-const tab4key = await dmn.css.tab.get("4key");
+| 코드 | 의미 |
+| --- | --- |
+| `PATH_NOT_AUTHORIZED` | 히스토리에 없는 경로 |
+| `NOT_FOUND` | 파일이 존재하지 않음 |
+| `NOT_REGULAR_FILE` | 일반 파일이 아님 |
+| `INVALID_EXTENSION` | `.css` 파일이 아님 |
+| `TOO_LARGE` | 1MiB 초과 |
+| `INVALID_UTF8` | 유효한 UTF-8이 아님 |
+| `IO_ERROR` | 기타 I/O 실패 |
-// 탭 CSS 파일 불러오기
-const result = await dmn.css.tab.load("4key");
+## dmn.css.tab
-// 탭 CSS 설정
-await dmn.css.tab.set("4key", {
- use: true,
- content: "[data-state='active'] { --key-bg: red; }",
-});
+탭별 CSS는 특정 키 탭에서 전역 CSS를 덮어씁니다. 탭 오버라이드는
+`{ path: string | null, content: string, enabled: boolean }`입니다. 탭
+CSS는 전역 커스텀 CSS 토글이 켜져 있을 때만 렌더링됩니다.
-// 탭 CSS 토글
-await dmn.css.tab.toggle("4key", true);
+### `dmn.css.tab.getAll(): Promise`
-// 탭 CSS 초기화
-await dmn.css.tab.clear("4key");
+모든 오버라이드를 `Record`로 반환합니다.
-// 변경 이벤트
-const unsub = dmn.css.tab.onChanged(({ tabId, css }) => {
- console.log(`${tabId} CSS 변경:`, css);
-});
-```
+### `dmn.css.tab.get(tabId: string): Promise`
-## dmn.js
+`{ tabId, css: TabCss | null }`을 반환합니다.
-커스텀 JS(플러그인) 파일 관리 API입니다.
+### `dmn.css.tab.load(tabId: string): Promise`
-### get()
+파일 선택 창을 열어 해당 탭용 CSS 파일을 불러옵니다. `dmn.css.load()`와
+같은 제약이 적용되며, 실패 시 `error`에 오류 코드 문자열이 담깁니다.
-현재 JS 플러그인 상태를 조회합니다.
+### `dmn.css.tab.activateHistory(tabId: string, path: string): Promise`
-```javascript
-const js = await dmn.js.get();
-console.log("플러그인 목록:", js.plugins);
+전역 히스토리 항목을 탭에 적용합니다. 경로는 히스토리에 있어야 하며,
+없으면 `PATH_NOT_AUTHORIZED`로 거부됩니다.
+
+```typescript
+type TabCssActivateResult = {
+ success: boolean;
+ code?: CssHistoryErrorCode;
+ tabId: string;
+ css?: TabCss;
+};
```
-### getUse()
+### `dmn.css.tab.export(tabId: string): Promise`
-JS 플러그인 사용 여부만 조회합니다. (설정의 `useCustomJS` 값)
+저장 창을 열어 탭에 등록된 CSS 콘텐츠를 `.css` 파일로 저장합니다.
-```javascript
-const enabled = await dmn.js.getUse();
+```typescript
+type TabCssExportResult = {
+ success: boolean;
+ code?: "NO_TAB_CSS" | "IO_ERROR";
+ error?: string; // IO_ERROR 상세
+ path?: string; // 성공 시 저장 위치
+};
```
-### toggle(enabled)
+저장 창을 취소하면 `success`는 `false`이고 `code`는 생략됩니다.
-JS 플러그인 사용 여부를 토글합니다.
+### `dmn.css.tab.set(tabId: string, css: TabCss | null): Promise`
-```javascript
-await dmn.js.toggle(true); // 플러그인 활성화
-await dmn.js.toggle(false); // 플러그인 비활성화
-```
+탭 오버라이드를 직접 설정하거나 (`null`로) 제거합니다. 파일 선택 창이나
+히스토리 활성화로 이전에 인가된 경로만 허용됩니다. 인가되지 않은 경로는
+제거되어 content만 저장되며, 저장 상태와 응답 모두 `path: null`이 됩니다.
-### load()
+### `dmn.css.tab.toggle(tabId: string, enabled: boolean): Promise`
-외부 JS 파일을 불러옵니다. 파일 선택 다이얼로그가 열립니다.
+탭 오버라이드를 제거하지 않고 켜거나 끕니다.
-```javascript
-const result = await dmn.js.load();
-if (result.success) {
- console.log("추가된 플러그인:", result.added);
-}
-```
+### `dmn.css.tab.clear(tabId: string): Promise`
-### reload()
+탭 오버라이드를 제거하고 전역 CSS로 되돌립니다.
-모든 플러그인을 다시 로드합니다.
+### `dmn.css.tab.onChanged(callback): Unsubscribe`
-```javascript
-const result = await dmn.js.reload();
-console.log("업데이트된 플러그인:", result.updated);
-```
+탭 오버라이드 변경을 구독합니다. 콜백은 `{ tabId, css: TabCss | null }`을
+받습니다.
-### remove(id)
+## dmn.js
-특정 플러그인을 제거합니다.
+JavaScript 플러그인 상태는 `{ plugins: JsPlugin[] }`이며 각 플러그인은
+`{ id, name, path, content, enabled }`입니다.
-```javascript
-await dmn.js.remove("my-plugin-id");
-```
+### `dmn.js.get(): Promise`
-### setPluginEnabled(id, enabled)
+현재 플러그인 목록을 반환합니다.
-특정 플러그인의 활성화 상태를 변경합니다.
+### `dmn.js.getUse(): Promise`
-```javascript
-await dmn.js.setPluginEnabled("kps", true); // 활성화
-await dmn.js.setPluginEnabled("kps", false); // 비활성화
-```
+JS 플러그인 활성화 여부를 반환합니다.
-### setContent(content)
+### `dmn.js.toggle(enabled: boolean): Promise<{ enabled: boolean }>`
-JS 파일 내용을 직접 설정합니다.
+JS 플러그인 전체를 켜거나 끕니다.
-```javascript
-await dmn.js.setContent(`// @id: inline-plugin
+### `dmn.js.load(): Promise`
+
+파일 선택 창을 열어 선택한 플러그인 파일들을 추가합니다.
-console.log("hello from inline plugin");
-`);
+```typescript
+type JsLoadResult = {
+ success: boolean;
+ added: JsPlugin[];
+ errors?: { path: string; error: string }[];
+};
```
-### reset()
+### `dmn.js.reload(): Promise`
-JS 설정(경로/내용/플러그인 목록)을 기본값으로 초기화합니다.
+모든 플러그인 파일을 디스크에서 다시 읽고 `{ updated, errors? }`를
+반환합니다.
-```javascript
-await dmn.js.reset();
-```
+### `dmn.js.remove(id: string): Promise`
-### 이벤트 구독
+id로 플러그인을 제거합니다.
-```javascript
-// JS 플러그인 활성화 상태 변경
-const unsub1 = dmn.js.onUse(({ enabled }) => {
- console.log("JS 플러그인 활성화:", enabled);
-});
+### `dmn.js.setPluginEnabled(id: string, enabled: boolean): Promise`
+
+개별 플러그인을 켜거나 끕니다.
+
+### `dmn.js.setContent(content: string): Promise`
+
+인라인 JS 콘텐츠를 교체합니다.
+
+### `dmn.js.reset(): Promise`
-// JS 플러그인 상태 변경
-const unsub2 = dmn.js.onState((js) => {
- console.log("플러그인 목록:", js.plugins);
+JS 플러그인을 끄고 플러그인 목록을 초기화합니다.
+
+### `dmn.js.onUse(callback): Unsubscribe`
+
+활성화 상태 변경을 구독합니다 (`{ enabled: boolean }`).
+
+### `dmn.js.onState(callback): Unsubscribe`
+
+플러그인 목록 변경을 구독합니다. 콜백은 `CustomJs`를 받습니다.
+
+```javascript
+const unsub = dmn.js.onState(({ plugins }) => {
+ console.log(
+ "plugins:",
+ plugins.map((p) => `${p.name}(${p.enabled ? "on" : "off"})`),
+ );
});
```
diff --git a/docs/content/ko/custom-css/page.mdx b/docs/content/ko/custom-css/page.mdx
index e4d51f3e..1eb6d12e 100644
--- a/docs/content/ko/custom-css/page.mdx
+++ b/docs/content/ko/custom-css/page.mdx
@@ -47,9 +47,20 @@ DM Note는 CSS 변수(Custom Properties)를 사용하여 스타일을 제어합
## CSS 파일 적용하기
1. 원하는 스타일로 CSS 파일을 작성합니다.
-2. **Settings** 탭으로 이동합니다.
-3. **커스텀 CSS** 섹션에서 **CSS 파일 선택**을 클릭합니다.
-4. 작성한 CSS 파일을 선택합니다.
+2. **Settings** 탭으로 이동해 **CSS 관리** 버튼을 클릭합니다.
+3. **활성화** 토글을 켜고 **CSS 파일 불러오기**를 클릭합니다.
+4. 작성한 CSS 파일을 선택합니다 (`.css`, 최대 1MiB). 이전에 불러온 파일은 패널 목록에 남아 한 번의 클릭으로 다시 적용할 수 있습니다.
+
+## 탭별 CSS
+
+키 탭마다 전역 CSS를 덮어쓰는 전용 파일을 둘 수 있습니다. 그리드를 우클릭하고 탭 CSS 메뉴를 선택하면 팝오버가 열립니다:
+
+- **불러오기**: 현재 탭에만 적용할 CSS 파일을 선택합니다.
+- **히스토리 목록**: 커스텀 CSS 패널에서 불러왔던 파일이 파일 액션 아래에 표시되며, 항목의 **적용** 버튼으로 이 탭에 바로 적용합니다.
+- **내보내기**: 현재 탭에 등록된 CSS를 `.css` 파일로 저장합니다. 탭이 콘텐츠 사본을 보관하므로 원본 파일이 이동·삭제된 경우에도 복구할 수 있습니다.
+- 상단 토글로 탭 오버라이드를 제거하지 않고 켜고 끌 수 있습니다.
+
+탭 CSS는 전역 **커스텀 CSS** 토글이 켜져 있을 때만 적용됩니다.
## 빠른 시작 예제
diff --git a/docs/content/ko/custom-css/variables/page.mdx b/docs/content/ko/custom-css/variables/page.mdx
index 2bc30022..bc6d8129 100644
--- a/docs/content/ko/custom-css/variables/page.mdx
+++ b/docs/content/ko/custom-css/variables/page.mdx
@@ -11,35 +11,39 @@ DM Note에서 스타일링에 사용할 수 있는 모든 CSS 변수를 정리
키 요소에 적용되는 CSS 변수입니다. `[data-state="inactive"]` 또는 `[data-state="active"]` 선택자와 함께 사용합니다.
-| 변수 | 설명 | 타입 | 예시 |
-| ------------------ | ------------------------ | ---------- | ------------------------ |
-| `--key-radius` | 키 모서리의 둥근 정도 | `` | `8px`, `50%` |
-| `--key-bg` | 키의 배경색 | `` | `#ff2b80` |
-| `--key-bg-image` | 키의 배경 이미지(그라데이션) | `` | `linear-gradient(90deg, #f0f, #0ff)` |
-| `--key-border` | 키의 테두리 스타일 | `` | `2px solid #fff`, `none` |
-| `--key-text-color` | 키 텍스트 색상 | `` | `#ffffff` |
-| `--key-offset-x` | 활성 상태에서 X축 이동량 | `` | `0px`, `2px` |
-| `--key-offset-y` | 활성 상태에서 Y축 이동량 | `` | `4px` |
+| 변수 | 설명 | 타입 | 예시 |
+| --------------------- | ----------------------------------- | ---------- | ------------------------------------ |
+| `--key-radius` | 키 모서리의 둥근 정도 | `` | `8px`, `50%` |
+| `--key-bg` | 키 배경색과 저장된 배경 이미지 해제 | `` | `#ff2b80` |
+| `--key-bg-image` | 키의 배경 이미지 | `` | `linear-gradient(90deg, #f0f, #0ff)` |
+| `--key-border` | 키 테두리와 저장된 보더 링 해제 | `` | `2px solid #fff`, `none` |
+| `--key-border-image` | 저장된 보더 링의 이미지 교체 | `` | `linear-gradient(90deg, #f0f, #0ff)` |
+| `--key-padding` | 키 내부 여백 | `` | `0px`, `3px` |
+| `--key-text-color` | 키 텍스트 색상 | `` | `#ffffff` |
+| `--key-shadow` | 대기 및 공통 폴백 그림자 | `` | `0 4px 10px rgba(0,0,0,.28)`, `none` |
+| `--key-active-shadow` | 입력 상태 그림자 | `` | `0 3px 8px rgba(0,0,0,.32)`, `none` |
+| `--key-offset-x` | 활성 상태에서 X축 이동량 | `` | `0px`, `2px` |
+| `--key-offset-y` | 활성 상태에서 Y축 이동량 | `` | `4px` |
- `--key-bg`는 `background-color`로 적용됩니다. 그라데이션을 사용하려면
- `background` 또는 `background-image`를 직접 지정하고, 필요하면 `--key-bg:
- transparent;`로 설정하세요. 앱에서 키 색상을 그라데이션으로 설정한 경우에는
- `background-image: var(--key-bg-image, ...)`로 렌더되므로 `--key-bg-image`로
- 덮어쓸 수 있습니다 (단색 키에는 인라인 `background-image`가 붙지 않습니다).
+ **인라인 스타일 우선**이 꺼져 있으면 속성 패널의 색·그라데이션·보더·글꼴·
+ 그림자는 낮은 우선순위의 기본값으로만 적용됩니다. `--key-bg`를 지정하면 저장된
+ 배경 그라데이션이 자동으로 사라지고, `--key-border`를 지정하면 저장된
+ 그라데이션 보더 링과 링 여백이 함께 사라집니다. 사용자 그라데이션은
+ `--key-bg-image`나 표준 `background` 속성으로 지정할 수 있습니다.
### 사용 예시
```css
-[data-state="inactive"] {
+[data-state='inactive'] {
--key-radius: 8px;
--key-bg: #2a2a2a;
--key-border: 2px solid #444;
--key-text-color: #888;
}
-[data-state="active"] {
+[data-state='active'] {
--key-radius: 8px;
--key-bg: #ff2b80;
--key-border: 2px solid #ff2b80;
@@ -53,22 +57,36 @@ DM Note에서 스타일링에 사용할 수 있는 모든 CSS 변수를 정리
카운터 요소에 적용되는 CSS 변수입니다. `.counter[data-counter-state="..."]` 선택자와 함께 사용합니다.
-| 변수 | 설명 | 타입 | 예시 |
-| ------------------------ | ------------------ | ---------- | ------------------------ |
-| `--counter-color` | 카운터 텍스트 색상 | `` | `#ffffff` |
-| `--counter-stroke-color` | 텍스트 외곽선 색상 | `` | `#000000`, `transparent` |
-| `--counter-stroke-width` | 텍스트 외곽선 두께 | `` | `1px`, `2px` |
+| 변수 | 설명 | 타입 | 예시 |
+| --------------------------- | ------------------------------ | ---------- | ------------------------------------ |
+| `--counter-color` | 텍스트 색상과 저장된 fill 해제 | `` | `#ffffff` |
+| `--counter-fill-image` | 텍스트 fill 이미지 | `` | `linear-gradient(90deg, #f0f, #0ff)` |
+| `--counter-fill-clip` | fill 클립 영역 | `` | `text`, `border-box` |
+| `--counter-text-fill-color` | WebKit 텍스트 채움색 | `` | `transparent`, `currentcolor` |
+| `--counter-stroke-color` | 텍스트 외곽선 색상 | `` | `#000000`, `transparent` |
+| `--counter-stroke-width` | 텍스트 외곽선 두께 | `` | `1px`, `2px` |
+
+`--counter-color`만 지정해도 앱에 저장된 카운터 fill 그라데이션은 자동으로
+해제됩니다. CSS로 새 fill 그라데이션을 만들 때는 다음 세 변수를 함께 사용합니다.
+
+```css
+.counter {
+ --counter-fill-image: linear-gradient(90deg, #f0f, #0ff);
+ --counter-fill-clip: text;
+ --counter-text-fill-color: transparent;
+}
+```
### 사용 예시
```css
-.counter[data-counter-state="inactive"] {
+.counter[data-counter-state='inactive'] {
--counter-color: #666;
--counter-stroke-color: transparent;
--counter-stroke-width: 0px;
}
-.counter[data-counter-state="active"] {
+.counter[data-counter-state='active'] {
--counter-color: #ff2b80;
--counter-stroke-color: #fff;
--counter-stroke-width: 1px;
@@ -80,12 +98,14 @@ DM Note에서 스타일링에 사용할 수 있는 모든 CSS 변수를 정리
그래프 요소에 적용되는 CSS 변수입니다.
그래프의 **인라인 스타일 우선**을 끄면 아래 변수를 통해 외형을 제어할 수 있습니다.
-| 변수 | 설명 | 타입 | 예시 |
-| ---------------- | ------------- | ---------- | ------------------------ |
-| `--graph-bg` | 그래프 배경 | `` | `rgba(17, 17, 20, 0.85)` |
-| `--graph-border` | 그래프 테두리 | `` | `1px solid #7dd3fc` |
-| `--graph-radius` | 그래프 라운딩 | `` | `10px` |
-| `--graph-color` | 라인/바 색상 | `` | `#86efac` |
+| 변수 | 설명 | 타입 | 예시 |
+| ---------------------- | ----------------------------------- | ---------- | ------------------------------------ |
+| `--graph-bg` | 그래프 배경 | `` | `rgba(17, 17, 20, 0.85)` |
+| `--graph-border` | 그래프 테두리와 저장된 보더 링 해제 | `` | `1px solid #7dd3fc`, `none` |
+| `--graph-border-image` | 저장된 보더 링의 이미지 교체 | `` | `linear-gradient(90deg, #f0f, #0ff)` |
+| `--graph-padding` | 그래프 내부 여백 | `` | `0px`, `3px` |
+| `--graph-radius` | 그래프 라운딩 | `` | `10px` |
+| `--graph-color` | 라인/바 색상 | `` | `#86efac` |
### 사용 예시
@@ -98,6 +118,31 @@ DM Note에서 스타일링에 사용할 수 있는 모든 CSS 변수를 정리
}
```
+## 노브 스타일 변수
+
+노브의 **인라인 스타일 우선**을 끄면 아래 변수를 사용할 수 있습니다. 노브에
+지정한 클래스는 위치 래퍼에 있으므로 일반 속성을 직접 지정할 때는
+`.노브클래스 [data-knob-element]` 선택자를 사용하세요.
+
+| 변수 | 설명 | 타입 | 예시 |
+| ---------------------- | --------------------------------- | ---------- | ------------------------------------ |
+| `--knob-bg` | 노브 배경 | `` | `#222` |
+| `--knob-border` | 노브 테두리와 저장된 보더 링 해제 | `` | `2px solid #fff`, `none` |
+| `--knob-border-image` | 저장된 보더 링의 이미지 교체 | `` | `linear-gradient(90deg, #f0f, #0ff)` |
+| `--knob-padding` | 노브 내부 여백 | `` | `0px`, `3px` |
+| `--knob-radius` | 노브 모서리 | `` | `50%`, `8px` |
+| `--knob-shadow` | 노브 그림자 | `` | `0 4px 10px rgba(0,0,0,.3)` |
+| `--knob-active-shadow` | 회전 중 그림자 | `` | `0 3px 8px rgba(0,0,0,.32)` |
+| `--knob-indicator` | 중앙 회전 표시 색상 | `` | `#fff` |
+
+```css
+.volume-knob {
+ --knob-bg: #17171b;
+ --knob-border: none;
+ --knob-indicator: #ff2b80;
+}
+```
+
## 선택자 레퍼런스
### 키 선택자
@@ -120,21 +165,33 @@ DM Note에서 스타일링에 사용할 수 있는 모든 CSS 변수를 정리
### 그래프 선택자
-| 선택자 | 설명 |
-| ----------------------- | ------------------------------------- |
-| `.그래프클래스명` | 특정 그래프 컨테이너 스타일링 |
-| `.그래프클래스명 svg` | 라인 그래프 SVG(선/채움) 스타일링 |
-| `.그래프클래스명 > div` | 그래프 내부 플롯 영역(라인/바 공통) |
+| 선택자 | 설명 |
+| ----------------------------- | --------------------------------------- |
+| `.그래프클래스명` | 특정 그래프 컨테이너 스타일링 |
+| `.그래프클래스명 svg` | 라인 그래프 SVG(선/채움) 스타일링 |
+| `.그래프클래스명 > div` | 그래프 내부 플롯 영역(라인/바 공통) |
| `.그래프클래스명 > div > div` | 바 그래프 막대 개별 스타일링 (bar 모드) |
+### 노브 선택자
+
+| 선택자 | 설명 |
+| --------------------------------- | ------------------------ |
+| `[data-knob-element]` | 모든 노브 표면 |
+| `[data-knob-state="inactive"]` | 입력 대기 중인 노브 표면 |
+| `[data-knob-state="active"]` | 입력 중인 노브 표면 |
+| `.노브클래스 [data-knob-element]` | 특정 클래스의 노브 표면 |
+| `[data-knob-indicator]` | 노브 중앙 회전 표시 |
+
## 표준 CSS 속성
-CSS 변수 외에도 다음 표준 CSS 속성들을 자유롭게 사용할 수 있습니다:
+**인라인 스타일 우선**이 꺼진 요소에는 다음과 같은 표준 외형 속성을 직접 사용할
+수 있습니다. 위치는 앱이 관리하므로 키·그래프·노브 이동에는
+`--key-offset-x`/`--key-offset-y`를 사용하세요.
### 자주 사용되는 속성
```css
-[data-state="active"] {
+[data-state='active'] {
/* 그림자 효과 */
box-shadow: 0px 0px 8px #ff2b80;
@@ -147,10 +204,7 @@ CSS 변수 외에도 다음 표준 CSS 속성들을 자유롭게 사용할 수
/* 폰트 스타일 */
font-size: 24px;
font-weight: 700;
- font-family: "Roboto", sans-serif;
-
- /* 변형 */
- transform: scale(1.05);
+ font-family: 'Roboto', sans-serif;
/* 필터 */
filter: brightness(1.2);
@@ -161,18 +215,21 @@ CSS 변수 외에도 다음 표준 CSS 속성들을 자유롭게 사용할 수
### !important 사용
-일부 스타일이 적용되지 않는 경우 `!important`를 사용할 수 있습니다:
+요소의 **인라인 스타일 우선**을 켠 경우에만 일반 선언보다 인라인 값이 우선합니다.
+이 모드까지 강제로 덮어쓸 때는 실제 렌더 CSS 속성 자체에 `!important`를 사용해야
+합니다. 폴백 커스텀 속성에 붙여도 인라인 속성 선언보다 우선할 수 없습니다:
```css
-[data-state="active"] {
- --key-bg: #ff2b80 !important;
+[data-state='active'] {
+ background: #ff2b80 !important;
box-shadow: 0px 0px 8px #ff2b80 !important;
}
```
### 브라우저 호환성
-DM Note는 Chromium 기반 WebView2에서 동작하므로 최신 CSS 기능을 대부분 지원합니다:
+DM Note는 Windows의 WebView2와 macOS의 WKWebView에서 동작합니다. 운영체제별
+WebView가 지원하는 최신 CSS 기능을 사용할 수 있습니다:
- CSS Variables (Custom Properties) ✅
- CSS Grid / Flexbox ✅
@@ -182,8 +239,8 @@ DM Note는 Chromium 기반 WebView2에서 동작하므로 최신 CSS 기능을
- mix-blend-mode ✅
- 단, 오버레이의 배경이 투명(`transparent`) 상태일 때는 `backdrop-filter`가 동작하지
- 않습니다.
+ 단, 오버레이의 배경이 투명(`transparent`) 상태일 때는 `backdrop-filter`가
+ 동작하지 않습니다.
### 상속과 우선순위
@@ -194,17 +251,17 @@ DM Note는 Chromium 기반 WebView2에서 동작하므로 최신 CSS 기능을
```css
/* 우선순위: 1 (가장 낮음) */
-[data-state="active"] {
+[data-state='active'] {
--key-bg: red;
}
/* 우선순위: 2 */
-.blue[data-state="active"] {
+.blue[data-state='active'] {
--key-bg: blue;
}
/* 우선순위: 3 (가장 높음) */
-.blue.special[data-state="active"] {
+.blue.special[data-state='active'] {
--key-bg: purple;
}
```
diff --git a/docs/content/ko/guide/settings/page.mdx b/docs/content/ko/guide/settings/page.mdx
index 8ac8142c..324100a6 100644
--- a/docs/content/ko/guide/settings/page.mdx
+++ b/docs/content/ko/guide/settings/page.mdx
@@ -69,9 +69,16 @@ DM Note의 다양한 설정 옵션을 안내합니다.
사용자 정의 CSS 파일을 로드하여 키와 카운터의 스타일을 커스터마이징합니다.
-1. **CSS 파일 선택** 버튼을 클릭합니다.
-2. CSS 파일을 선택합니다.
-3. 스타일이 즉시 적용됩니다.
+1. **CSS 관리** 버튼을 클릭해 커스텀 CSS 관리 패널을 엽니다.
+2. 패널 상단의 **활성화** 토글을 켭니다.
+3. **CSS 파일 불러오기** 버튼으로 CSS 파일을 선택합니다 (`.css`, 최대 1MiB).
+4. 스타일이 즉시 적용됩니다.
+
+관리 패널의 목록에는 이전에 불러온 CSS 파일이 최대 10개까지 남습니다.
+
+- 항목의 **적용** 버튼을 누르면 파일 선택 창 없이 해당 파일로 바로 전환됩니다.
+- 항목을 우클릭해 **목록에서 제거**를 선택할 수 있습니다 (파일 자체는 삭제되지 않습니다).
+- 적용할 수 없게 된 항목에는 배지가 붙습니다: **파일 없음**(이동·삭제됨), **사용 불가**(더 이상 일반 `.css` 파일이 아님), **용량 초과**(1MiB 초과).
CSS 스타일링에 대한 자세한 내용은 [커스텀 CSS 가이드](/docs/custom-css)를 참조하세요.
@@ -81,9 +88,9 @@ DM Note의 다양한 설정 옵션을 안내합니다.
JavaScript 플러그인을 로드하여 프로그램 기능을 확장합니다.
-1. **JS 플러그인 활성화** 토글을 켭니다.
-2. **플러그인 관리** 버튼을 클릭합니다.
-3. **JS 플러그인 추가** 버튼으로 파일을 선택합니다.
+1. **플러그인 관리** 버튼을 클릭해 관리 패널을 엽니다.
+2. 패널 상단의 **활성화** 토글을 켭니다.
+3. **플러그인 추가** 버튼으로 파일을 선택합니다.
플러그인 개발에 대한 자세한 내용은 [시작하기](/docs/getting-started)를 참조하세요.
diff --git a/src-tauri/gen/schemas/acl-manifests.json b/src-tauri/gen/schemas/acl-manifests.json
index 8196b09e..8bbcbf56 100644
--- a/src-tauri/gen/schemas/acl-manifests.json
+++ b/src-tauri/gen/schemas/acl-manifests.json
@@ -1 +1 @@
-{"__app-acl__":{"default_permission":null,"permissions":{"dmnote-allow-all":{"identifier":"dmnote-allow-all","description":"Full DM Note command access for renderer","commands":{"allow":["app_auto_update","app_bootstrap","app_cancel_editor_flush","app_open_external","app_quit","app_quit_after_editor_flush","app_restart","counter_animation_create","counter_animation_delete","counter_animation_list","counter_animation_update","css_get","css_get_use","css_load","css_reset","css_set_content","css_tab_clear","css_tab_get","css_tab_get_all","css_tab_load","css_tab_set","css_tab_toggle","css_toggle","custom_tabs_create","custom_tabs_delete","custom_tabs_list","custom_tabs_restore","custom_tabs_select","editor_commit","editor_get","editor_history_restore","font_load","get_cursor_settings","graph_positions_get","graph_positions_update","image_load","js_get","js_get_use","js_load","js_reload","js_remove_plugin","js_reset","js_set_content","js_set_plugin_enabled","js_toggle","key_sound_get_output_state","key_sound_get_status","key_sound_list_output_devices","key_sound_load_soundpack","key_sound_set_enabled","key_sound_set_latency_logging","key_sound_set_output_backend","key_sound_set_volume","key_sound_unload_soundpack","keys_get","keys_get_counters","keys_reset_all","keys_reset_counters","keys_reset_counters_mode","keys_reset_mode","keys_reset_single_counter","keys_set_counters","keys_set_mode","keys_update","keys_update_with_positions","knob_positions_get","knob_positions_update","layer_groups_get","layer_groups_update","note_tab_clear","note_tab_get","note_tab_get_all","note_tab_set","obs_regenerate_token","obs_start","obs_status","obs_stop","overlay_get","overlay_resize","overlay_set_anchor","overlay_set_lock","overlay_set_visible","plugin_bridge_send","plugin_bridge_send_to","plugin_storage_clear","plugin_storage_clear_by_prefix","plugin_storage_get","plugin_storage_has_data","plugin_storage_keys","plugin_storage_remove","plugin_storage_set","positions_get","positions_update","preset_load","preset_load_tab","preset_save","preset_save_tab","raw_input_subscribe","raw_input_unsubscribe","settings_get","settings_update","sound_delete","sound_list","sound_load","sound_load_original","sound_rename","sound_save_processed_wav","sound_set_enabled","sound_set_hidden","sound_update_processed_wav","stat_positions_get","stat_positions_update","window_close","window_minimize","window_open_devtools_all","window_show_main"],"deny":[]}}},"permission_sets":{},"global_scope_schema":null},"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"]},"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-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-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-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-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-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-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-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-internal-toggle-maximize"]},"permissions":{"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-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-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-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-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-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}}
\ No newline at end of file
+{"__app-acl__":{"default_permission":null,"permissions":{"dmnote-allow-all":{"identifier":"dmnote-allow-all","description":"Full DM Note command access for renderer","commands":{"allow":["app_auto_update","app_bootstrap","app_cancel_editor_flush","app_open_external","app_quit","app_quit_after_editor_flush","app_restart","counter_animation_create","counter_animation_delete","counter_animation_list","counter_animation_update","css_get","css_get_use","css_history_activate","css_history_get","css_history_remove","css_load","css_reset","css_set_content","css_tab_activate_history","css_tab_clear","css_tab_export","css_tab_get","css_tab_get_all","css_tab_load","css_tab_set","css_tab_toggle","css_toggle","custom_tabs_create","custom_tabs_delete","custom_tabs_list","custom_tabs_restore","custom_tabs_select","editor_commit","editor_get","editor_history_restore","font_load","get_cursor_settings","graph_positions_get","graph_positions_update","image_load","js_get","js_get_use","js_load","js_reload","js_remove_plugin","js_reset","js_set_content","js_set_plugin_enabled","js_toggle","key_sound_get_output_state","key_sound_get_status","key_sound_list_output_devices","key_sound_load_soundpack","key_sound_set_enabled","key_sound_set_latency_logging","key_sound_set_output_backend","key_sound_set_volume","key_sound_unload_soundpack","keys_get","keys_get_counters","keys_reset_all","keys_reset_counters","keys_reset_counters_mode","keys_reset_mode","keys_reset_single_counter","keys_set_counters","keys_set_mode","keys_update","keys_update_with_positions","knob_positions_get","knob_positions_update","layer_groups_get","layer_groups_update","note_tab_clear","note_tab_get","note_tab_get_all","note_tab_set","obs_regenerate_token","obs_start","obs_status","obs_stop","overlay_get","overlay_resize","overlay_set_anchor","overlay_set_lock","overlay_set_visible","plugin_bridge_send","plugin_bridge_send_to","plugin_storage_clear","plugin_storage_clear_by_prefix","plugin_storage_get","plugin_storage_has_data","plugin_storage_keys","plugin_storage_remove","plugin_storage_set","positions_get","positions_update","preset_load","preset_load_tab","preset_save","preset_save_tab","raw_input_subscribe","raw_input_unsubscribe","settings_get","settings_update","sound_delete","sound_list","sound_load","sound_load_original","sound_rename","sound_save_processed_wav","sound_set_enabled","sound_set_hidden","sound_update_processed_wav","stat_positions_get","stat_positions_update","window_close","window_minimize","window_open_devtools_all","window_show_main"],"deny":[]}}},"permission_sets":{},"global_scope_schema":null},"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"]},"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-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-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-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-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-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-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-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-internal-toggle-maximize"]},"permissions":{"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-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-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-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-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-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}}
\ No newline at end of file
diff --git a/src-tauri/permissions/dmnote-allow-all.json b/src-tauri/permissions/dmnote-allow-all.json
index 4c062d65..6c70500e 100644
--- a/src-tauri/permissions/dmnote-allow-all.json
+++ b/src-tauri/permissions/dmnote-allow-all.json
@@ -19,10 +19,15 @@
"counter_animation_update",
"css_get",
"css_get_use",
+ "css_history_activate",
+ "css_history_get",
+ "css_history_remove",
"css_load",
"css_reset",
"css_set_content",
+ "css_tab_activate_history",
"css_tab_clear",
+ "css_tab_export",
"css_tab_get",
"css_tab_get_all",
"css_tab_load",
diff --git a/src-tauri/src/commands/editor/css.rs b/src-tauri/src/commands/editor/css.rs
index 2b90ad74..fd770087 100644
--- a/src-tauri/src/commands/editor/css.rs
+++ b/src-tauri/src/commands/editor/css.rs
@@ -1,23 +1,30 @@
-use std::fs;
+use std::{
+ path::{Path, PathBuf},
+ time::{SystemTime, UNIX_EPOCH},
+};
use rfd::FileDialog;
use serde::Serialize;
-use tauri::{AppHandle, Emitter, State};
+use tauri::{AppHandle, State};
use crate::{
+ commands::editor::state::emit_best_effort,
+ custom_css::{
+ custom_css_settings_diff, history_paths_match, inspect_css_history_status,
+ normalize_custom_css_history, record_custom_css_load, touch_custom_css_history,
+ validate_css_path, CssHistoryErrorCode, CssPathError, CustomCssHistoryStatus,
+ ValidatedCssFile, MAX_CUSTOM_CSS_BYTES,
+ },
+ defaults::default_keys,
errors::CmdResult,
- models::{CustomCss, TabCss, TabCssOverrides},
- state::AppState,
+ models::{AppStoreData, CustomCss, CustomCssHistoryEntry, TabCss, TabCssOverrides},
+ state::{atomic_file::atomic_replace, AppState},
};
/// OBS 브릿지에 CSS 설정 변경을 settings_diff로 전달 (전체 스냅샷 브로드캐스트 방지)
fn notify_obs_css(state: &AppState) {
let snap = state.store.snapshot();
- let diff = serde_json::json!({
- "useCustomCSS": snap.use_custom_css,
- "customCSS": snap.custom_css,
- });
- state.notify_obs_settings_diff(diff);
+ state.notify_obs_settings_diff(custom_css_settings_diff(&snap));
}
#[derive(Serialize)]
@@ -43,6 +50,106 @@ pub struct CssLoadResponse {
pub path: Option,
}
+#[derive(Serialize, Clone)]
+#[serde(rename_all = "camelCase")]
+pub struct CustomCssHistoryItem {
+ pub path: String,
+ pub last_used_at: i64,
+ pub status: CustomCssHistoryStatus,
+}
+
+#[derive(Serialize)]
+pub struct CssActivateResponse {
+ pub success: bool,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub code: Option,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub content: Option,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub path: Option,
+}
+
+fn history_items(entries: &[CustomCssHistoryEntry]) -> Vec {
+ entries
+ .iter()
+ .map(|entry| CustomCssHistoryItem {
+ path: entry.path.clone(),
+ last_used_at: entry.last_used_at,
+ status: inspect_css_history_status(Path::new(&entry.path)),
+ })
+ .collect()
+}
+
+fn has_history_path(entries: &[CustomCssHistoryEntry], path: &str) -> bool {
+ entries
+ .iter()
+ .any(|entry| history_paths_match(&entry.path, path))
+}
+
+fn replace_tab_css_override(store: &mut AppStoreData, tab_id: &str, css: Option) {
+ if let Some(css) = css {
+ store.tab_css_overrides.insert(tab_id.to_string(), css);
+ } else {
+ store.tab_css_overrides.remove(tab_id);
+ }
+}
+
+fn current_unix_millis() -> i64 {
+ SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .map(|duration| i64::try_from(duration.as_millis()).unwrap_or(i64::MAX))
+ .unwrap_or(0)
+}
+
+fn log_css_rejection(operation: &str, path: &str, error: &CssPathError) {
+ log::warn!(
+ "[{operation}] Rejected CSS path code={} path={} detail={}",
+ error.code.as_str(),
+ path,
+ error.detail
+ );
+}
+
+fn activate_failure(code: CssHistoryErrorCode, path: String) -> CssActivateResponse {
+ CssActivateResponse {
+ success: false,
+ code: Some(code),
+ content: None,
+ path: Some(path),
+ }
+}
+
+// touch_history: 새 파일 불러오기만 히스토리 맨 앞에 추가.
+// 히스토리 항목 적용은 순서를 바꾸지 않음 (목록 순서 고정)
+fn commit_loaded_css(
+ state: &AppState,
+ loaded: &ValidatedCssFile,
+ touch_history: bool,
+) -> CmdResult<(CustomCss, bool)> {
+ let css = CustomCss {
+ path: Some(loaded.canonical_path.clone()),
+ content: loaded.content.clone(),
+ };
+ let timestamp = current_unix_millis();
+ let updated = state.store.update(|store| {
+ store.custom_css = css.clone();
+ if touch_history {
+ record_custom_css_load(
+ &mut store.custom_css_history,
+ loaded.canonical_path.clone(),
+ timestamp,
+ );
+ } else {
+ touch_custom_css_history(
+ &mut store.custom_css_history,
+ &loaded.canonical_path,
+ timestamp,
+ );
+ }
+ })?;
+ Ok((css, updated.use_custom_css))
+}
+
// ========== 탭별 CSS 응답 타입 ==========
#[derive(Serialize, Clone)]
@@ -87,6 +194,36 @@ pub struct TabCssSetResponse {
pub css: Option,
}
+#[derive(Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct TabCssActivateResponse {
+ pub success: bool,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub code: Option,
+ pub tab_id: String,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub css: Option,
+}
+
+#[derive(Debug, Clone, Copy, Serialize)]
+#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
+pub enum TabCssExportErrorCode {
+ NoTabCss,
+ IoError,
+}
+
+#[derive(Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct TabCssExportResponse {
+ pub success: bool,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub code: Option,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub error: Option,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub path: Option,
+}
+
#[tauri::command]
pub fn css_get(state: State<'_, AppState>) -> CmdResult {
Ok(state.store.snapshot().custom_css)
@@ -103,43 +240,41 @@ pub fn css_toggle(
app: AppHandle,
enabled: bool,
) -> CmdResult {
+ let _operation_guard = state.lock_css_operation();
state.store.update(|store| {
store.use_custom_css = enabled;
})?;
-
- app.emit("css:use", &CssToggleResponse { enabled })?;
-
+ let css = state.store.snapshot().custom_css;
if enabled {
- let css = state.store.snapshot().custom_css;
- app.emit("css:content", &css)?;
-
- // CSS 핫리로딩: 활성화 시 파일 워칭 시작
+ state.unwatch_global_css();
if let Some(path) = &css.path {
if let Err(err) = state.watch_global_css(path) {
log::warn!("[css_toggle] Failed to start watching: {}", err);
}
}
} else {
- // CSS 핫리로딩: 비활성화 시 워칭 중지
state.unwatch_global_css();
}
+ emit_best_effort(&app, "css:use", &CssToggleResponse { enabled });
+ if enabled {
+ emit_best_effort(&app, "css:content", &css);
+ }
notify_obs_css(&state);
Ok(CssToggleResponse { enabled })
}
#[tauri::command]
pub fn css_reset(state: State<'_, AppState>, app: AppHandle) -> CmdResult<()> {
- // CSS 핫리로딩: 전역 CSS 워칭 중지
- state.unwatch_global_css();
-
+ let _operation_guard = state.lock_css_operation();
state.store.update(|store| {
store.use_custom_css = false;
store.custom_css = CustomCss::default();
})?;
+ state.unwatch_global_css();
- app.emit("css:use", &CssToggleResponse { enabled: false })?;
- app.emit("css:content", &CustomCss::default())?;
+ emit_best_effort(&app, "css:use", &CssToggleResponse { enabled: false });
+ emit_best_effort(&app, "css:content", &CustomCss::default());
notify_obs_css(&state);
Ok(())
@@ -151,14 +286,21 @@ pub fn css_set_content(
app: AppHandle,
content: String,
) -> CmdResult {
+ let _operation_guard = state.lock_css_operation();
+ if content.len() as u64 > MAX_CUSTOM_CSS_BYTES {
+ return Ok(CssSetContentResponse {
+ success: false,
+ error: Some(CssHistoryErrorCode::TooLarge.as_str().to_string()),
+ });
+ }
let mut current = state.store.snapshot().custom_css;
- current.content = content.clone();
+ current.content = content;
state.store.update(|store| {
store.custom_css = current.clone();
})?;
- app.emit("css:content", ¤t)?;
+ emit_best_effort(&app, "css:content", ¤t);
notify_obs_css(&state);
Ok(CssSetContentResponse {
@@ -180,44 +322,121 @@ pub fn css_load(state: State<'_, AppState>, app: AppHandle) -> CmdResult {
- // 이전 파일 워칭 중지
- state.unwatch_global_css();
+ let selected_path = path.to_string_lossy().to_string();
+ let _operation_guard = state.lock_css_operation();
+ let loaded = match validate_css_path(&path) {
+ Ok(loaded) => loaded,
+ Err(error) => {
+ log_css_rejection("css_load", &selected_path, &error);
+ return Ok(CssLoadResponse {
+ success: false,
+ error: Some(error.code.as_str().to_string()),
+ content: None,
+ path: Some(selected_path),
+ });
+ }
+ };
+ let (css, use_custom_css) = commit_loaded_css(&state, &loaded, true)?;
+ state.authorize_css_path(&loaded.canonical_path);
+ state.unwatch_global_css();
+ if use_custom_css {
+ if let Err(error) = state.watch_global_css(&loaded.canonical_path) {
+ log::warn!("[css_load] Failed to start watching: {error}");
+ }
+ }
+ emit_best_effort(&app, "css:content", &css);
+ notify_obs_css(&state);
- let css = CustomCss {
- path: Some(path_string.clone()),
- content: content.clone(),
- };
- state.store.update(|store| {
- store.custom_css = css.clone();
- })?;
+ Ok(CssLoadResponse {
+ success: true,
+ error: None,
+ content: Some(loaded.content),
+ path: Some(loaded.canonical_path),
+ })
+}
- app.emit("css:content", &css)?;
+#[tauri::command]
+pub fn css_history_get(state: State<'_, AppState>) -> CmdResult> {
+ Ok(history_items(&state.store.snapshot().custom_css_history))
+}
- // CSS 핫리로딩: 새 파일 워칭 시작 (use_custom_css가 활성화된 경우에만)
- if state.store.snapshot().use_custom_css {
- if let Err(err) = state.watch_global_css(&path_string) {
- log::warn!("[css_load] Failed to start watching: {}", err);
- }
- }
+#[tauri::command]
+pub fn css_history_activate(
+ state: State<'_, AppState>,
+ app: AppHandle,
+ path: String,
+) -> CmdResult {
+ let _operation_guard = state.lock_css_operation();
+ let authorized = state
+ .store
+ .with_state(|store| has_history_path(&store.custom_css_history, &path));
+ if !authorized {
+ log::warn!(
+ "[css_history_activate] Rejected CSS path code={} path={}",
+ CssHistoryErrorCode::PathNotAuthorized.as_str(),
+ path
+ );
+ return Ok(activate_failure(
+ CssHistoryErrorCode::PathNotAuthorized,
+ path,
+ ));
+ }
- notify_obs_css(&state);
- Ok(CssLoadResponse {
- success: true,
- error: None,
- content: Some(content),
- path: Some(path_string),
- })
+ let loaded = match validate_css_path(Path::new(&path)) {
+ Ok(loaded) => loaded,
+ Err(error) => {
+ log_css_rejection("css_history_activate", &path, &error);
+ return Ok(activate_failure(error.code, path));
+ }
+ };
+ let canonical_authorized = state
+ .store
+ .with_state(|store| has_history_path(&store.custom_css_history, &loaded.canonical_path));
+ if !canonical_authorized {
+ log::warn!(
+ "[css_history_activate] Rejected canonical CSS path code={} path={}",
+ CssHistoryErrorCode::PathNotAuthorized.as_str(),
+ loaded.canonical_path
+ );
+ return Ok(activate_failure(
+ CssHistoryErrorCode::PathNotAuthorized,
+ path,
+ ));
+ }
+
+ let (css, use_custom_css) = commit_loaded_css(&state, &loaded, false)?;
+ state.authorize_css_path(&loaded.canonical_path);
+ state.unwatch_global_css();
+ if use_custom_css {
+ if let Err(error) = state.watch_global_css(&loaded.canonical_path) {
+ log::warn!("[css_history_activate] Failed to start watching: {error}");
}
- Err(err) => Ok(CssLoadResponse {
- success: false,
- error: Some(err.to_string()),
- content: None,
- path: Some(path_string),
- }),
}
+ emit_best_effort(&app, "css:content", &css);
+ notify_obs_css(&state);
+
+ Ok(CssActivateResponse {
+ success: true,
+ code: None,
+ content: Some(loaded.content),
+ path: Some(loaded.canonical_path),
+ })
+}
+
+#[tauri::command]
+pub fn css_history_remove(
+ state: State<'_, AppState>,
+ path: String,
+) -> CmdResult> {
+ let operation_guard = state.lock_css_operation();
+ let updated = state.store.update(|store| {
+ store
+ .custom_css_history
+ .retain(|entry| !history_paths_match(&entry.path, &path));
+ normalize_custom_css_history(&mut store.custom_css_history);
+ })?;
+ drop(operation_guard);
+ Ok(history_items(&updated.custom_css_history))
}
// ========== 탭별 CSS 커맨드 ==========
@@ -254,53 +473,48 @@ pub fn css_tab_load(
});
};
- let path_string = path.to_string_lossy().to_string();
- match fs::read_to_string(&path) {
- Ok(content) => {
- // 이전 탭 CSS 워칭 중지
- state.unwatch_tab_css(&tab_id);
-
- let tab_css = TabCss {
- path: Some(path_string.clone()),
- content: content.clone(),
- enabled: true,
- };
-
- state.store.update(|store| {
- store
- .tab_css_overrides
- .insert(tab_id.clone(), tab_css.clone());
- })?;
-
- let response = TabCssResponse {
- tab_id: tab_id.clone(),
- css: Some(tab_css.clone()),
- };
- app.emit("tabCss:changed", &response)?;
-
- // CSS 핫리로딩: 새 탭 CSS 파일 워칭 시작
- if let Err(err) = state.watch_tab_css(&path_string, &tab_id) {
- log::warn!(
- "[css_tab_load] Failed to start watching tab {}: {}",
- tab_id,
- err
- );
- }
-
- Ok(TabCssLoadResponse {
- success: true,
- error: None,
+ let selected_path = path.to_string_lossy().to_string();
+ let _operation_guard = state.lock_css_operation();
+ let loaded = match validate_css_path(&path) {
+ Ok(loaded) => loaded,
+ Err(error) => {
+ log_css_rejection("css_tab_load", &selected_path, &error);
+ return Ok(TabCssLoadResponse {
+ success: false,
+ error: Some(error.code.as_str().to_string()),
tab_id,
- css: Some(tab_css),
- })
+ css: None,
+ });
}
- Err(err) => Ok(TabCssLoadResponse {
- success: false,
- error: Some(err.to_string()),
- tab_id,
- css: None,
- }),
+ };
+ let tab_css = TabCss {
+ path: Some(loaded.canonical_path.clone()),
+ content: loaded.content,
+ enabled: true,
+ };
+ state.store.update(|store| {
+ replace_tab_css_override(store, &tab_id, Some(tab_css.clone()));
+ })?;
+ state.authorize_css_path(&loaded.canonical_path);
+ state.unwatch_tab_css(&tab_id);
+ if let Err(error) = state.watch_tab_css(&loaded.canonical_path, &tab_id) {
+ log::warn!("[css_tab_load] Failed to watch tab {tab_id}: {error}");
}
+ emit_best_effort(
+ &app,
+ "tabCss:changed",
+ &TabCssResponse {
+ tab_id: tab_id.clone(),
+ css: Some(tab_css.clone()),
+ },
+ );
+
+ Ok(TabCssLoadResponse {
+ success: true,
+ error: None,
+ tab_id,
+ css: Some(tab_css),
+ })
}
/// 특정 탭의 CSS 제거 (전역 CSS로 폴백)
@@ -310,18 +524,20 @@ pub fn css_tab_clear(
app: AppHandle,
tab_id: String,
) -> CmdResult {
- // CSS 핫리로딩: 탭 CSS 워칭 중지
- state.unwatch_tab_css(&tab_id);
-
+ let _operation_guard = state.lock_css_operation();
state.store.update(|store| {
- store.tab_css_overrides.remove(&tab_id);
+ replace_tab_css_override(store, &tab_id, None);
})?;
+ state.unwatch_tab_css(&tab_id);
- let response = TabCssResponse {
- tab_id: tab_id.clone(),
- css: None,
- };
- app.emit("tabCss:changed", &response)?;
+ emit_best_effort(
+ &app,
+ "tabCss:changed",
+ &TabCssResponse {
+ tab_id: tab_id.clone(),
+ css: None,
+ },
+ );
Ok(TabCssClearResponse {
success: true,
@@ -337,39 +553,30 @@ pub fn css_tab_set(
tab_id: String,
css: Option,
) -> CmdResult {
- // 이전 탭 CSS 워칭 중지
+ let _operation_guard = state.lock_css_operation();
+ let css = css.map(|tab_css| prepare_tab_css_for_set(&state, tab_css));
+ state.store.update(|store| {
+ replace_tab_css_override(store, &tab_id, css.clone());
+ })?;
state.unwatch_tab_css(&tab_id);
-
- if let Some(ref tab_css) = css {
- state.store.update(|store| {
- store
- .tab_css_overrides
- .insert(tab_id.clone(), tab_css.clone());
- })?;
-
- // CSS 핫리로딩: 파일이 있고 enabled인 경우 워칭 시작
- if tab_css.enabled {
- if let Some(path) = &tab_css.path {
- if let Err(err) = state.watch_tab_css(path, &tab_id) {
- log::warn!(
- "[css_tab_set] Failed to start watching tab {}: {}",
- tab_id,
- err
- );
- }
- }
+ if let Some(path) = css
+ .as_ref()
+ .filter(|tab_css| tab_css.enabled)
+ .and_then(|tab_css| tab_css.path.as_deref())
+ {
+ if let Err(error) = state.watch_tab_css(path, &tab_id) {
+ log::warn!("[css_tab_set] Failed to watch tab {tab_id}: {error}");
}
- } else {
- state.store.update(|store| {
- store.tab_css_overrides.remove(&tab_id);
- })?;
}
- let response = TabCssResponse {
- tab_id: tab_id.clone(),
- css: css.clone(),
- };
- app.emit("tabCss:changed", &response)?;
+ emit_best_effort(
+ &app,
+ "tabCss:changed",
+ &TabCssResponse {
+ tab_id: tab_id.clone(),
+ css: css.clone(),
+ },
+ );
Ok(TabCssSetResponse {
success: true,
@@ -386,6 +593,7 @@ pub fn css_tab_toggle(
tab_id: String,
enabled: bool,
) -> CmdResult {
+ let _operation_guard = state.lock_css_operation();
let mut updated_css: Option = None;
state.store.update(|store| {
@@ -406,7 +614,7 @@ pub fn css_tab_toggle(
}
})?;
- // CSS 핫리로딩: 활성화/비활성화에 따른 워칭 관리
+ state.unwatch_tab_css(&tab_id);
if let Some(ref css) = updated_css {
if enabled {
if let Some(path) = &css.path {
@@ -418,16 +626,17 @@ pub fn css_tab_toggle(
);
}
}
- } else {
- state.unwatch_tab_css(&tab_id);
}
}
- let response = TabCssResponse {
- tab_id: tab_id.clone(),
- css: updated_css,
- };
- app.emit("tabCss:changed", &response)?;
+ emit_best_effort(
+ &app,
+ "tabCss:changed",
+ &TabCssResponse {
+ tab_id: tab_id.clone(),
+ css: updated_css,
+ },
+ );
Ok(TabCssToggleResponse {
success: true,
@@ -435,3 +644,338 @@ pub fn css_tab_toggle(
enabled,
})
}
+
+fn prepare_tab_css_for_set(state: &AppState, css: TabCss) -> TabCss {
+ prepare_tab_css_for_set_with(css, |path| state.is_css_path_authorized(path))
+}
+
+fn prepare_tab_css_for_set_with(
+ mut css: TabCss,
+ is_authorized: impl FnOnce(&str) -> bool,
+) -> TabCss {
+ let Some(path) = css.path.clone() else {
+ return css;
+ };
+ if !is_authorized(&path) {
+ log::warn!(
+ "[css_tab_set] Dropped unauthorized CSS path path={} code={}",
+ path,
+ CssHistoryErrorCode::PathNotAuthorized.as_str()
+ );
+ css.path = None;
+ return css;
+ }
+
+ match validate_css_path(Path::new(&path)) {
+ Ok(loaded) => css.path = Some(loaded.canonical_path),
+ Err(error) => {
+ log_css_rejection("css_tab_set", &path, &error);
+ css.path = None;
+ }
+ }
+ css
+}
+
+fn is_valid_tab_id(state: &AppState, tab_id: &str) -> bool {
+ default_keys().contains_key(tab_id)
+ || state
+ .store
+ .with_state(|store| store.custom_tabs.iter().any(|tab| tab.id == tab_id))
+}
+
+fn tab_activate_failure(
+ tab_id: String,
+ code: Option,
+) -> TabCssActivateResponse {
+ TabCssActivateResponse {
+ success: false,
+ code,
+ tab_id,
+ css: None,
+ }
+}
+
+#[tauri::command]
+pub fn css_tab_activate_history(
+ state: State<'_, AppState>,
+ app: AppHandle,
+ tab_id: String,
+ path: String,
+) -> CmdResult {
+ let _operation_guard = state.lock_css_operation();
+ let authorized = state
+ .store
+ .with_state(|store| has_history_path(&store.custom_css_history, &path));
+ if !authorized {
+ log::warn!(
+ "[css_tab_activate_history] Rejected CSS path code={} path={}",
+ CssHistoryErrorCode::PathNotAuthorized.as_str(),
+ path
+ );
+ return Ok(tab_activate_failure(
+ tab_id,
+ Some(CssHistoryErrorCode::PathNotAuthorized),
+ ));
+ }
+
+ let loaded = match validate_css_path(Path::new(&path)) {
+ Ok(loaded) => loaded,
+ Err(error) => {
+ log_css_rejection("css_tab_activate_history", &path, &error);
+ return Ok(tab_activate_failure(tab_id, Some(error.code)));
+ }
+ };
+ let canonical_authorized = state
+ .store
+ .with_state(|store| has_history_path(&store.custom_css_history, &loaded.canonical_path));
+ if !canonical_authorized {
+ log::warn!(
+ "[css_tab_activate_history] Rejected canonical CSS path code={} path={}",
+ CssHistoryErrorCode::PathNotAuthorized.as_str(),
+ loaded.canonical_path
+ );
+ return Ok(tab_activate_failure(
+ tab_id,
+ Some(CssHistoryErrorCode::PathNotAuthorized),
+ ));
+ }
+ if !is_valid_tab_id(&state, &tab_id) {
+ log::warn!("[css_tab_activate_history] Rejected unknown tab id={tab_id}");
+ return Ok(tab_activate_failure(tab_id, None));
+ }
+
+ let tab_css = TabCss {
+ path: Some(loaded.canonical_path.clone()),
+ content: loaded.content,
+ enabled: true,
+ };
+ let timestamp = current_unix_millis();
+ state.store.update(|store| {
+ replace_tab_css_override(store, &tab_id, Some(tab_css.clone()));
+ touch_custom_css_history(
+ &mut store.custom_css_history,
+ &loaded.canonical_path,
+ timestamp,
+ );
+ })?;
+ state.authorize_css_path(&loaded.canonical_path);
+ state.unwatch_tab_css(&tab_id);
+ if let Err(error) = state.watch_tab_css(&loaded.canonical_path, &tab_id) {
+ log::warn!("[css_tab_activate_history] Failed to watch tab {tab_id}: {error}");
+ }
+ emit_best_effort(
+ &app,
+ "tabCss:changed",
+ &TabCssResponse {
+ tab_id: tab_id.clone(),
+ css: Some(tab_css.clone()),
+ },
+ );
+
+ Ok(TabCssActivateResponse {
+ success: true,
+ code: None,
+ tab_id,
+ css: Some(tab_css),
+ })
+}
+
+fn no_tab_css_export() -> TabCssExportResponse {
+ TabCssExportResponse {
+ success: false,
+ code: Some(TabCssExportErrorCode::NoTabCss),
+ error: None,
+ path: None,
+ }
+}
+
+fn ensure_css_extension(mut path: PathBuf) -> PathBuf {
+ let has_css_extension = path
+ .extension()
+ .and_then(|extension| extension.to_str())
+ .is_some_and(|extension| extension.eq_ignore_ascii_case("css"));
+ if !has_css_extension {
+ path.set_extension("css");
+ }
+ path
+}
+
+fn write_tab_css_export(path: &Path, content: &str) -> anyhow::Result<()> {
+ atomic_replace(path, content.as_bytes(), "tab-css-export")
+}
+
+#[tauri::command]
+pub fn css_tab_export(
+ state: State<'_, AppState>,
+ tab_id: String,
+) -> CmdResult {
+ let initial_css = state
+ .store
+ .with_state(|store| store.tab_css_overrides.get(&tab_id).cloned());
+ let Some(initial_css) = initial_css.filter(|css| !css.content.is_empty()) else {
+ return Ok(no_tab_css_export());
+ };
+ let default_file_name = initial_css
+ .path
+ .as_deref()
+ .and_then(|path| Path::new(path).file_name())
+ .map(|name| name.to_string_lossy().to_string())
+ .unwrap_or_else(|| format!("{tab_id}.css"));
+ let selected_path = FileDialog::new()
+ .set_file_name(&default_file_name)
+ .add_filter("CSS", &["css"])
+ .save_file();
+ let Some(selected_path) = selected_path else {
+ return Ok(TabCssExportResponse {
+ success: false,
+ code: None,
+ error: None,
+ path: None,
+ });
+ };
+ let export_path = ensure_css_extension(selected_path);
+ let current_content = state.store.with_state(|store| {
+ store
+ .tab_css_overrides
+ .get(&tab_id)
+ .map(|css| css.content.clone())
+ });
+ let Some(current_content) = current_content.filter(|content| !content.is_empty()) else {
+ return Ok(no_tab_css_export());
+ };
+
+ if let Err(error) = write_tab_css_export(&export_path, ¤t_content) {
+ return Ok(TabCssExportResponse {
+ success: false,
+ code: Some(TabCssExportErrorCode::IoError),
+ error: Some(format!("{error:#}")),
+ path: None,
+ });
+ }
+
+ Ok(TabCssExportResponse {
+ success: true,
+ code: None,
+ error: None,
+ path: Some(export_path.to_string_lossy().to_string()),
+ })
+}
+
+#[cfg(test)]
+mod tests {
+ use super::{
+ ensure_css_extension, prepare_tab_css_for_set_with, replace_tab_css_override,
+ write_tab_css_export,
+ };
+ use crate::models::{AppStoreData, TabCss};
+ use parking_lot::Mutex;
+ use std::{
+ fs,
+ path::Path,
+ sync::{mpsc, Arc},
+ thread,
+ };
+
+ fn test_directory(label: &str) -> std::path::PathBuf {
+ std::env::temp_dir().join(format!(
+ "dmnote-css-command-{label}-{}",
+ uuid::Uuid::new_v4()
+ ))
+ }
+
+ #[test]
+ fn unauthorized_tab_set_preserves_content_without_persisting_path() {
+ let css = TabCss {
+ path: Some("/tmp/not-authorized.css".to_string()),
+ content: "preserved".to_string(),
+ enabled: true,
+ };
+
+ let prepared = prepare_tab_css_for_set_with(css, |_| false);
+
+ assert_eq!(prepared.path, None);
+ assert_eq!(prepared.content, "preserved");
+ assert!(prepared.enabled);
+ }
+
+ #[test]
+ fn authorized_tab_set_persists_the_canonical_path() {
+ let root = test_directory("authorized");
+ fs::create_dir_all(&root).unwrap();
+ let path = root.join("theme.css");
+ fs::write(&path, "body {}").unwrap();
+ let css = TabCss {
+ path: Some(path.to_string_lossy().to_string()),
+ content: "preserved".to_string(),
+ enabled: true,
+ };
+
+ let prepared = prepare_tab_css_for_set_with(css, |_| true);
+
+ let canonical = fs::canonicalize(&path)
+ .unwrap()
+ .to_string_lossy()
+ .to_string();
+ assert_eq!(prepared.path.as_deref(), Some(canonical.as_str()));
+ assert_eq!(prepared.content, "preserved");
+ let _ = fs::remove_dir_all(root);
+ }
+
+ #[test]
+ fn export_corrects_extension_and_atomically_replaces_existing_file() {
+ let root = test_directory("export");
+ fs::create_dir_all(&root).unwrap();
+ let selected = root.join("theme.txt");
+ let export = ensure_css_extension(selected);
+ fs::write(&export, "old").unwrap();
+
+ write_tab_css_export(&export, "new").unwrap();
+
+ assert_eq!(
+ export.extension().and_then(|value| value.to_str()),
+ Some("css")
+ );
+ assert_eq!(fs::read_to_string(&export).unwrap(), "new");
+ assert_eq!(fs::read_dir(&root).unwrap().count(), 1);
+ assert!(Path::new(&export).is_file());
+ let _ = fs::remove_dir_all(root);
+ }
+
+ #[test]
+ fn clear_waiting_on_activation_lock_wins_with_the_last_commit() {
+ let operation_lock = Arc::new(Mutex::new(()));
+ let store = Arc::new(Mutex::new(AppStoreData::default()));
+ let (locked_tx, locked_rx) = mpsc::channel();
+ let (continue_tx, continue_rx) = mpsc::channel();
+
+ let activation_lock = operation_lock.clone();
+ let activation_store = store.clone();
+ let activate = thread::spawn(move || {
+ let _guard = activation_lock.lock();
+ locked_tx.send(()).unwrap();
+ continue_rx.recv().unwrap();
+ replace_tab_css_override(
+ &mut activation_store.lock(),
+ "4key",
+ Some(TabCss {
+ path: Some("/tmp/theme.css".to_string()),
+ content: "active".to_string(),
+ enabled: true,
+ }),
+ );
+ });
+ locked_rx.recv().unwrap();
+
+ let clear_lock = operation_lock.clone();
+ let clear_store = store.clone();
+ let clear = thread::spawn(move || {
+ let _guard = clear_lock.lock();
+ replace_tab_css_override(&mut clear_store.lock(), "4key", None);
+ });
+ continue_tx.send(()).unwrap();
+ activate.join().unwrap();
+ clear.join().unwrap();
+
+ assert!(!store.lock().tab_css_overrides.contains_key("4key"));
+ }
+}
diff --git a/src-tauri/src/commands/keys/keys.rs b/src-tauri/src/commands/keys/keys.rs
index 5c79fb86..ad628ab1 100644
--- a/src-tauri/src/commands/keys/keys.rs
+++ b/src-tauri/src/commands/keys/keys.rs
@@ -413,6 +413,8 @@ pub fn keys_reset_all(state: State<'_, AppState>, app: AppHandle) -> CmdResult, app: AppHandle) -> CmdResult, app: AppHandle) -> CmdResult = Vec::new();
let tab_note_overrides = crate::models::TabNoteOverrides::new();
- for tab_id in &cleared_tab_css_ids {
- state.unwatch_tab_css(tab_id);
- }
if let Err(error) = state.emit_settings_changed(&settings_diff, &app) {
log::error!("[Keys] failed to publish reset settings: {error:#}");
}
diff --git a/src-tauri/src/commands/layout/settings.rs b/src-tauri/src/commands/layout/settings.rs
index 060cf403..c01c27f4 100644
--- a/src-tauri/src/commands/layout/settings.rs
+++ b/src-tauri/src/commands/layout/settings.rs
@@ -17,7 +17,22 @@ pub fn settings_update(
app: AppHandle,
patch: SettingsPatchInput,
) -> CmdResult {
+ let css_changed = patch.use_custom_css.is_some() || patch.custom_css.is_some();
+ let operation_guard = if css_changed {
+ Some(state.lock_css_operation())
+ } else {
+ None
+ };
+ let previous = if css_changed {
+ Some(state.store.snapshot())
+ } else {
+ None
+ };
let diff = state.settings.apply_patch(patch)?;
+ if let Some(previous) = previous.as_ref() {
+ state.resync_global_css_watcher(previous, &state.store.snapshot());
+ }
+ drop(operation_guard);
state.emit_settings_changed(&diff, &app)?;
Ok(diff.full.unwrap_or_else(|| state.settings.snapshot()))
}
diff --git a/src-tauri/src/commands/preset/load.rs b/src-tauri/src/commands/preset/load.rs
index 8024e58f..ca5478ef 100644
--- a/src-tauri/src/commands/preset/load.rs
+++ b/src-tauri/src/commands/preset/load.rs
@@ -14,14 +14,15 @@ use crate::{
css::TabCssResponse,
state::{emit_best_effort, publish_editor_change},
},
+ custom_css::validate_css_path,
defaults::{default_keys, default_positions},
errors::{CmdResult, CommandError},
models::{
AppStoreData, CommittedEditorChange, CustomCss, CustomCssPatch, CustomJs, CustomJsPatch,
EditorCommitOrigin, EditorField, FontSettings, FontType, GradientSpec, GraphPositions,
KeyMappings, KeyPosition, KeyPositions, KnobPositions, LayerGroups, NoteSettings,
- NoteSettingsPatch, SettingsPatchInput, StatPositions, TabCssOverrides, TabNoteSettings,
- SHADOW_BLUR_MAX, SHADOW_BLUR_MIN, SHADOW_OFFSET_MAX, SHADOW_OFFSET_MIN,
+ NoteSettingsPatch, SettingsPatchInput, StatPositions, TabCss, TabCssOverrides,
+ TabNoteSettings, SHADOW_BLUR_MAX, SHADOW_BLUR_MIN, SHADOW_OFFSET_MAX, SHADOW_OFFSET_MIN,
},
services::settings::apply_patch_to_store,
state::AppState,
@@ -252,7 +253,9 @@ pub fn preset_load(state: State<'_, AppState>, app: AppHandle) -> CmdResult, app: AppHandle) -> CmdResult, app: AppHandle) -> CmdResult, app: AppHandle) -> CmdResult, app: AppHandle) -> CmdResult, app: AppHandle) -> CmdResult css.path = Some(loaded.canonical_path),
+ Err(error) => {
+ log::warn!(
+ "[{operation}] Dropped invalid global CSS path code={} path={} detail={}",
+ error.code.as_str(),
+ path,
+ error.detail
+ );
+ css.path = None;
+ }
+ }
+}
+
+fn normalize_imported_tab_css(css: &mut TabCss, operation: &str) {
+ let Some(path) = css.path.clone() else {
+ return;
+ };
+ match validate_css_path(Path::new(&path)) {
+ Ok(loaded) => css.path = Some(loaded.canonical_path),
+ Err(error) => {
+ log::warn!(
+ "[{operation}] Dropped invalid tab CSS path code={} path={} detail={}",
+ error.code.as_str(),
+ path,
+ error.detail
+ );
+ css.path = None;
+ }
+ }
+}
+
+fn normalize_imported_tab_css_overrides(
+ mut overrides: TabCssOverrides,
+ operation: &str,
+) -> TabCssOverrides {
+ for css in overrides.values_mut() {
+ normalize_imported_tab_css(css, operation);
+ }
+ overrides
+}
+
fn choose_tab_preset_source_tab(
keys: &KeyMappings,
selected_key_type: Option<&str>,
@@ -1409,9 +1463,43 @@ mod tests {
use super::*;
use crate::{
defaults::{default_keys, default_positions},
- models::{CustomFont, JsPlugin, KnobPosition},
+ models::{CustomCssHistoryEntry, CustomFont, JsPlugin, KnobPosition},
};
+ #[test]
+ fn full_preset_settings_patch_preserves_custom_css_history() {
+ let history = vec![CustomCssHistoryEntry {
+ path: "/tmp/preserved.css".to_string(),
+ loaded_at: 123,
+ last_used_at: 123,
+ }];
+ let mut store = AppStoreData {
+ custom_css_history: history.clone(),
+ ..AppStoreData::default()
+ };
+ let mut preset = PresetFile {
+ use_custom_css: Some(true),
+ custom_css: Some(CustomCss {
+ path: Some("/tmp/preset.css".to_string()),
+ content: "body {}".to_string(),
+ }),
+ ..PresetFile::default()
+ };
+ let resolved = resolve_full_preset_settings(&mut preset, &store);
+ let patch = SettingsPatchInput {
+ use_custom_css: Some(resolved.use_custom_css),
+ custom_css: Some(CustomCssPatch {
+ path: Some(resolved.custom_css.path),
+ content: Some(resolved.custom_css.content),
+ }),
+ ..SettingsPatchInput::default()
+ };
+
+ apply_patch_to_store(&mut store, &patch);
+
+ assert_eq!(store.custom_css_history, history);
+ }
+
#[test]
fn preset_source_bytes_remain_unchanged_on_success_and_parse_failure() {
let temp_dir = std::env::temp_dir().join(format!(
diff --git a/src-tauri/src/commands/preset/mod.rs b/src-tauri/src/commands/preset/mod.rs
index 60f0281c..858ce86b 100644
--- a/src-tauri/src/commands/preset/mod.rs
+++ b/src-tauri/src/commands/preset/mod.rs
@@ -274,6 +274,13 @@ mod tests {
assert!(preset.tab_css_overrides.is_none());
}
+ #[test]
+ fn preset_wire_schema_excludes_custom_css_history() {
+ let serialized = serde_json::to_value(PresetFile::default()).unwrap();
+
+ assert!(serialized.get("customCssHistory").is_none());
+ }
+
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct PreFeaturePresetPosition {
diff --git a/src-tauri/src/custom_css.rs b/src-tauri/src/custom_css.rs
new file mode 100644
index 00000000..5cfcc992
--- /dev/null
+++ b/src-tauri/src/custom_css.rs
@@ -0,0 +1,461 @@
+use std::{
+ collections::HashSet,
+ fs::{self, File},
+ io::Read,
+ path::{Path, PathBuf},
+};
+
+use serde::Serialize;
+
+use crate::{
+ models::{AppStoreData, CustomCssHistoryEntry},
+ state::local_asset_path::path_identity_key,
+};
+
+pub(crate) const MAX_CUSTOM_CSS_BYTES: u64 = 1024 * 1024;
+pub(crate) const MAX_CUSTOM_CSS_HISTORY_ENTRIES: usize = 10;
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
+#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
+pub enum CssHistoryErrorCode {
+ PathNotAuthorized,
+ NotFound,
+ NotRegularFile,
+ InvalidExtension,
+ TooLarge,
+ InvalidUtf8,
+ IoError,
+}
+
+impl CssHistoryErrorCode {
+ pub(crate) fn as_str(self) -> &'static str {
+ match self {
+ Self::PathNotAuthorized => "PATH_NOT_AUTHORIZED",
+ Self::NotFound => "NOT_FOUND",
+ Self::NotRegularFile => "NOT_REGULAR_FILE",
+ Self::InvalidExtension => "INVALID_EXTENSION",
+ Self::TooLarge => "TOO_LARGE",
+ Self::InvalidUtf8 => "INVALID_UTF8",
+ Self::IoError => "IO_ERROR",
+ }
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub enum CustomCssHistoryStatus {
+ Available,
+ Missing,
+ Invalid,
+ TooLarge,
+}
+
+#[derive(Debug)]
+pub(crate) struct CssPathError {
+ pub(crate) code: CssHistoryErrorCode,
+ pub(crate) detail: String,
+}
+
+impl CssPathError {
+ fn new(code: CssHistoryErrorCode, detail: impl Into) -> Self {
+ Self {
+ code,
+ detail: detail.into(),
+ }
+ }
+}
+
+#[derive(Debug)]
+pub(crate) struct ValidatedCssFile {
+ pub(crate) canonical_path: String,
+ pub(crate) content: String,
+}
+
+pub(crate) fn validate_css_path(path: &Path) -> Result {
+ let canonical = canonicalize_css_path(path)?;
+ let metadata = fs::metadata(&canonical).map_err(map_io_error)?;
+ validate_css_metadata(&canonical, &metadata)?;
+
+ let file = File::open(&canonical).map_err(map_io_error)?;
+ let open_metadata = file.metadata().map_err(map_io_error)?;
+ validate_css_metadata(&canonical, &open_metadata)?;
+
+ let mut bytes =
+ Vec::with_capacity((open_metadata.len().min(MAX_CUSTOM_CSS_BYTES) + 1) as usize);
+ file.take(MAX_CUSTOM_CSS_BYTES + 1)
+ .read_to_end(&mut bytes)
+ .map_err(map_io_error)?;
+ if bytes.len() as u64 > MAX_CUSTOM_CSS_BYTES {
+ return Err(CssPathError::new(
+ CssHistoryErrorCode::TooLarge,
+ format!("CSS file exceeds {MAX_CUSTOM_CSS_BYTES} bytes"),
+ ));
+ }
+
+ let content = String::from_utf8(bytes)
+ .map_err(|error| CssPathError::new(CssHistoryErrorCode::InvalidUtf8, error.to_string()))?;
+ let canonical_path = canonical.into_os_string().into_string().map_err(|_| {
+ CssPathError::new(CssHistoryErrorCode::IoError, "CSS path is not valid UTF-8")
+ })?;
+
+ Ok(ValidatedCssFile {
+ canonical_path,
+ content,
+ })
+}
+
+pub(crate) fn inspect_css_history_status(path: &Path) -> CustomCssHistoryStatus {
+ let canonical = match canonicalize_css_path(path) {
+ Ok(path) => path,
+ Err(error) if error.code == CssHistoryErrorCode::NotFound => {
+ return CustomCssHistoryStatus::Missing;
+ }
+ Err(_) => return CustomCssHistoryStatus::Invalid,
+ };
+ let metadata = match fs::metadata(&canonical) {
+ Ok(metadata) => metadata,
+ Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
+ return CustomCssHistoryStatus::Missing;
+ }
+ Err(_) => return CustomCssHistoryStatus::Invalid,
+ };
+ if !metadata.is_file() || !has_css_extension(&canonical) {
+ return CustomCssHistoryStatus::Invalid;
+ }
+ if metadata.len() > MAX_CUSTOM_CSS_BYTES {
+ return CustomCssHistoryStatus::TooLarge;
+ }
+ CustomCssHistoryStatus::Available
+}
+
+pub(crate) fn normalize_custom_css_history(history: &mut Vec) -> bool {
+ let original = history.clone();
+ history.retain(|entry| Path::new(&entry.path).is_absolute());
+ history.sort_by_key(|entry| std::cmp::Reverse(entry.loaded_at));
+
+ let mut seen = HashSet::with_capacity(history.len());
+ history.retain(|entry| seen.insert(path_identity_key(Path::new(&entry.path))));
+ while history.len() > MAX_CUSTOM_CSS_HISTORY_ENTRIES {
+ let eviction_index = history
+ .iter()
+ .enumerate()
+ .min_by_key(|(_, entry)| (entry.last_used_at, entry.loaded_at))
+ .map(|(index, _)| index)
+ .expect("CSS history must contain an eviction candidate");
+ history.remove(eviction_index);
+ }
+ history.sort_by_key(|entry| std::cmp::Reverse(entry.loaded_at));
+
+ *history != original
+}
+
+pub(crate) fn record_custom_css_load(
+ history: &mut Vec,
+ path: String,
+ timestamp: i64,
+) {
+ let identity = path_identity_key(Path::new(&path));
+ history.retain(|entry| path_identity_key(Path::new(&entry.path)) != identity);
+ history.push(CustomCssHistoryEntry {
+ path,
+ loaded_at: timestamp,
+ last_used_at: timestamp,
+ });
+ normalize_custom_css_history(history);
+}
+
+pub(crate) fn touch_custom_css_history(
+ history: &mut [CustomCssHistoryEntry],
+ path: &str,
+ timestamp: i64,
+) -> bool {
+ let identity = path_identity_key(Path::new(path));
+ let Some(entry) = history
+ .iter_mut()
+ .find(|entry| path_identity_key(Path::new(&entry.path)) == identity)
+ else {
+ return false;
+ };
+ entry.last_used_at = timestamp;
+ true
+}
+
+pub(crate) fn migrate_custom_css_history_at_load(
+ history: &mut Vec,
+ active_path: Option<&str>,
+ timestamp: i64,
+) -> bool {
+ let original = history.clone();
+ for entry in history.iter_mut().filter(|entry| entry.loaded_at == 0) {
+ entry.loaded_at = entry.last_used_at;
+ }
+
+ if let Some(path) = active_path.filter(|path| {
+ Path::new(path).is_absolute()
+ && !history
+ .iter()
+ .any(|entry| history_paths_match(&entry.path, path))
+ }) {
+ history.push(CustomCssHistoryEntry {
+ path: path.to_string(),
+ loaded_at: timestamp,
+ last_used_at: timestamp,
+ });
+ }
+
+ normalize_custom_css_history(history);
+ *history != original
+}
+
+pub(crate) fn history_paths_match(left: &str, right: &str) -> bool {
+ path_identity_key(Path::new(left)) == path_identity_key(Path::new(right))
+}
+
+pub(crate) fn custom_css_settings_diff(state: &AppStoreData) -> serde_json::Value {
+ serde_json::json!({
+ "useCustomCSS": state.use_custom_css,
+ "customCSS": state.custom_css,
+ })
+}
+
+fn canonicalize_css_path(path: &Path) -> Result {
+ let canonical = fs::canonicalize(path).map_err(map_io_error)?;
+ if !canonical.is_absolute() {
+ return Err(CssPathError::new(
+ CssHistoryErrorCode::IoError,
+ "CSS path is not absolute after canonicalization",
+ ));
+ }
+ Ok(canonical)
+}
+
+fn validate_css_metadata(path: &Path, metadata: &fs::Metadata) -> Result<(), CssPathError> {
+ if !metadata.is_file() {
+ return Err(CssPathError::new(
+ CssHistoryErrorCode::NotRegularFile,
+ "CSS path is not a regular file",
+ ));
+ }
+ if !has_css_extension(path) {
+ return Err(CssPathError::new(
+ CssHistoryErrorCode::InvalidExtension,
+ "canonical CSS path does not have a .css extension",
+ ));
+ }
+ if metadata.len() > MAX_CUSTOM_CSS_BYTES {
+ return Err(CssPathError::new(
+ CssHistoryErrorCode::TooLarge,
+ format!("CSS file exceeds {MAX_CUSTOM_CSS_BYTES} bytes"),
+ ));
+ }
+ Ok(())
+}
+
+fn has_css_extension(path: &Path) -> bool {
+ path.extension()
+ .and_then(|extension| extension.to_str())
+ .is_some_and(|extension| extension.eq_ignore_ascii_case("css"))
+}
+
+fn map_io_error(error: std::io::Error) -> CssPathError {
+ let code = if error.kind() == std::io::ErrorKind::NotFound {
+ CssHistoryErrorCode::NotFound
+ } else {
+ CssHistoryErrorCode::IoError
+ };
+ CssPathError::new(code, error.to_string())
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ fn test_directory(label: &str) -> PathBuf {
+ std::env::temp_dir().join(format!("dmnote-css-{label}-{}", uuid::Uuid::new_v4()))
+ }
+
+ #[test]
+ fn history_normalization_filters_deduplicates_sorts_and_caps() {
+ let root = test_directory("history-normalize");
+ let duplicate = root.join("duplicate.css").to_string_lossy().to_string();
+ let mut history = vec![
+ CustomCssHistoryEntry {
+ path: "relative.css".to_string(),
+ loaded_at: 100,
+ last_used_at: 100,
+ },
+ CustomCssHistoryEntry {
+ path: duplicate.clone(),
+ loaded_at: 1,
+ last_used_at: 1,
+ },
+ CustomCssHistoryEntry {
+ path: duplicate.clone(),
+ loaded_at: 50,
+ last_used_at: 50,
+ },
+ ];
+ for index in 0..12 {
+ history.push(CustomCssHistoryEntry {
+ path: root
+ .join(format!("entry-{index}.css"))
+ .to_string_lossy()
+ .to_string(),
+ loaded_at: index,
+ last_used_at: index,
+ });
+ }
+
+ assert!(normalize_custom_css_history(&mut history));
+ assert_eq!(history.len(), MAX_CUSTOM_CSS_HISTORY_ENTRIES);
+ assert!(history
+ .windows(2)
+ .all(|pair| pair[0].loaded_at >= pair[1].loaded_at));
+ assert!(history
+ .iter()
+ .all(|entry| Path::new(&entry.path).is_absolute()));
+ assert_eq!(
+ history
+ .iter()
+ .filter(|entry| entry.path == duplicate)
+ .count(),
+ 1
+ );
+ assert_eq!(
+ history
+ .iter()
+ .find(|entry| entry.path == duplicate)
+ .unwrap()
+ .last_used_at,
+ 50
+ );
+ assert!(!normalize_custom_css_history(&mut history));
+ }
+
+ #[test]
+ fn activation_updates_lru_without_changing_display_order() {
+ let root = test_directory("activation-order");
+ let first = root.join("first.css").to_string_lossy().to_string();
+ let second = root.join("second.css").to_string_lossy().to_string();
+ let mut history = vec![
+ CustomCssHistoryEntry {
+ path: first.clone(),
+ loaded_at: 20,
+ last_used_at: 20,
+ },
+ CustomCssHistoryEntry {
+ path: second,
+ loaded_at: 10,
+ last_used_at: 10,
+ },
+ ];
+
+ assert!(touch_custom_css_history(&mut history, &first, 30));
+ normalize_custom_css_history(&mut history);
+
+ assert_eq!(history[0].path, first);
+ assert_eq!(history[0].loaded_at, 20);
+ assert_eq!(history[0].last_used_at, 30);
+ }
+
+ #[test]
+ fn eviction_uses_last_used_at_without_reordering_loaded_at() {
+ let root = test_directory("lru-eviction");
+ let mut history = (0..=MAX_CUSTOM_CSS_HISTORY_ENTRIES)
+ .map(|index| CustomCssHistoryEntry {
+ path: root
+ .join(format!("entry-{index}.css"))
+ .to_string_lossy()
+ .to_string(),
+ loaded_at: index as i64,
+ last_used_at: index as i64,
+ })
+ .collect::>();
+ history[0].last_used_at = 100;
+
+ normalize_custom_css_history(&mut history);
+
+ assert_eq!(history.len(), MAX_CUSTOM_CSS_HISTORY_ENTRIES);
+ assert!(history.iter().any(|entry| entry.loaded_at == 0));
+ assert!(!history.iter().any(|entry| entry.loaded_at == 1));
+ assert!(history
+ .windows(2)
+ .all(|pair| pair[0].loaded_at >= pair[1].loaded_at));
+ }
+
+ #[test]
+ fn validation_accepts_case_insensitive_css_extension() {
+ let root = test_directory("valid");
+ fs::create_dir_all(&root).unwrap();
+ let path = root.join("theme.CSS");
+ fs::write(&path, b"body { color: white; }").unwrap();
+
+ let validated = validate_css_path(&path).unwrap();
+
+ assert_eq!(validated.content, "body { color: white; }");
+ assert!(Path::new(&validated.canonical_path).is_absolute());
+ let _ = fs::remove_dir_all(root);
+ }
+
+ #[test]
+ fn css_history_wire_values_match_the_api_contract() {
+ let codes = [
+ (
+ CssHistoryErrorCode::PathNotAuthorized,
+ "PATH_NOT_AUTHORIZED",
+ ),
+ (CssHistoryErrorCode::NotFound, "NOT_FOUND"),
+ (CssHistoryErrorCode::NotRegularFile, "NOT_REGULAR_FILE"),
+ (CssHistoryErrorCode::InvalidExtension, "INVALID_EXTENSION"),
+ (CssHistoryErrorCode::TooLarge, "TOO_LARGE"),
+ (CssHistoryErrorCode::InvalidUtf8, "INVALID_UTF8"),
+ (CssHistoryErrorCode::IoError, "IO_ERROR"),
+ ];
+ for (code, expected) in codes {
+ assert_eq!(serde_json::to_value(code).unwrap(), expected);
+ }
+ assert_eq!(
+ serde_json::to_value(CustomCssHistoryStatus::TooLarge).unwrap(),
+ "tooLarge"
+ );
+ }
+
+ #[test]
+ fn validation_rejects_oversized_and_non_utf8_files() {
+ let root = test_directory("invalid-content");
+ fs::create_dir_all(&root).unwrap();
+ let oversized = root.join("oversized.css");
+ fs::write(&oversized, vec![b'a'; MAX_CUSTOM_CSS_BYTES as usize + 1]).unwrap();
+ let non_utf8 = root.join("non-utf8.css");
+ fs::write(&non_utf8, [0xFF]).unwrap();
+
+ assert_eq!(
+ validate_css_path(&oversized).unwrap_err().code,
+ CssHistoryErrorCode::TooLarge
+ );
+ assert_eq!(
+ validate_css_path(&non_utf8).unwrap_err().code,
+ CssHistoryErrorCode::InvalidUtf8
+ );
+ let _ = fs::remove_dir_all(root);
+ }
+
+ #[cfg(unix)]
+ #[test]
+ fn validation_checks_the_canonical_target_extension() {
+ use std::os::unix::fs::symlink;
+
+ let root = test_directory("symlink-extension");
+ fs::create_dir_all(&root).unwrap();
+ let target = root.join("secret.txt");
+ let alias = root.join("theme.css");
+ fs::write(&target, b"not css").unwrap();
+ symlink(&target, &alias).unwrap();
+
+ assert_eq!(
+ validate_css_path(&alias).unwrap_err().code,
+ CssHistoryErrorCode::InvalidExtension
+ );
+ let _ = fs::remove_dir_all(root);
+ }
+}
diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs
index 06319edd..b1124983 100644
--- a/src-tauri/src/lib.rs
+++ b/src-tauri/src/lib.rs
@@ -1,6 +1,7 @@
pub mod audio;
pub mod commands;
pub mod cursor;
+pub mod custom_css;
pub mod defaults;
pub mod errors;
pub mod ipc;
diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs
index 07c404ce..98a3c3da 100644
--- a/src-tauri/src/main.rs
+++ b/src-tauri/src/main.rs
@@ -3,6 +3,7 @@
mod audio;
mod commands;
mod cursor;
+mod custom_css;
mod defaults;
mod errors;
mod ipc;
@@ -178,12 +179,17 @@ fn main() {
commands::editor::css::css_reset,
commands::editor::css::css_set_content,
commands::editor::css::css_load,
+ commands::editor::css::css_history_get,
+ commands::editor::css::css_history_activate,
+ commands::editor::css::css_history_remove,
commands::editor::css::css_tab_get_all,
commands::editor::css::css_tab_get,
commands::editor::css::css_tab_load,
commands::editor::css::css_tab_clear,
commands::editor::css::css_tab_set,
commands::editor::css::css_tab_toggle,
+ commands::editor::css::css_tab_activate_history,
+ commands::editor::css::css_tab_export,
commands::editor::js::js_get,
commands::editor::js::js_get_use,
commands::editor::js::js_toggle,
diff --git a/src-tauri/src/models/mod.rs b/src-tauri/src/models/mod.rs
index 968c9c76..a3b103ce 100644
--- a/src-tauri/src/models/mod.rs
+++ b/src-tauri/src/models/mod.rs
@@ -1441,6 +1441,15 @@ pub struct CustomCss {
pub content: String,
}
+#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
+#[serde(rename_all = "camelCase")]
+pub struct CustomCssHistoryEntry {
+ pub path: String,
+ #[serde(default)]
+ pub loaded_at: i64,
+ pub last_used_at: i64,
+}
+
/// 탭별 CSS 설정
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
@@ -1737,6 +1746,8 @@ pub struct AppStoreData {
#[serde(default)]
pub custom_css: CustomCss,
#[serde(default)]
+ pub custom_css_history: Vec,
+ #[serde(default)]
pub font_settings: FontSettings,
#[serde(default)]
pub counter_animation_presets: Vec,
@@ -1818,6 +1829,7 @@ impl Default for AppStoreData {
background_color: "transparent".to_string(),
use_custom_css: false,
custom_css: CustomCss::default(),
+ custom_css_history: Vec::new(),
font_settings: FontSettings::default(),
counter_animation_presets: Vec::new(),
tab_css_overrides: TabCssOverrides::new(),
diff --git a/src-tauri/src/services/css_watcher.rs b/src-tauri/src/services/css_watcher.rs
index c6f5b135..d973e09b 100644
--- a/src-tauri/src/services/css_watcher.rs
+++ b/src-tauri/src/services/css_watcher.rs
@@ -6,7 +6,6 @@
//! - 디바운싱으로 연속 변경 시 한 번만 리로드
use std::collections::HashMap;
-use std::fs;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Duration;
@@ -14,10 +13,15 @@ use std::time::Duration;
use notify::RecommendedWatcher;
use notify_debouncer_mini::{new_debouncer, Debouncer};
use parking_lot::RwLock;
-use tauri::{AppHandle, Emitter, Manager};
+use tauri::{AppHandle, Manager};
-use crate::models::{CustomCss, TabCss};
+use crate::commands::editor::{css::TabCssResponse, state::emit_best_effort};
use crate::state::{AppState, AppStore};
+use crate::{
+ custom_css::{custom_css_settings_diff, validate_css_path, ValidatedCssFile},
+ models::{AppStoreData, CustomCss, TabCss},
+ state::local_asset_path::path_identity_key,
+};
/// CSS 워칭 타입
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
@@ -73,17 +77,17 @@ impl CssWatcher {
/// 특정 경로에 대한 워칭 시작
fn watch_path(&self, path: &str, target: CssWatchTarget) -> Result<(), String> {
- let path_buf = PathBuf::from(path);
-
- // 파일이 존재하는지 확인
- if !path_buf.exists() {
- return Err(format!("File not found: {}", path));
- }
+ let path_buf = std::fs::canonicalize(path)
+ .map_err(|error| format!("Failed to resolve CSS path {path}: {error}"))?;
+ let identity = path_identity_key(&path_buf);
let mut watchers = self.watchers.write();
- // 이미 같은 경로를 워칭 중인 경우
- if let Some(entry) = watchers.get_mut(&path_buf) {
+ if let Some(entry) = watchers
+ .iter_mut()
+ .find(|(watched_path, _)| path_identity_key(watched_path) == identity)
+ .map(|(_, entry)| entry)
+ {
// 같은 타겟이 이미 등록되어 있으면 무시
if !entry.targets.contains(&target) {
entry.targets.push(target);
@@ -211,120 +215,257 @@ fn handle_css_change(store: &AppStore, app: &AppHandle, changed_path: &Path) ->
log::debug!("[CssWatcher] File changed: {}", changed_path_str);
- // 전역 CSS 체크
- if let Some(global_path) = &snapshot.custom_css.path {
- if paths_match(global_path, &changed_path_str) && snapshot.use_custom_css {
- return reload_global_css(store, app, global_path);
- }
- }
-
- // 탭별 CSS 체크
- for (tab_id, tab_css) in &snapshot.tab_css_overrides {
- if let Some(tab_path) = &tab_css.path {
- if paths_match(tab_path, &changed_path_str) && tab_css.enabled {
- return reload_tab_css(store, app, tab_id, tab_path);
- }
- }
+ let global_matches = snapshot.use_custom_css
+ && snapshot
+ .custom_css
+ .path
+ .as_deref()
+ .is_some_and(|path| paths_match(path, &changed_path_str));
+ let tab_matches = snapshot.tab_css_overrides.values().any(|css| {
+ css.enabled
+ && css
+ .path
+ .as_deref()
+ .is_some_and(|path| paths_match(path, &changed_path_str))
+ });
+ if !global_matches && !tab_matches {
+ return Ok(());
}
- Ok(())
+ reload_css_consumers(store, app, &changed_path_str)
}
-/// 전역 CSS 리로드
-fn reload_global_css(store: &AppStore, app: &AppHandle, path: &str) -> Result<(), String> {
- let content = fs::read_to_string(path).map_err(|e| e.to_string())?;
-
- let css = CustomCss {
- path: Some(path.to_string()),
- content: content.clone(),
- };
-
+fn reload_css_consumers(store: &AppStore, app: &AppHandle, path: &str) -> Result<(), String> {
+ let app_state = app.state::();
+ let _operation_guard = app_state.lock_css_operation();
+ let loaded = validate_css_path(Path::new(path)).map_err(|error| {
+ format!(
+ "CSS reload rejected code={} path={} detail={}",
+ error.code.as_str(),
+ path,
+ error.detail
+ )
+ })?;
+ let mut committed_global = None;
+ let mut committed_tabs = Vec::new();
store
- .update(|s| {
- s.custom_css = css.clone();
+ .update(|state| {
+ (committed_global, committed_tabs) = apply_reload_if_current(state, path, &loaded);
})
- .map_err(|e| e.to_string())?;
-
- app.emit("css:content", &css).map_err(|e| e.to_string())?;
+ .map_err(|error| error.to_string())?;
- // OBS 브릿지에 CSS 변경 알림 (settings_diff 방식 — 전체 스냅샷은 키 상태 리셋 유발)
- let app_state = app.state::();
- let snap = store.snapshot();
- let diff = serde_json::json!({
- "useCustomCSS": snap.use_custom_css,
- "customCSS": snap.custom_css,
- });
- app_state.notify_obs_settings_diff(diff);
+ if let Some(css) = committed_global.as_ref() {
+ emit_best_effort(app, "css:content", css);
+ app_state.notify_obs_settings_diff(custom_css_settings_diff(&store.snapshot()));
+ }
+ for (tab_id, css) in &committed_tabs {
+ emit_best_effort(
+ app,
+ "tabCss:changed",
+ &TabCssResponse {
+ tab_id: tab_id.clone(),
+ css: Some(css.clone()),
+ },
+ );
+ }
- log::info!("[CssWatcher] Reloaded global CSS from: {}", path);
+ if committed_global.is_none() && committed_tabs.is_empty() {
+ log::debug!("[CssWatcher] Discarded stale CSS reload for: {path}");
+ } else {
+ log::info!(
+ "[CssWatcher] Reloaded CSS from {} global={} tabs={}",
+ path,
+ committed_global.is_some(),
+ committed_tabs.len()
+ );
+ }
Ok(())
}
-/// 탭별 CSS 리로드
-fn reload_tab_css(
- store: &AppStore,
- app: &AppHandle,
- tab_id: &str,
+fn apply_reload_if_current(
+ state: &mut AppStoreData,
path: &str,
-) -> Result<(), String> {
- let content = fs::read_to_string(path).map_err(|e| e.to_string())?;
-
- let tab_css = TabCss {
- path: Some(path.to_string()),
- content: content.clone(),
- enabled: true,
+ loaded: &ValidatedCssFile,
+) -> (Option, Vec<(String, TabCss)>) {
+ let global_css = if state.use_custom_css
+ && state
+ .custom_css
+ .path
+ .as_deref()
+ .is_some_and(|current| paths_match(current, path))
+ {
+ let css = CustomCss {
+ path: Some(loaded.canonical_path.clone()),
+ content: loaded.content.clone(),
+ };
+ state.custom_css = css.clone();
+ Some(css)
+ } else {
+ None
};
- store
- .update(|s| {
- s.tab_css_overrides
- .insert(tab_id.to_string(), tab_css.clone());
- })
- .map_err(|e| e.to_string())?;
+ let mut tabs = Vec::new();
+ for (tab_id, css) in &mut state.tab_css_overrides {
+ if !css.enabled {
+ continue;
+ }
+ let Some(current_path) = css.path.as_deref() else {
+ continue;
+ };
+ if !paths_match(current_path, path) {
+ continue;
+ }
- #[derive(serde::Serialize, Clone)]
- #[serde(rename_all = "camelCase")]
- struct TabCssResponse {
- tab_id: String,
- css: Option,
+ css.path = Some(loaded.canonical_path.clone());
+ css.content = loaded.content.clone();
+ tabs.push((tab_id.clone(), css.clone()));
}
- let response = TabCssResponse {
- tab_id: tab_id.to_string(),
- css: Some(tab_css),
- };
-
- app.emit("tabCss:changed", &response)
- .map_err(|e| e.to_string())?;
-
- log::info!("[CssWatcher] Reloaded tab CSS {} from: {}", tab_id, path);
- Ok(())
+ (global_css, tabs)
}
-/// 경로 비교 (플랫폼별 차이 무시)
fn paths_match(path1: &str, path2: &str) -> bool {
let p1 = PathBuf::from(path1);
let p2 = PathBuf::from(path2);
+ if path_identity_key(&p1) == path_identity_key(&p2) {
+ return true;
+ }
- // 먼저 파일명이 같은지 빠르게 확인
- match (p1.file_name(), p2.file_name()) {
- (Some(name1), Some(name2)) if name1 == name2 => {
- // 파일명이 같으면 전체 경로 비교
- // 플랫폼 차이를 무시하기 위해 정규화된 문자열로 비교
- let normalized1 = path1.replace('\\', "/").to_lowercase();
- let normalized2 = path2.replace('\\', "/").to_lowercase();
+ matches!(
+ (p1.canonicalize(), p2.canonicalize()),
+ (Ok(canonical1), Ok(canonical2)) if canonical1 == canonical2
+ )
+}
- if normalized1 == normalized2 {
- return true;
- }
+#[cfg(test)]
+mod tests {
+ use super::apply_reload_if_current;
+ use crate::{
+ custom_css::ValidatedCssFile,
+ models::{AppStoreData, CustomCss, TabCss},
+ };
- // 문자열 불일치 시 canonicalize로 최종 확인 (고비용 — 마지막 단계에서만)
- if let (Ok(canonical1), Ok(canonical2)) = (p1.canonicalize(), p2.canonicalize()) {
- return canonical1 == canonical2;
- }
+ #[test]
+ fn stale_global_reload_cannot_replace_a_new_path() {
+ let mut state = AppStoreData {
+ use_custom_css: true,
+ custom_css: CustomCss {
+ path: Some("/tmp/new.css".to_string()),
+ content: "new".to_string(),
+ },
+ ..AppStoreData::default()
+ };
+ let stale = ValidatedCssFile {
+ canonical_path: "/tmp/old.css".to_string(),
+ content: "old".to_string(),
+ };
- false
- }
- _ => false,
+ let (global, tabs) = apply_reload_if_current(&mut state, "/tmp/old.css", &stale);
+ assert!(global.is_none());
+ assert!(tabs.is_empty());
+ assert_eq!(state.custom_css.content, "new");
+ }
+
+ #[test]
+ fn current_global_reload_replaces_content() {
+ let path = "/tmp/current.css";
+ let mut state = AppStoreData {
+ use_custom_css: true,
+ custom_css: CustomCss {
+ path: Some(path.to_string()),
+ content: "before".to_string(),
+ },
+ ..AppStoreData::default()
+ };
+ let reloaded = ValidatedCssFile {
+ canonical_path: "/tmp/canonical.css".to_string(),
+ content: "after".to_string(),
+ };
+
+ let (global, _) = apply_reload_if_current(&mut state, path, &reloaded);
+ assert!(global.is_some());
+ assert_eq!(state.custom_css.content, "after");
+ assert_eq!(state.custom_css.path.as_deref(), Some("/tmp/canonical.css"));
+ }
+
+ #[cfg(unix)]
+ #[test]
+ fn symlink_alias_and_canonical_target_share_reload_identity() {
+ use std::os::unix::fs::symlink;
+
+ let root = std::env::temp_dir().join(format!(
+ "dmnote-css-watcher-canonical-{}",
+ uuid::Uuid::new_v4()
+ ));
+ std::fs::create_dir_all(&root).unwrap();
+ let target = root.join("target.css");
+ let alias = root.join("alias.css");
+ std::fs::write(&target, "body {}").unwrap();
+ symlink(&target, &alias).unwrap();
+
+ assert!(super::paths_match(
+ &alias.to_string_lossy(),
+ &target.to_string_lossy()
+ ));
+ let _ = std::fs::remove_dir_all(root);
+ }
+
+ #[test]
+ fn stale_tab_reload_preserves_new_path_and_enabled_state() {
+ let mut state = AppStoreData::default();
+ state.tab_css_overrides.insert(
+ "4key".to_string(),
+ TabCss {
+ path: Some("/tmp/new.css".to_string()),
+ content: "new".to_string(),
+ enabled: false,
+ },
+ );
+ let loaded = ValidatedCssFile {
+ canonical_path: "/tmp/old.css".to_string(),
+ content: "old".to_string(),
+ };
+
+ let (_, tabs) = apply_reload_if_current(&mut state, "/tmp/old.css", &loaded);
+
+ assert!(tabs.is_empty());
+ let css = &state.tab_css_overrides["4key"];
+ assert_eq!(css.content, "new");
+ assert!(!css.enabled);
+ }
+
+ #[test]
+ fn shared_file_reload_fans_out_to_global_and_all_tabs() {
+ let path = "/tmp/shared.css";
+ let tab_css = TabCss {
+ path: Some(path.to_string()),
+ content: "before".to_string(),
+ enabled: true,
+ };
+ let mut state = AppStoreData {
+ use_custom_css: true,
+ custom_css: CustomCss {
+ path: Some(path.to_string()),
+ content: "before".to_string(),
+ },
+ ..AppStoreData::default()
+ };
+ state
+ .tab_css_overrides
+ .insert("4key".to_string(), tab_css.clone());
+ state.tab_css_overrides.insert("5key".to_string(), tab_css);
+ let loaded = ValidatedCssFile {
+ canonical_path: path.to_string(),
+ content: "after".to_string(),
+ };
+
+ let (global, tabs) = apply_reload_if_current(&mut state, path, &loaded);
+
+ assert_eq!(global.unwrap().content, "after");
+ assert_eq!(tabs.len(), 2);
+ assert!(state
+ .tab_css_overrides
+ .values()
+ .all(|css| css.content == "after" && css.enabled));
}
}
diff --git a/src-tauri/src/state/app_state.rs b/src-tauri/src/state/app_state.rs
index b8434a61..c39b72de 100644
--- a/src-tauri/src/state/app_state.rs
+++ b/src-tauri/src/state/app_state.rs
@@ -2,6 +2,7 @@ use std::time::Instant;
use std::{
collections::HashSet,
io::{BufRead, BufReader},
+ path::Path,
process::{Child, ChildStdin, Command, Stdio},
sync::{
atomic::{AtomicBool, AtomicU64, Ordering},
@@ -34,11 +35,13 @@ use crate::{
},
keyboard::KeyboardManager,
models::{
- overlay_resize_anchor_from_str, BootstrapOverlayState, BootstrapPayload, DefaultsPayload,
- KeyCounterSettings, KeyCounters, KeyMappings, KeySoundOutputBackendPersist, OverlayBounds,
- OverlayResizeAnchor, SettingsDiff, SettingsState,
+ overlay_resize_anchor_from_str, AppStoreData, BootstrapOverlayState, BootstrapPayload,
+ DefaultsPayload, KeyCounterSettings, KeyCounters, KeyMappings,
+ KeySoundOutputBackendPersist, OverlayBounds, OverlayResizeAnchor, SettingsDiff,
+ SettingsState,
},
services::{css_watcher::CssWatcher, obs_bridge::ObsBridgeService, settings::SettingsService},
+ state::local_asset_path::path_identity_key,
};
const OVERLAY_LABEL: &str = "overlay";
@@ -131,6 +134,10 @@ pub struct AppState {
/// Raw input stream subscriber count - emit only when > 0
raw_input_subscribers: Arc,
key_sound: Arc,
+ /// 전역 CSS 상태와 워처 전환 직렬화
+ css_operation_lock: Mutex<()>,
+ /// 현재 세션에서 사용자가 승인한 CSS 경로
+ authorized_css_paths: RwLock>,
/// CSS 파일 핫리로딩 워처
css_watcher: RwLock
+);
+
+export default SearchField;
diff --git a/src/renderer/components/main/common/SettingRow.tsx b/src/renderer/components/main/common/SettingRow.tsx
index 9b9920c6..72395bb4 100644
--- a/src/renderer/components/main/common/SettingRow.tsx
+++ b/src/renderer/components/main/common/SettingRow.tsx
@@ -1,11 +1,13 @@
import React from 'react';
+import Checkbox from '@components/main/common/Checkbox';
import {
- SECTION_CARD_CLASS_COMFORTABLE,
- FORM_ROW_CLASS_COMFORTABLE,
+ SETTINGS_CARD_CLASS,
+ SETTINGS_ROW_CLASS,
+ SETTINGS_LABEL_CLASS,
} from '@utils/cardRecipes';
-// 설정 페이지 전용 comfortable 밀도 카드·행
-// 높이/패딩 오버라이드 API를 두지 않음 — 제3 밀도 방지 가드레일
+// 설정 페이지 카드·행 - 설정 페인과 같은 설정 표면 밀도 공유
+// 높이/패딩 오버라이드 API를 두지 않음 - 임의 밀도 방지 가드레일
interface SettingCardProps {
children: React.ReactNode;
@@ -19,7 +21,7 @@ export const SettingCard = ({
onMouseLeave,
}: SettingCardProps) => (