Skip to main content

Overview

Inkdown uses an event system to notify plugins of changes and user actions. Subscribe to events to react to file changes, editor updates, and more.
// Subscribe to file creation
this.registerEvent(
  this.app.workspace.onFileCreate((file) => {
    console.log('File created:', file.path);
  })
);

Events Base Class

Many Inkdown managers extend the Events class.
abstract class Events {
  on(name: string, callback: (...data: unknown[]) => void, ctx?: unknown): EventRef;
  off(name: string, callback: (...data: unknown[]) => void): void;
  offref(ref: EventRef): void;
  trigger(name: string, ...data: unknown[]): void;
}

type EventRef = () => void;

Registration

Using Plugin.registerEvent()

registerEvent
(eventRef: EventRef) => void
Register an event listener that’s automatically cleaned up when the plugin unloads.
export default class MyPlugin extends Plugin {
  async onload() {
    // Automatic cleanup
    this.registerEvent(
      this.app.workspace.onFileCreate((file) => {
        console.log('File created:', file.path);
      })
    );
  }
}
Always use registerEvent() to ensure listeners are cleaned up when your plugin unloads.

Manual Event Handling

If you need to manually manage events:
const eventRef = this.app.workspace.onFileCreate((file) => {
  console.log('File created:', file.path);
});

// Later, to unsubscribe:
eventRef();

Workspace Events

onFileCreate()

onFileCreate
(callback: (file: TFile) => void) => EventRef
Fired when a file is created.
this.registerEvent(
  this.app.workspace.onFileCreate((file) => {
    console.log('New file:', file.path);
    this.showNotice(`Created: ${file.basename}`);
    
    // Initialize file with template
    if (file.extension === 'md') {
      this.initializeFile(file);
    }
  })
);

onFileModify()

onFileModify
(callback: (file: TFile) => void) => EventRef
Fired when a file is modified.
this.registerEvent(
  this.app.workspace.onFileModify((file) => {
    console.log('File modified:', file.path);
    
    // Update cache
    this.invalidateCache(file.path);
    
    // Auto-save after modification
    this.scheduleAutoSave(file);
  })
);

onFileDelete()

onFileDelete
(callback: (file: TAbstractFile) => void) => EventRef
Fired when a file or folder is deleted.
this.registerEvent(
  this.app.workspace.onFileDelete((file) => {
    console.log('Deleted:', file.path);
    
    // Clean up references
    this.removeFromIndex(file.path);
    
    // Update UI
    this.refreshView();
  })
);

onFileRename()

onFileRename
(callback: (file: TFile, oldPath: string) => void) => EventRef
Fired when a file is renamed or moved.
this.registerEvent(
  this.app.workspace.onFileRename((file, oldPath) => {
    console.log(`Renamed: ${oldPath} -> ${file.path}`);
    
    // Update links
    this.updateLinks(oldPath, file.path);
    
    // Update bookmarks
    this.updateBookmarks(oldPath, file.path);
  })
);

Workspace UI Events

onActiveFileChange()

onActiveFileChange
(callback: (file: TFile | null) => void) => EventRef
Fired when the active file changes.
this.registerEvent(
  this.app.workspaceUI.onActiveFileChange((file) => {
    if (file) {
      console.log('Active file:', file.path);
      this.updateStatusBar(file);
    } else {
      console.log('No active file');
      this.clearStatusBar();
    }
  })
);

onFileOpen()

onFileOpen
(callback: (file: TFile) => void) => EventRef
Fired when a file is opened.
this.registerEvent(
  this.app.workspaceUI.onFileOpen((file) => {
    console.log('File opened:', file.path);
    this.addToRecentFiles(file.path);
    this.preloadRelatedFiles(file);
  })
);

Metadata Cache Events

onChanged()

onChanged
(callback: (file: TFile, data: string, cache: CachedMetadata) => void) => EventRef
Fired when file metadata is updated.
this.registerEvent(
  this.app.metadataCache.onChanged((file, data, cache) => {
    console.log('Metadata updated:', file.path);
    console.log('Frontmatter:', cache.frontmatter);
    console.log('Links:', cache.links);
    console.log('Tags:', cache.tags);
    
    // Update search index
    this.updateSearchIndex(file, cache);
  })
);

Manager Events

Managers like BookmarkManager and TagManager emit generic events:

BookmarkManager

this.registerEvent(
  this.app.bookmarkManager.on('change', () => {
    console.log('Bookmarks changed');
    this.refreshBookmarkView();
  })
);

TagManager

this.registerEvent(
  this.app.tagManager.on('change', () => {
    console.log('Tags changed');
    this.refreshTagView();
  })
);

Plugin Manager (via Plugins)

const unsubscribe = this.app.plugins.onPluginChange(
  (pluginId, changeType) => {
    console.log(`${pluginId}: ${changeType}`);
    
    if (changeType === 'enabled') {
      console.log(`${pluginId} was enabled`);
    } else if (changeType === 'disabled') {
      console.log(`${pluginId} was disabled`);
    } else if (changeType === 'settings-changed') {
      console.log(`${pluginId} settings changed`);
    }
  }
);

// Manual cleanup (if not using registerEvent)
// unsubscribe();

DOM Events

registerDomEvent()

registerDomEvent
(el: HTMLElement, type: string, callback: Function) => void
Register DOM event listeners with automatic cleanup.
const button = document.createElement('button');
button.textContent = 'Click Me';

