Skip to main content

obsidian-settings

Write declarative settings tabs for Obsidian plugins using the 1.13.0+ getSettingDefinitions() API. Use when creating or editing a PluginSettingTab, adding settings controls (toggle, text, number, dropdown, slider, file, folder, color), building settings groups/lists/sub-pages, wiring conditional visibility or validation, migrating from the imperative display() approach, or touching any file under apps/obsidian/src/services/*/setting-tab/. Also use when opening the settings modal from code, deep-linking to a settings sub-page, or revealing/highlighting one setting row. Also use when the user mentions "setting tab", "plugin settings", "getSettingDefinitions", "SettingControl", "SettingDefinitionItem", "app.setting", or "openTabById", even if they don't explicitly ask for this skill.

소스 정보

저장소
aidenlx/zotlit
최근 소스 활동
2026년 8월 13일 13:43
감지된 SKILL.md 언어
영어
스타
1,029
포크
64

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

파일 탐색기
3 개 파일

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
name
obsidian-settings
description
Write declarative settings tabs for Obsidian plugins using the 1.13.0+ getSettingDefinitions() API. Use when creating or editing a PluginSettingTab, adding settings controls (toggle, text, number, dropdown, slider, file, folder, color), building settings groups/lists/sub-pages, wiring conditional visibility or validation, migrating from the imperative display() approach, or touching any file under apps/obsidian/src/services/*/setting-tab/. Also use when opening the settings modal from code, deep-linking to a settings sub-page, or revealing/highlighting one setting row. Also use when the user mentions "setting tab", "plugin settings", "getSettingDefinitions", "SettingControl", "SettingDefinitionItem", "app.setting", or "openTabById", even if they don't explicitly ask for this skill.
# Obsidian Declarative Settings (1.13.0+) Settings tabs describe their UI as data — an array of definition objects returned from `getSettingDefinitions()`. Obsidian handles rendering, search indexing, persistence, and validation. ## Type definitions The canonical source is `packages/obsidian-api/obsidian.d.ts`. Read the relevant types there when you need precise signatures — the guide below covers usage patterns, not exhaustive type docs. Quick lookup commands: - `rg 'SettingControl<' packages/obsidian-api/obsidian.d.ts` — all control types - `rg 'SettingDefinition' packages/obsidian-api/obsidian.d.ts` — all definition types - `rg 'class PluginSettingTab' packages/obsidian-api/obsidian.d.ts` — tab class - `rg 'class SettingPage' packages/obsidian-api/obsidian.d.ts` — imperative sub-page class ## How it works Override `getSettingDefinitions()` on your `PluginSettingTab` subclass. Each entry in the returned array is a `SettingDefinitionItem` — one of: | Shape | What it does | |---|---| | `{ name, control: { type, key } }` | Binds one settings key to a UI control. Auto-reads/writes/saves. | | `{ name, render: (setting, group) => … }` | Full imperative control over one `Setting` row. No auto-save. | | `{ name, action: (el, index) => … }` | Clickable action row. | | `{ name }` (no control/render/action) | Static heading or info row. | | `{ type: 'group', heading, items }` | Visual grouping with a heading. | | `{ type: 'list', heading, items, onDelete, … }` | User-managed collection (add/delete/reorder). | | `{ type: 'page', name, items }` or `{ type: 'page', name, page: () => … }` | Navigable sub-page. | `control`, `render`, and `action` are mutually exclusive on a single definition. ## Control types Every control reads from and writes to `this.plugin.settings[key]` automatically. Obsidian calls `saveData()` after each change. | Type | Stored value | Required fields | Optional fields | |---|---|---|---| | `toggle` | `boolean` | `key` | `defaultValue`, `disabled` | | `text` | `string` | `key` | `placeholder`, `defaultValue`, `validate`, `disabled` | | `textarea` | `string` | `key` | `placeholder`, `rows`, `defaultValue`, `validate`, `disabled` | | `number` | `number` | `key` | `min`, `max`, `step`, `placeholder`, `defaultValue`, `validate`, `disabled` | | `slider` | `number` | `key`, `min`, `max`, `step` | `defaultValue`, `displayFormat`, `disabled` | | `dropdown` | `string` | `key`, `options` | `defaultValue`, `disabled` | | `file` | `string` (path) | `key` | `filter`, `placeholder`, `defaultValue`, `validate`, `disabled` | | `folder` | `string` (path) | `key` | `filter`, `includeRoot`, `placeholder`, `defaultValue`, `validate`, `disabled` | | `color` | `string` (hex) | `key` | `defaultValue`, `disabled` | `slider` requires all three of `min`, `max`, `step` (not optional like on `number`). ### Examples ```ts // Toggle { name: 'Enable sync', control: { type: 'toggle', key: 'syncEnabled' } } // Text with validation { name: 'API key', control: { type: 'text', key: 'apiKey', placeholder: 'Enter key…', validate: (v) => v.length < 8 ? 'Must be at least 8 characters.' : undefined, }, } // Dropdown with default { name: 'Theme', control: { type: 'dropdown', key: 'theme', defaultValue: 'system', options: { system: 'System', light: 'Light', dark: 'Dark' }, }, } // Number with range { name: 'Max results', control: { type: 'number', key: 'maxResults', min: 1, max: 100, defaultValue: 20 }, } // Slider (min/max/step required) { name: 'Opacity', control: { type: 'slider', key: 'opacity', min: 0, max: 100, step: 1 }, } // Folder picker { name: 'Output folder', control: { type: 'folder', key: 'outputDir', includeRoot: true }, } ``` ## Conditional visibility and disabling Two predicates toggle a setting's state without rebuilding the tab: - `visible` on any definition — hides the row when `false`. Hidden rows are excluded from search. - `disabled` on a `control` or `action` — disables interaction without hiding. Both accept `boolean | (() => boolean)`. The function form re-evaluates on every DOM-state refresh. For `control` definitions, Obsidian refreshes automatically after every change. ```ts getSettingDefinitions() { return [ { name: 'Advanced mode', control: { type: 'toggle', key: 'advanced' } }, { name: 'Debug level', visible: () => this.plugin.settings.advanced, control: { type: 'dropdown', key: 'logLevel', defaultValue: 'info', options: { info: 'Info', verbose: 'Verbose' }, }, }, ]; } ``` Use `visible` when a setting is irrelevant in the current state. Use `disabled` when it exists but is locked (prerequisite not met, feature not unlocked). After mutating state from a `render` callback or other imperative path, call `this.refreshDomState()` to re-run predicates without a full re-render. For changes that add/remove definitions (not just toggle visibility), call `this.update()` instead. ## Validation Every control accepts an optional `validate` callback. Return a non-empty string to reject and show an inline error. Return `void`/`undefined`/empty string to accept. Async validators work too. ```ts { name: 'Extension', control: { type: 'text', key: 'ext', validate: (v) => /\s/.test(v) ? 'No spaces allowed.' : undefined, }, } ``` `validate` is a UI gate, not a data invariant. Stored values may already be invalid (from older plugin versions). Validate again when reading settings in `loadSettings()` if invariants matter. ## Groups Group related settings under a heading: ```ts { type: 'group', heading: 'Appearance', items: [ { name: 'Font size', control: { type: 'number', key: 'fontSize', min: 8, max: 32 } }, { name: 'Accent color', control: { type: 'color', key: 'accent' } }, ], } ``` Groups also accept `search` (renders a filter input in the header — since 1.13.1), `extraButtons`, `cls`, and `visible`. Groups cannot nest inside groups. Use sub-pages for deeper hierarchy. ## Lists For user-managed collections (add/delete/reorder rows), use `type: 'list'`: ```ts { type: 'list', heading: 'Watched folders', emptyState: 'No folders yet.', addItem: { name: 'Add folder', action: () => this.openAddFolderModal(), }, onReorder: async (oldIndex, newIndex) => { let folders = this.plugin.settings.folders; let [moved] = folders.splice(oldIndex, 1); folders.splice(newIndex, 0, moved); await this.plugin.saveData(this.plugin.settings); }, onDelete: async (idx) => { this.plugin.settings.folders.splice(idx, 1); await this.plugin.saveData(this.plugin.settings); this.update(); }, items: this.plugin.settings.folders.map((path) => ({ name: path, searchable: false, })), } ``` - `onReorder` adds drag handles. DOM is already reordered — just update your data and save. - `onDelete` wires both the delete button and the Delete key. Always call `this.update()` after. - `addItem` renders a `+` button (desktop) / tappable row (mobile). Open a Modal for multi-field input. - `emptyState` shows when `items` is empty. - `searchable: false` on items keeps individual rows out of global search. For action rows inside lists, use `action` and read the live `index` argument (not the outer map index, which goes stale after reorder): ```ts items: commands.map((cmd) => ({ name: cmd.name, searchable: false, action: (el, index) => this.plugin.doSomething(index), })), ``` ## Sub-pages Navigable sub-pages for sections with self-contained scope. Use sparingly — only when the parent tab is too long to scan. ### Declarative (preferred) ```ts { type: 'page', name: 'Advanced', desc: 'Power-user options.', items: [ { name: 'Debug logging', control: { type: 'toggle', key: 'debug' } }, { type: 'group', heading: 'Cache', items: [ { name: 'Size (MB)', control: { type: 'slider', key: 'cacheMb', min: 1, max: 500, step: 1 } }, ]}, ], } ``` ### Imperative (when runtime state drives UI) Subclass `SettingPage` and pass a factory. Controls built in `display()` are invisible to `getSettingDefinitions()` — no search indexing, no auto-save, no `visible`/`disabled` predicates. ```ts import { SettingPage, Setting } from 'obsidian'; class StatusPage extends SettingPage { constructor(private plugin: MyPlugin) { super(); this.title = 'Status'; } display() { this.containerEl.empty(); new Setting(this.containerEl) .setName('Refresh') .addButton((btn) => btn.setButtonText('Refresh').onClick(() => this.display())); } hide() { /* clean up observers/timers */ } } // In getSettingDefinitions(): { type: 'page', name: 'Status', page: () => new StatusPage(this.plugin) } ``` `items` and `page` are mutually exclusive. Page names must be unique among siblings at the same depth. Since 1.13.1: `displayValue` shows a value on the page entry, `status: 'warning'` adds an indicator. ## Render callback Use `render` when a setting needs side effects, derived values, or controls not covered by the declarative types (moment format, progress bars, custom suggesters, multi-button rows). ```ts { name: 'Date format', render: (setting) => { setting.addMomentFormat((fmt) => fmt .setValue(this.plugin.settings.dateFormat) .onChange(async (v) => { this.plugin.settings.dateFormat = v; await this.plugin.saveData(this.plugin.settings); })); }, } ``` `render` does not auto-save — always call `saveData()` yourself. Return a cleanup function from `render` if you create subscriptions that outlive the DOM (ResizeObserver, setInterval, etc.). Plain DOM listeners attached to elements inside the row are cleaned up automatically. ## Custom settings storage By default, `control` definitions read/write `this.plugin.settings[key]` and auto-call `saveData()`. Override `getControlValue(key)` and `setControlValue(key, value)` to read/write a different store (Svelte store, reactive proxy, immutable pattern). When overriding `setControlValue`, persist the value yourself — the automatic `saveData()` call is replaced. See `references/settings-guide.md` § "Custom settings storage" and § "Advanced: nested settings with dot-notation keys" for the dot-path recipe. ## Reacting to external changes If the tab displays state that changes outside the settings UI (vault contents, plugin computation), call `this.update()` to rebuild from `getSettingDefinitions()`. Wire listeners in the constructor and register through `plugin.registerEvent()`. Debounce bursty events. ```ts constructor(app: App, plugin: MyPlugin) { super(app, plugin); this.plugin = plugin; let refresh = debounce(() => this.update(), 200, true); plugin.registerEvent(this.app.vault.on('create', refresh)); plugin.registerEvent(this.app.vault.on('delete', refresh)); } ``` ### Gotcha: `update()` skips the row holding focus The reconciler behind `update()`/`refreshCurrentPage` **deliberately skips clearing and re-rendering any setting row whose `settingEl` contains `document.activeElement`** — it preserves focus while the user edits a field. A matched row (stable key) only re-runs its `render`/control build when
GitHub에서 보기
이 SKILL.md는 매우 커서 SkillsMP가 여기에는 첫 섹션만 미리 보여줍니다. GitHub에서 보기