Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ src-tauri/src/
- **sweep 불변식**: 자산 정리는 즉시 삭제가 아니라 `trash/<세션>/` 30일 격리 — 이를 우회하는 직접 `remove_file` 정리 경로 추가 금지. store 복구가 발생한 세션은 sweep이 자동 스킵됨(`skip_asset_sweep`)
- **store에 사용자 생성 컬렉션 필드를 추가할 때**: `migration.rs`의 `recover_collection_field`에 항목 단위 복구 등록 검토 (범용 헬퍼 재사용, 한 줄). 미등록 시 그 필드만 "손상 시 통째 초기화"로 폴백
- **`keys[mode][i]` ↔ `keyPositions[mode][i]`는 인덱스 결합** — 복구·마이그레이션에서 배열 요소 제거 금지, 제자리 대체(`""` / default)만 허용
- **편집 결합 컬렉션을 추가할 때**: 전용 세분 저장 커맨드를 새로 만들지 말고 `EditorDocumentV1` 필드와 `editor_commit` patch·검증·이벤트에 함께 추가
- **editor_commit 오류 코드를 추가할 때**: 백엔드 오류 정의와 프론트 `EDITOR_ERROR_CODES`(`src/types/editor.ts`)에 반드시 함께 추가 — 프론트 목록에 없는 코드는 `retryable` 값과 무관하게 "이름표 없는 오류"로 취급되어 미저장 편집이 즉시 폐기됨

## API 문서 동기화

Expand All @@ -157,4 +159,3 @@ src-tauri/src/
2. **린트**: `cd src-tauri && cargo clippy`
3. **포맷팅**: `cd src-tauri && cargo fmt`
4. **permissions 확인**: 커맨드 추가/삭제 시 빌드 후 `permissions/dmnote-allow-all.json` 자동 갱신 확인

2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ src-tauri/src/
- **sweep 불변식**: 자산 정리는 즉시 삭제가 아니라 `trash/<세션>/` 30일 격리 — 이를 우회하는 직접 `remove_file` 정리 경로 추가 금지. store 복구가 발생한 세션은 sweep이 자동 스킵됨(`skip_asset_sweep`)
- **store에 사용자 생성 컬렉션 필드를 추가할 때**: `migration.rs`의 `recover_collection_field`에 항목 단위 복구 등록 검토 (범용 헬퍼 재사용, 한 줄). 미등록 시 그 필드만 "손상 시 통째 초기화"로 폴백
- **`keys[mode][i]` ↔ `keyPositions[mode][i]`는 인덱스 결합** — 복구·마이그레이션에서 배열 요소 제거 금지, 제자리 대체(`""` / default)만 허용
- **편집 결합 컬렉션을 추가할 때**: 전용 세분 저장 커맨드를 새로 만들지 말고 `EditorDocumentV1` 필드와 `editor_commit` patch·검증·이벤트에 함께 추가
- **editor_commit 오류 코드를 추가할 때**: 백엔드 오류 정의와 프론트 `EDITOR_ERROR_CODES`(`src/types/editor.ts`)에 반드시 함께 추가 — 프론트 목록에 없는 코드는 `retryable` 값과 무관하게 "이름표 없는 오류"로 취급되어 미저장 편집이 즉시 폐기됨

## API 문서 동기화