this.registerDomEvent(button, 'click', (evt: MouseEvent) => {
  console.log('Button clicked');
  this.showNotice('Button clicked!');
});

// Window events
this.registerDomEvent(window, 'resize', () => {
  console.log('Window resized');
  this.updateLayout();
});

// Document events
this.registerDomEvent(document, 'keydown', (evt: KeyboardEvent) => {
  if (evt.key === 'Escape') {
    this.closeModal();
  }
});

Intervals and Timers

registerInterval()

registerInterval
(id: number) => number
Register an interval for automatic cleanup.
export default class MyPlugin extends Plugin {
  async onload() {
    // Auto-save every 30 seconds
    this.registerInterval(
      window.setInterval(() => {
        this.autoSave();
      }, 30000)
    );
    
    // Update status bar every second
    this.registerInterval(
      window.setInterval(() => {
        this.updateStatusBar();
      }, 1000)
    );
  }
}

Common Patterns

Track File Changes

export default class MyPlugin extends Plugin {
  private modifiedFiles = new Set<string>();

  async onload() {
    this.registerEvent(
      this.app.workspace.onFileModify((file) => {
        this.modifiedFiles.add(file.path);
      })
    );

    // Process modified files periodically
    this.registerInterval(
      window.setInterval(() => {
        if (this.modifiedFiles.size > 0) {
          this.processModifiedFiles(Array.from(this.modifiedFiles));
          this.modifiedFiles.clear();
        }
      }, 5000)
    );
  }

  async processModifiedFiles(paths: string[]) {
    console.log('Processing modified files:', paths);
    // Process files...
  }
}

Debounce Events

export default class MyPlugin extends Plugin {
  private saveTimeout: number | null = null;

  async onload() {
    this.registerEvent(
      this.app.workspace.onFileModify((file) => {
        this.debouncedSave(file);
      })
    );
  }

  debouncedSave(file: TFile) {
    if (this.saveTimeout !== null) {
      clearTimeout(this.saveTimeout);
    }

    this.saveTimeout = window.setTimeout(() => {
      this.performSave(file);
      this.saveTimeout = null;
    }, 2000);
  }

  async performSave(file: TFile) {
    console.log('Saving:', file.path);
    // Perform save...
  }
}

React to Active File

export default class MyPlugin extends Plugin {
  private currentFile: TFile | null = null;

  async onload() {
    this.registerEvent(
      this.app.workspaceUI.onActiveFileChange((file) => {
        this.onActiveFileChange(file);
      })
    );
  }

  onActiveFileChange(file: TFile | null) {
    // Clean up previous file
    if (this.currentFile) {
      this.cleanupFile(this.currentFile);
    }

    // Set up new file
    this.currentFile = file;
    if (file) {
      this.setupFile(file);
    }
  }

  setupFile(file: TFile) {
    console.log('Setting up:', file.path);
    // Initialize for this file
  }

  cleanupFile(file: TFile) {
    console.log('Cleaning up:', file.path);
    // Clean up resources
  }
}

Update on Metadata Changes

export default class MyPlugin extends Plugin {
  private cache = new Map<string, CachedMetadata>();

  async onload() {
    this.registerEvent(
      this.app.metadataCache.onChanged((file, data, cache) => {
        this.cache.set(file.path, cache);
        this.updateView(file, cache);
      })
    );
  }

  updateView(file: TFile, cache: CachedMetadata) {
    // Check for specific changes
    if (cache.frontmatter?.status === 'published') {
      this.showNotice(`${file.basename} is published`);
    }

    // Update based on tags
    if (cache.tags?.some(t => t.tag === '#important')) {
      this.addToImportantList(file);
    }
  }
}

Best Practices

Always Use registerEvent()

// ✅ Good - automatic cleanup
this.registerEvent(
  this.app.workspace.onFileCreate((file) => {
    console.log('Created:', file.path);
  })
);

// ❌ Bad - manual cleanup required
const ref = this.app.workspace.onFileCreate((file) => {
  console.log('Created:', file.path);
});
// Must remember to call ref() in onunload!

Handle Null Values

// ✅ Good
this.registerEvent(
  this.app.workspaceUI.onActiveFileChange((file) => {
    if (file) {
      this.updateForFile(file);
    } else {
      this.clearView();
    }
  })
);

// ❌ Bad - assumes file exists
this.registerEvent(
  this.app.workspaceUI.onActiveFileChange((file) => {
    this.updateForFile(file); // May be null!
  })
);

Avoid Heavy Operations in Event Handlers

// ✅ Good - debounce heavy operations
private updateTimeout: number | null = null;

this.registerEvent(
  this.app.workspace.onFileModify((file) => {
    if (this.updateTimeout) clearTimeout(this.updateTimeout);
    this.updateTimeout = window.setTimeout(() => {
      this.heavyOperation(file);
    }, 500);
  })
);

// ❌ Bad - heavy operation on every change
this.registerEvent(
  this.app.workspace.onFileModify((file) => {
    this.heavyOperation(file); // Called too often!
  })
);

Clean Up Intervals

// ✅ Good - use registerInterval
this.registerInterval(
  window.setInterval(() => {
    this.update();
  }, 1000)
);

// ❌ Bad - manual cleanup needed
const interval = window.setInterval(() => {
  this.update();
}, 1000);
// Must clear in onunload!