zudo-text

検索したい単語を入力

いつでも検索バーを開ける

Settings & Workspace Binding API

Two related but distinct concerns live here: the settings commands, and the workspace binding commands that decide which workspace this app instance opens.

Read Settings & Configuration first if you have not — it explains the three-layer split these commands sit on. The short version: user preferences are a workspace document (.zudotext.settings.json), and the binding that says which workspace to open is a local file (config.json v2). The Rust commands below own the local half.

Note

The workspace_* Tauri command family — workspace_get_dir, workspace_set_dir, workspace_list_all, workspace_register, workspace_add_existing, workspace_remove, workspace_scaffold, workspace_update_metadata, workspace_list_files, workspace_switch — no longer exists. It was deleted with the workspace concept in #4223; see Retired: the workspace command family below. The two read-only survivors on the bridge were themselves renamed from bridge.workspace to bridge.localDir (#4992) so workspace was free for the account-workspace domain word this epic's later waves (#4991) introduce.

Settings Commands

settings_get

Read settings from the local settings file.

const settings = await invoke<object | null>('settings_get');

Returns: object | null — the parsed JSON settings object, or null if the project root is empty, or the file doesn't exist or is invalid JSON.

Behavior: Uses an in-memory cache backed by mtime invalidation. On each call the file's modification time is compared with the cached mtime; unchanged means the cached value is returned without re-reading.

Which store is authoritative. This command is not the primary read path. The synced settings document lives in the workspace and is read through bridge.workspaceFiles (see renderer/lib/workspace-settings-document.ts). settings_get is the pre-unlock fallback — it serves the appearance load that runs before first paint, and the pnpm dev:rest path where no cloud workspace is armed. In a production desktop build there is no local project root at all, so AppState.project_root is empty and this returns null.

settings_save

Write settings to the local settings file.

const success = await invoke<boolean>('settings_save', {
  settings: { /* full settings object */ },
});
ParameterTypeDescription
settingsobjectThe complete settings JSON object

Returns: booleantrue on success; false if the project root is empty, the value is not a JSON object, or the write fails.

Validation: The value must be a JSON object (not an array, string, etc.). No deeper schema validation happens in Rust — the frontend and @takazudo/app-defaults own schema enforcement via validateSettings().

Writes never fall back here. The renderer writes the settings document to the workspace; a write that landed in a machine-local file would resurrect the second store the cloud pivot removed and would be invisible to every other device.

settings_save_blocking

Identical body to settings_save, exposed under its own name for the renderer's quit path.

await invoke<boolean>('settings_save_blocking', { settings });

