Commands and settings are essential features for making your plugin accessible and configurable. This guide covers how to register commands and create settings interfaces.
Plugin Commands
Commands allow users to trigger plugin functionality from the command palette or via hotkeys.
Basic Command
Register a simple command:
import { Plugin } from 'obsidian' ;
export default class ExamplePlugin extends Plugin {
async onload () {
this . addCommand ({
id: 'example-command' ,
name: 'Example command' ,
callback : () => {
console . log ( 'Command executed!' );
}
});
}
}
The command ID is automatically prefixed with your plugin ID. Users see the name in the command palette.
Command with Icon
Add an icon to commands that appear in the toolbar:
this . addCommand ({
id: 'open-view' ,
name: 'Open custom view' ,
icon: 'layout-dashboard' ,
callback : () => {
// Open your view
}
});
Command Structure
interface Command {
id : string ; // Unique command ID
name : string ; // Display name
icon ?: string ; // Optional icon
mobileOnly ?: boolean ; // Only on mobile
repeatable ?: boolean ; // Repeat when holding hotkey
callback ?: () => any ; // Simple callback
checkCallback ?: ( checking : boolean ) => boolean | void ;
editorCallback ?: ( editor : Editor , view : MarkdownView ) => any ;
editorCheckCallback ?: ( checking : boolean , editor : Editor , view : MarkdownView ) => boolean | void ;
hotkeys ?: Hotkey []; // Default hotkeys
}
Check Callbacks
Use check callbacks to conditionally enable commands:
this . addCommand ({
id: 'format-selection' ,
name: 'Format selected text' ,
checkCallback : ( checking : boolean ) => {
const view = this . app . workspace . getActiveViewOfType ( MarkdownView );
// Check if command should be available
if ( view && view . editor . somethingSelected ()) {
if ( ! checking ) {
// Only execute when not checking
const selection = view . editor . getSelection ();
view . editor . replaceSelection ( selection . toUpperCase ());
}
return true ; // Command is available
}
return false ; // Command is hidden
}
});
When checking is true, the command palette is just checking if the command should be shown. Don’t perform the action during checking.
Editor Commands
Commands that work with the active editor:
Editor Callback
this . addCommand ({
id: 'insert-date' ,
name: 'Insert current date' ,
editorCallback : ( editor : Editor , view : MarkdownView ) => {
const date = new Date (). toISOString (). split ( 'T' )[ 0 ];
editor . replaceSelection ( date );
}
});
Editor Check Callback
this . addCommand ({
id: 'toggle-bold' ,
name: 'Toggle bold formatting' ,
editorCheckCallback : ( checking : boolean , editor : Editor , view : MarkdownView ) => {
const selection = editor . getSelection ();
if ( selection ) {
if ( ! checking ) {
if ( selection . startsWith ( '**' ) && selection . endsWith ( '**' )) {
// Remove bold
editor . replaceSelection ( selection . slice ( 2 , - 2 ));
} else {
// Add bold
editor . replaceSelection ( `** ${ selection } **` );
}
}
return true ;
}
return false ;
}
});
Default Hotkeys
Avoid setting default hotkeys when possible to prevent conflicts with user-configured hotkeys.
If you must set a default hotkey:
this . addCommand ({
id: 'quick-action' ,
name: 'Quick action' ,
hotkeys: [
{
modifiers: [ 'Mod' , 'Shift' ],
key: 'q'
}
],
callback : () => {
// Action
}
});
Modifier keys:
'Mod' - Cmd on macOS, Ctrl elsewhere
'Ctrl' - Ctrl on all platforms
'Meta' - Cmd on macOS, Win key elsewhere
'Shift' - Shift key
'Alt' - Alt/Option key
Repeatable Commands
Allow commands to repeat when holding the hotkey:
this . addCommand ({
id: 'increase-heading' ,
name: 'Increase heading level' ,
repeatable: true ,
editorCallback : ( editor ) => {
const cursor = editor . getCursor ();
const line = editor . getLine ( cursor . line );
if ( line . startsWith ( '#' )) {
editor . setLine ( cursor . line , '#' + line );
}
}
});
Remove Commands
Dynamically remove commands (rarely needed):
// Remove a command
this . removeCommand ( 'plugin-id:command-id' );
Plugin Settings
Create a settings tab to let users configure your plugin.
Define Settings Interface
interface ExampleSettings {
apiKey : string ;
enableFeature : boolean ;
refreshInterval : number ;
theme : 'light' | 'dark' | 'auto' ;
}
const DEFAULT_SETTINGS : ExampleSettings = {
apiKey: '' ,
enableFeature: true ,
refreshInterval: 60 ,
theme: 'auto'
};
Create Settings Tab
import { App , PluginSettingTab , Setting } from 'obsidian' ;
import ExamplePlugin from './main' ;
export class ExampleSettingTab extends PluginSettingTab {
plugin : ExamplePlugin ;
constructor ( app : App , plugin : ExamplePlugin ) {
super ( app , plugin );
this . plugin = plugin ;
}
display () : void {
const { containerEl } = this ;
containerEl . empty ();
containerEl . createEl ( 'h2' , { text: 'Example Plugin Settings' });
// Text input
new Setting ( containerEl )
. setName ( 'API Key' )
. setDesc ( 'Enter your API key for external service' )
. addText ( text => text
. setPlaceholder ( 'Enter your key' )
. setValue ( this . plugin . settings . apiKey )
. onChange ( async ( value ) => {
this . plugin . settings . apiKey = value ;
await this . plugin . saveSettings ();
}));
// Toggle
new Setting ( containerEl )
. setName ( 'Enable feature' )
. setDesc ( 'Turn on the special feature' )
. addToggle ( toggle => toggle
. setValue ( this . plugin . settings . enableFeature )
. onChange ( async ( value ) => {
this . plugin . settings . enableFeature = value ;
await this . plugin . saveSettings ();
}));
// Slider
new Setting ( containerEl )
. setName ( 'Refresh interval' )
. setDesc ( 'How often to refresh (in seconds)' )
. addSlider ( slider => slider
. setLimits ( 10 , 300 , 10 )
. setValue ( this . plugin . settings . refreshInterval )
. setDynamicTooltip ()
. onChange ( async ( value ) => {
this . plugin . settings . refreshInterval = value ;
await this . plugin . saveSettings ();
}));
// Dropdown
new Setting ( containerEl )
. setName ( 'Theme' )
. setDesc ( 'Choose color theme' )
. addDropdown ( dropdown => dropdown
. addOption ( 'light' , 'Light' )
. addOption ( 'dark' , 'Dark' )
. addOption ( 'auto' , 'Auto' )
. setValue ( this . plugin . settings . theme )
. onChange ( async ( value ) => {
this . plugin . settings . theme = value as any ;
await this . plugin . saveSettings ();
}));
}
}
Register Settings Tab
export default class ExamplePlugin extends Plugin {
settings : ExampleSettings ;
async onload () {
await this . loadSettings ();
// Add settings tab
this . addSettingTab ( new ExampleSettingTab ( this . app , this ));
}
async loadSettings () {
this . settings = Object . assign ({}, DEFAULT_SETTINGS , await this . loadData ());
}
async saveSettings () {
await this . saveData ( this . settings );
}
}
Advanced Settings
Section Headings
new Setting ( containerEl )
. setName ( 'Advanced options' )
. setHeading ();
new Setting ( containerEl )
. setName ( 'Debug mode' )
. addToggle ( ... );
new Setting ( containerEl )
. setName ( 'Clear cache' )
. setDesc ( 'Remove all cached data' )
. addButton ( button => button
. setButtonText ( 'Clear' )
. setWarning ()
. onClick ( async () => {
await this . plugin . clearCache ();
new Notice ( 'Cache cleared!' );
}));
Disabled Settings
const setting = new Setting ( containerEl )
. setName ( 'Pro feature' )
. addToggle ( toggle => toggle
. setValue ( false ));
if ( ! this . plugin . isPro ) {
setting . setDisabled ( true );
}
new Setting ( containerEl )
. setName ( 'Date range' )
. addText ( text => text
. setPlaceholder ( 'Start date' )
. onChange ( async ( value ) => {
this . plugin . settings . startDate = value ;
await this . plugin . saveSettings ();
}))
. addText ( text => text
. setPlaceholder ( 'End date' )
. onChange ( async ( value ) => {
this . plugin . settings . endDate = value ;
await this . plugin . saveSettings ();
}));
Custom Setting Content
const setting = new Setting ( containerEl )
. setName ( 'Custom control' );
setting . controlEl . createDiv ({ cls: 'custom-control' }, ( el ) => {
// Build custom UI
const button = el . createEl ( 'button' , { text: 'Custom' });
button . addEventListener ( 'click' , () => {
// Handle click
});
});
Settings Best Practices
Make setting names clear and descriptive:
// Good
new Setting ( containerEl )
. setName ( 'Automatically save on window blur' )
. setDesc ( 'Save your work when switching to another application' );
// Avoid
new Setting ( containerEl )
. setName ( 'Auto-save' );
Validate user input before saving:
new Setting ( containerEl )
. setName ( 'Port number' )
. addText ( text => text
. setValue ( String ( this . plugin . settings . port ))
. onChange ( async ( value ) => {
const port = parseInt ( value );
if ( port > 0 && port < 65536 ) {
this . plugin . settings . port = port ;
await this . plugin . saveSettings ();
} else {
new Notice ( 'Invalid port number' );
}
}));
Always provide sensible defaults:
const DEFAULT_SETTINGS : ExampleSettings = {
// All settings have defaults
apiKey: '' ,
enabled: true ,
timeout: 5000
};
async loadSettings () {
// Merge with defaults
this . settings = Object . assign ({}, DEFAULT_SETTINGS , await this . loadData ());
}
Update plugin behavior when settings change:
async saveSettings () {
await this . saveData ( this . settings );
// Apply settings
this . updateInterval ();
this . refreshUI ();
}
External Settings Changes
Handle when settings are modified externally (e.g., by sync):
export default class ExamplePlugin extends Plugin {
async onload () {
await this . loadSettings ();
}
onExternalSettingsChange () {
// Reload settings from disk
this . loadSettings ();
}
async loadSettings () {
this . settings = Object . assign ({}, DEFAULT_SETTINGS , await this . loadData ());
// Apply new settings
this . applySettings ();
}
}
UI Components Build modals and user interfaces
Editor Integration Work with the editor API