Expand Down
1 change: 1 addition & 0 deletions docs/content/en/api-reference/_meta.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export default {
index: "Overview",
app: "App",
keys: "Keys",
editor: "Editor",
knobs: "Knobs",
settings: "Settings",
overlay: "Overlay",
Expand Down
15 changes: 10 additions & 5 deletions docs/content/en/api-reference/app/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ description: dmn.app, dmn.window API reference

App-level control including boot, restart, and external URLs.

<Callout type="info">
Automatic update is main-window only. Restart and quit wait for pending editor
writes in every native Tauri window before the process exits.
</Callout>

### `dmn.app.bootstrap(): Promise<Bootstrap>`

Returns app bootstrap data (settings, keys, presets, etc.)
Expand All @@ -31,12 +36,12 @@ Restarts the app. Settings are preserved.
await dmn.app.restart();
```

### `dmn.app.openExternalUrl(url: string): Promise<void>`
### `dmn.app.openExternal(url: string): Promise<void>`

Opens a URL in the system default browser.

```javascript
await dmn.app.openExternalUrl("https://dmstudio.app");
await dmn.app.openExternal('https://dmstudio.app');
```

---
Expand All @@ -50,7 +55,7 @@ Window management API.
Returns the current window type.

```typescript
type WindowType = "main" | "overlay";
type WindowType = 'main' | 'overlay';

console.log(dmn.window.type); // 'main' or 'overlay'
```
Expand Down Expand Up @@ -86,13 +91,13 @@ await dmn.window.openDevtoolsAll();
```javascript
// App info display
dmn.plugin.defineElement({
name: "App Info",
name: 'App Info',
maxInstances: 1,

template: (state, settings, { html }) => html`
<div style="background: #333; padding: 16px; border-radius: 8px;">
<div>Window: ${dmn.window.type}</div>
<div>Mode: ${state.mode ?? "loading..."}</div>
<div>Mode: ${state.mode ?? 'loading...'}</div>
</div>
`,

Expand Down
240 changes: 240 additions & 0 deletions docs/content/en/api-reference/editor/page.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
---
title: Editor API
description: Atomic editor document reads, commits, and change events
---

# Editor API

<Callout type="warning">
OBS is read-only. Native Tauri main and overlay windows can write through the
revision coordinator; OBS can only read and subscribe to the canonical
document.
</Callout>

`dmn.editor` reads and updates the six collections that make up the editor
layout as one revisioned document. Use it when one user action changes more
than one collection, or when a consumer needs a single ordered change stream.

## Document Types

```typescript
type EditorField =
| 'keys'
| 'keyPositions'
| 'statPositions'
| 'graphPositions'
| 'knobPositions'
| 'layerGroups';

interface EditorDocumentV1 {
schemaVersion: 1;
keys: KeyMappings;
keyPositions: KeyPositions;
statPositions: StatItemPositions;
graphPositions: GraphItemPositions;
knobPositions: KnobItemPositions;
layerGroups: LayerGroups;
}

type EditorPatchV1 = {
schemaVersion: 1;
} & Partial<Pick<EditorDocumentV1, EditorField>>;
```

Each field included in an `EditorPatchV1` is the complete canonical value of
that top-level collection. It is not an item-level diff.

## Read the Current Document

### `dmn.editor.get(): Promise<EditorGetResult>`

Returns the current revision and its matching full document in one snapshot.

```typescript
interface EditorGetResult {
revision: number;
document: EditorDocumentV1;
}

const { revision, document } = await dmn.editor.get();
console.log(revision, document.keyPositions);
```

Keep the returned `revision`. A later commit uses it as `baseRevision` so that
an older snapshot cannot silently overwrite a newer edit.

## Commit Changes Atomically

### `dmn.editor.commit(request): Promise<EditorCommitResult>`

Merges `changes` into the document at `baseRevision`, validates the completed
document, and persists all included collections in one atomic replacement. A
rejected commit does not partially apply its changes. DM Note requests the
strongest file and directory durability barriers available on each supported
platform, while no application can guarantee recovery from hardware or
firmware that violates those barriers.

<Callout type="info">
The app's Undo/Redo path also restores the six editor collections, custom-tab
metadata, selected mode, counters, preset settings, and per-tab note settings
in one backend store transaction. This internal command is intentionally not
part of the public plugin API.
</Callout>

```typescript
interface EditorCommitRequest {
baseRevision: number;
mutationId: string;
changes: EditorPatchV1;
}

interface EditorCommitResult {
revision: number;
changedFields: EditorField[];
}

const snapshot = await dmn.editor.get();
const result = await dmn.editor.commit({
baseRevision: snapshot.revision,
mutationId: crypto.randomUUID(),
changes: {
schemaVersion: 1,
statPositions: nextStatPositions,
layerGroups: nextLayerGroups,
},
});

console.log('Committed revision:', result.revision);
```

`mutationId` must be a UUID string no longer than 64 bytes. A recent request
retried with the same ID in the current app process is deduplicated. This is
intended for immediate IPC retries; the in-memory deduplication window does not
survive an app restart. Reusing a retained ID for a different request is
rejected.

If the submitted values are already current, the result keeps the current
revision, returns an empty `changedFields`, and emits no canonical committed
event. Compatibility wrappers may still project their legacy per-field refresh
event once; retrying the same `mutationId` does not project it again.

### Paired key structure changes

`keys[mode][i]` and `keyPositions[mode][i]` describe the same item by index.
When an edit adds or removes a mode, or changes an array length, submit both
collections in the same commit. Their mode sets and lengths must match.

```typescript
await dmn.editor.commit({
baseRevision,
mutationId: crypto.randomUUID(),
changes: {
schemaVersion: 1,
keys: nextKeys,
keyPositions: nextKeyPositions,
},
});
```

A same-shape edit, such as changing a key label or a position property, may
update only its own collection. A shape-changing `keys`-only or
`keyPositions`-only request is rejected with `PAIRED_UPDATE_REQUIRED`.

When using the compatibility `dmn.keys` API, call
`dmn.keys.updateWithPositions(keys, keyPositions)` for these structural
changes. Do not split them across `update()` and `updatePositions()`.

## Commit Errors

`commit()` rejects with this structured error. Branch on `errorCode`, not on
the human-readable `message`.

```typescript
type EditorCommitErrorCode =
| 'REVISION_CONFLICT'
| 'VALIDATION_FAILED'
| 'PAIRED_UPDATE_REQUIRED'
| 'MUTATION_ID_REUSED'
| 'IO_ERROR';

interface EditorCommitError {
errorCode: EditorCommitErrorCode;
message: string;
details?: {
currentRevision?: number;
validationCode?: string;
field?: string;
};
retryable: boolean;
}
```

| Code | Meaning | `retryable` |
| ------------------------ | ---------------------------------------------------------- | ----------- |
| `REVISION_CONFLICT` | `baseRevision` is stale; read, reconcile, and commit again | `true` |
| `VALIDATION_FAILED` | The completed document violates an editor validation rule | `false` |
| `PAIRED_UPDATE_REQUIRED` | A structural key change omitted its paired collection | `false` |
| `MUTATION_ID_REUSED` | The same mutation ID was used for a different request | `false` |
| `IO_ERROR` | The document could not be persisted | `true` |

`details.currentRevision`, `details.validationCode`, or `details.field` is
included when it applies to the error.

## Subscribe to Committed Changes

### `dmn.editor.onCommitted(callback): ReadyUnsubscribe`

Subscribes to the canonical `editor:committed` stream. One event represents
one successful atomic editor commit.

```typescript
interface EditorCommittedV1 {
schemaVersion: 1;
revision: number;
mutationId: string;
origin?: string;
changedFields: EditorField[];
patch: EditorPatchV1;
}

const unsubscribe = dmn.editor.onCommitted((event) => {
console.log(event.revision, event.changedFields);
applyEditorPatch(event.patch);
});

await unsubscribe.ready;

dmn.plugin.registerCleanup(() => {
unsubscribe();
});
```

The returned unsubscribe function has a `ready: Promise<void>` property. Await
it when no event may be missed before taking a snapshot.

Events can arrive before the matching `commit()` promise resolves. Use
`mutationId` and `revision` to avoid applying the same change twice. If an
observed revision skips one or more values, call `dmn.editor.get()` and replace
the local snapshot. Ignore unknown `origin` values and future unknown fields.

## Legacy Change Events

<Callout type="warning">
The per-collection events below remain available with their existing payloads
for plugin compatibility, but they are deprecated for new editor state
synchronization. Use `dmn.editor.onCommitted()` so one multi-collection edit
is observed as one ordered change.
</Callout>

| Compatibility event | Replacement |
| ------------------------------------- | -------------------------- |
| `dmn.keys.onChanged()` | `dmn.editor.onCommitted()` |
| `dmn.keys.onPositionsChanged()` | `dmn.editor.onCommitted()` |
| `dmn.statItems.onPositionsChanged()` | `dmn.editor.onCommitted()` |
| `dmn.graphItems.onPositionsChanged()` | `dmn.editor.onCommitted()` |
| `dmn.knobItems.onPositionsChanged()` | `dmn.editor.onCommitted()` |
| `dmn.layerGroups.onChanged()` | `dmn.editor.onCommitted()` |

Existing plugins do not need to migrate immediately. New synchronization code
should subscribe to only the canonical event stream instead of applying both
the canonical and compatibility events.
1 change: 1 addition & 0 deletions docs/content/en/api-reference/index/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ All plugins access DM Note features through the `dmn` global object.
| [`dmn.app`](/docs/api-reference/app) | App boot, restart, external URL |
| [`dmn.window`](/docs/api-reference/app#window) | Window type, minimize, close |
| [`dmn.keys`](/docs/api-reference/keys) | Key mapping, events, custom tabs |
| [`dmn.editor`](/docs/api-reference/editor) | Atomic editor state commits |
| [`dmn.knobItems`](/docs/api-reference/knobs) | HID knob elements |
| [`dmn.settings`](/docs/api-reference/settings) | App settings get/set |
| [`dmn.overlay`](/docs/api-reference/overlay) | Overlay control |
Expand Down
Loading
Loading