The normal write path debounces and fire-and-forgets the IPC. On beforeunload the renderer tears down before an unawaited write lands (#2782), so the quit path calls this non-debounced entry point instead. The distinct name documents the across-quit contract.

The write chokepoint

Every Rust write of .zudotext.settings.json routes through one sanitizer in tauri-app/core/src/settings.rs, which drops or blanks three groups of keys before persisting:

GroupKeysWhy
Transient top-level stateactiveDraft, draftCount, similarDocsPanelSize, similarDocsInnerRatioPer-device; must not travel
Retired keysgeneral.projectRoot; sync.cloudServerUrl, cloudWorkspaceId, cloudEncryptionConfiguredObsolete local identity fields in a document that now syncs
Local bootstrap identitysync.cloudDeviceIdDevice identity, not a preference

Behavioural sync preferences — cloudRealtimeEnabled, cloudDeviceName — are deliberately not in the identity list and do persist. The workspace id comes from the runtime binding, encryption readiness from bridge state, and the sync-server URL from runtime bootstrap configuration. The remaining list mirrors LOCAL_BOOTSTRAP_IDENTITY_SYNC_FIELDS in packages/app-defaults/src/settings-identity.ts; keep the two in step.

Workspace Binding Commands

These read and write ~/.config/zudotext/<appname>/config.json (schema v2). All logic lives in zudotext_core::generator::app_config; the command layer only resolves the running instance's config dir and maps errors for the IPC boundary.

AppBindingDto

The wire shape is a discriminated record rather than a bare Option<string>, so the renderer can report why an instance is unbound instead of guessing. The DTO's field is named workspaceId on the wire (the Rust-side residual rename landed in epic #4991 Wave 4).

interface AppBindingDto {
  status: 'bound' | 'unbound';
  workspaceId?: string;   // present only when status === 'bound'
  reason?: 'missing' | 'unreadable' | 'malformed' | 'missing-workspace-id' | 'new-workspace-intent';
}
ReasonMeaning
missingNo config.json — a fresh install
unreadableThe file exists but could not be read
malformedPresent but not parseable as the v2 schema
missing-workspace-idNo usable workspace.id — either v2-shaped with a blank/absent id, or a retired v1 {"workspace": …} config (pre-release policy ships no migration code, so it just falls through here)
new-workspace-intentThe instance is unbound because the user is mid-flow toward binding a brand-new workspace

All four are normal outcomes, not errors. Every one routes to onboarding, and none of them creates local storage as a fallback.

app_binding_read

const binding = await invoke<AppBindingDto>('app_binding_read');

Returns: AppBindingDto. Never throws — an unreadable or corrupt file resolves to unbound with a reason.

app_binding_persist

Bind this instance to a workspace, replacing any existing binding.

await invoke<void>('app_binding_persist', { workspaceId: 'workspace-abc123' });
ParameterTypeDescription
workspace_idstringWorkspace to bind to; rejected when blank

Returns: Result<(), string>.

Behavior: Always forces the write — the caller has just proven the workspace opens, so a stale binding (or a leftover v1 config) must not win over it.

bind-only-after-unlock: call this only after a successful unlock and snapshot. Nothing in Rust enforces the ordering; the renderer's onboarding flow owns it. Persisting earlier can strand the instance on a workspace it cannot open, with no way out from inside the app.

app_binding_clear

const existed = await invoke<boolean>('app_binding_clear');

Returns: Result<boolean, string> — whether a config file was actually removed. The next launch resolves as unbound and routes to onboarding.

Bridge facade

const binding = await bridge.appBinding.read();     // AppBindingState
await bridge.appBinding.persist(workspaceId);
const existed = await bridge.appBinding.clear();

The bridge narrows the flat Rust DTO into a discriminated union so no caller has to:

type AppBindingState =
  | { status: 'bound'; workspaceId: string }
  | { status: 'unbound'; reason: AppBindingUnboundReason };

Retired: the workspace command family

Every workspace_* Tauri command, the workspaces.json registry, and the Rust workspace_registry module were deleted in #4223.

They were not reimplemented against the workspace. An app instance points at exactly one workspace, so there is nothing to enumerate, register, import, or switch between — the operations lost their referent rather than changing their backing store. The workspace chooser UI went with them (#4214); Cloud-first onboarding is the flow that replaced it.

Existing workspaces.json files on disk are left untouched — per the pre-release policy there is no migration; they are simply never read.

Two read-only methods survive on the bridge's localDir namespace (renamed from workspace in #4992 — no Tauri command behind them on the cloud path), both explicitly transitional:

MethodPost-pivot behavior
getDir()Returns WORKSPACE_SYNTHETIC_ROOT — a synthetic absolute-looking value, never a real directory
listFiles(path, filter?)Enumerates the workspace model beneath that synthetic root; the filter contract is unchanged

Both exist only to keep absolute-path-shaped callers working during migration. New code should use bridge.workspaceFiles, which speaks workspace-relative POSIX paths and never sees the synthetic root. Never use the synthetic root as an identity — it is the same value in every workspace; use the workspace id.

Utility Commands

fonts_list

List available system font families.

const fonts = await invoke<FontFamily[]>('fonts_list');
interface FontFamily {
  family: string;       // Font family name (e.g., "JetBrains Mono")
  isMonospace: boolean; // Whether the font is detected as monospace
}

Returns: FontFamily[] — sorted alphabetically, deduplicated. Empty array on non-macOS platforms.

Behavior: Uses the font-kit crate to enumerate system fonts, loading each to check is_monospace(). Deduplicated by family name, preferring isMonospace: true when a family appears more than once.

set_window_opacity

await invoke('set_window_opacity', { opacity: 0.8 });
ParameterTypeDescription
opacitynumberOpacity value (clamped to 0.3–1.0)

Returns: Result<(), string> — errors if the native window handle can't be obtained.

Behavior: On macOS, accesses the underlying NSWindow and calls setAlphaValue(). Values below 0.3 are clamped to prevent invisible windows. No-op on non-macOS.

print_webview

await invoke('print_webview');

Returns: Result<(), string>. On macOS, accesses the underlying WKWebView and triggers the native print dialog. No-op elsewhere.

reveal_directory

Open a directory in the OS file manager (Finder on macOS).

await invoke('reveal_directory', { path: '/Users/me/Projects/notes' });
ParameterTypeDescription
pathstringAbsolute path to reveal

This addresses your real filesystem, not the workspace — workspace entries have no OS path to reveal.

get_project_root

const root = await invoke<string>('get_project_root');

Returns: stringAppState.project_root. This is the local root for the surviving local-file surfaces (assets, skills roots), not where user content lives. It is the repo root in dev, the sandbox documents directory on iOS, and an empty string in a production desktop build.

get_home_dir

const home = await invoke<string>('get_home_dir');

Returns: string — the home directory (e.g. /Users/username), or an empty string if it can't be determined.