Skip to content

Instantly share code, notes, and snippets.

@dsebastien
Created July 21, 2026 06:53
Show Gist options
  • Select an option

  • Save dsebastien/f93665134ce37159ff4ff30667ef8c0c to your computer and use it in GitHub Desktop.

Select an option

Save dsebastien/f93665134ce37159ff4ff30667ef8c0c to your computer and use it in GitHub Desktop.
Dispatcher Templater template

<%* // Master Templater dispatcher — auto-detects note type via OSK plugin and // applies the matching type-specific template. // // Both paths converge on write_template_to_file(tplFile, file), the same // call the OSK plugin uses internally (main.js applyTemplaterTemplate). It // targets a specific file by reference, so it sidesteps two failure modes // of the prior implementation: // - tp.file.include racing with the included template's tp.file.move // (content can be written to the pre-move path and lost). // - append_template_to_active_file reading a stale active file after the // suggester modal closes (template applied to wrong file or no-op). // // Behavior: // 1. File already has content → exit (idempotency guard, e.g. Periodic Notes). // 2. File has a real name → recognize/pick on next tick → write template. // 3. File is "Untitled" → register rename watcher, exit silently. // On rename → recognize/pick → write template. // // Wired as the root folder_template in .obsidian/plugins/templater-obsidian/data.json.

if (tp.file.content && tp.file.content.length > 0) return;

const oskApi = app.plugins.plugins['obsidian-starter-kit']?.api; const tplPlugin = app.plugins.plugins['templater-obsidian']; if (oskApi == null || tplPlugin == null) return; if (typeof tplPlugin.templater?.write_template_to_file !== 'function') return;

const isUntitled = (name) => /^Untitled(\s+\d+)?$/i.test(name);

const recognizeOrPick = async (path) => { const recognized = await oskApi.recognizeNoteType(path); if (recognized && recognized.success === true && recognized.data != null) { return recognized.data; } // Periodic-notes fallback — bypasses the suggester for wikilink navigation // clicks (prev/next day, week breadcrumb, etc.). Detection is derived from // the Periodic Notes plugin's own settings (single source of truth): each // granularity's format and template are read live, so this never drifts // when the user changes a format or template path in the plugin. // A note's basename is matched against the leaf segment of the moment format // via strict parsing — correct for week-year (gggg/ww) and quarter (Q) tokens // that a plain \d{4} regex only approximates. // Returns { templatePath } directly — the only field applyTemplate needs. // The tp.file.move in each periodic template relocates the file from vault // root to the correct subfolder automatically. const basename = path.split('/').pop().replace(/.md$/i, ''); const pn = app.plugins.plugins['periodic-notes']; let pnSettings = pn?.settings; if (pnSettings == null) { try { pnSettings = JSON.parse(await app.vault.adapter.read('.obsidian/plugins/periodic-notes/data.json')); } catch (e) { pnSettings = null; } } if (pnSettings != null && window.moment != null) { // Order matters: try finer granularities first so a shorter format cannot // shadow a longer one (it can't with strict parsing, but keep it explicit). for (const g of ['daily', 'weekly', 'monthly', 'quarterly', 'yearly']) { const cfg = pnSettings[g]; if (!cfg || cfg.enabled === false || !cfg.format || !cfg.template) continue; const fmtLeaf = cfg.format.split('/').pop(); // basename portion of the format if (window.moment(basename, fmtLeaf, true).isValid()) { return { templatePath: cfg.template }; } } } const list = oskApi.listNoteTypes(); const types = (list && list.success === true && Array.isArray(list.data)) ? list.data : []; const sorted = types.slice().sort((a, b) => (a.name || '').localeCompare(b.name || '')); return await tp.system.suggester(t => t.name, sorted, false, 'Pick a note type (Esc to skip)'); };

const applyTemplate = async (file, noteType) => { if (!noteType || !noteType.templatePath) return; const tplFile = app.vault.getAbstractFileByPath(noteType.templatePath); if (tplFile == null) return; // Re-check idempotency: user may have typed between the dispatcher firing // and the template being applied. const content = await app.vault.read(file); if (content && content.trim().length > 0) return; await tplPlugin.templater.write_template_to_file(tplFile, file); };

const targetPath = tp.file.path(true); const targetFile = app.vault.getAbstractFileByPath(targetPath); if (targetFile == null) return;

// Path A: file has a real name. Defer to next tick so the dispatcher's render // (empty) finalizes first, then write the chosen template to the file. if (!isUntitled(targetFile.basename)) { setTimeout(async () => { const noteType = await recognizeOrPick(targetFile.path); await applyTemplate(targetFile, noteType); }, 0); return; }

// Path B: file is "Untitled". Register a one-time rename watcher. let dispatched = false; const ref = app.vault.on('rename', async (file, oldPath) => { if (dispatched) return; // Match by reference (Obsidian mutates path on the same TFile) or by oldPath. if (file !== targetFile && oldPath !== targetPath) return; if (isUntitled(file.basename)) return; dispatched = true; app.vault.offref(ref);

const noteType = await recognizeOrPick(file.path); await applyTemplate(file, noteType); });

// Safety: drop the watcher after 10 minutes if no rename happened. setTimeout(() => { if (!dispatched) { dispatched = true; app.vault.offref(ref); } }, 10 * 60 * 1000); -%>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment