import type { App } from 'obsidian' import type { PMSettings, PriorityConfig, StatusConfig } from '../types' export interface TaskNotesStatus { id: string value: string label: string color: string isCompleted: boolean order: number } export interface TaskNotesPriority { id: string value: string label: string color: string weight: number } export interface TaskNotesDependency { uid: string reltype?: string gap?: string } /** A TaskNotes task from its runtime API, field names already normalized by their mapping. */ export interface TaskNotesTaskInfo { path: string title: string status: string priority: string due?: string scheduled?: string timeEstimate?: number tags?: string[] projects?: string[] blockedBy?: Array archived: boolean completedDate?: string dateCreated?: string dateModified?: string recurrence?: string } /** The slice of the TaskNotes runtime API v1 this plugin consumes. */ export interface TaskNotesApi { apiVersion: number hasCapability(capability: string): boolean getStatuses(): TaskNotesStatus[] getPriorities(): TaskNotesPriority[] getTask(path: string): Promise getSettingsSnapshot(): { taskTag?: string; fieldMapping?: Record } } function getTaskNotesPlugin(app: App): { api?: unknown } | null { const registry = (app as App & { plugins?: { getPlugin?: (id: string) => unknown } }).plugins const plugin = registry?.getPlugin?.('tasknotes') return plugin && typeof plugin === 'object' ? plugin : null } /** Installed and enabled, whatever its version. */ export function isTaskNotesInstalled(app: App): boolean { return getTaskNotesPlugin(app) !== null } /** Null when the plugin is missing or predates API v1 (TaskNotes 4.10). */ export function getTaskNotesApi(app: App): TaskNotesApi | null { const api = getTaskNotesPlugin(app)?.api as TaskNotesApi | undefined if (!api || api.apiVersion !== 1 || !api.hasCapability('catalog.read')) return null return api } interface PaletteUpsert { id: string make: () => T differs: (existing: T) => boolean apply: (existing: T) => void } /** * Patch matching ids in place and insert missing ones after the last match, so * TaskNotes' relative order carries over without disturbing entries it doesn't know. */ function upsertPalette( target: T[], incoming: PaletteUpsert[] ): { added: number; updated: number } { let added = 0 let updated = 0 let anchor = -1 for (const item of incoming) { const idx = target.findIndex((cfg) => cfg.id === item.id) if (idx >= 0) { anchor = idx if (item.differs(target[idx])) { item.apply(target[idx]) updated++ } } else { anchor += 1 target.splice(anchor, 0, item.make()) added++ } } return { added, updated } } /** What `upsertPalette` would change, without touching the target. */ function countPaletteChanges( target: T[], incoming: PaletteUpsert[] ): { added: number; updated: number } { let added = 0 let updated = 0 for (const item of incoming) { const existing = target.find((cfg) => cfg.id === item.id) if (!existing) added++ else if (item.differs(existing)) updated++ } return { added, updated } } /** Resolves a "[[wikilink]]" or plain vault path, or null when it doesn't resolve. */ export function resolveTaskNotesRef(app: App, ref: string, sourcePath: string): string | null { const inner = ref.replace(/^\[\[/, '').replace(/\]\]$/, '').split('|')[0].split('#')[0].trim() if (!inner) return null if (app.vault.getFileByPath(inner)) return inner return app.metadataCache.getFirstLinkpathDest(inner, sourcePath)?.path ?? null } /** * Append entries for TaskNotes values imported tasks use but our settings don't define, * so those tasks stay visible in status-driven views. Returns how many were added. */ export function ensurePaletteEntries( api: TaskNotesApi, settings: PMSettings, statusValues: Set, priorityValues: Set ): number { let added = 0 for (const s of api.getStatuses()) { if (statusValues.has(s.value) && !settings.statuses.some((cfg) => cfg.id === s.value)) { settings.statuses.push({ id: s.value, label: s.label, color: s.color, icon: '', complete: s.isCompleted }) added++ } } for (const p of api.getPriorities()) { if (priorityValues.has(p.value) && !settings.priorities.some((cfg) => cfg.id === p.value)) { settings.priorities.push({ id: p.value, label: p.label, color: p.color, icon: '' }) added++ } } return added } function statusUpserts(api: TaskNotesApi): PaletteUpsert[] { return [...api.getStatuses()] .sort((a, b) => a.order - b.order) .map((s) => ({ id: s.value, make: () => ({ id: s.value, label: s.label, color: s.color, icon: '', complete: s.isCompleted }), differs: (existing: StatusConfig) => existing.label !== s.label || existing.color !== s.color || existing.complete !== s.isCompleted, apply: (existing: StatusConfig) => { existing.label = s.label existing.color = s.color existing.complete = s.isCompleted } })) } function priorityUpserts(api: TaskNotesApi): PaletteUpsert[] { return [...api.getPriorities()] .sort((a, b) => b.weight - a.weight) .map((p) => ({ id: p.value, make: () => ({ id: p.value, label: p.label, color: p.color, icon: '' }), differs: (existing: PriorityConfig) => existing.label !== p.label || existing.color !== p.color, apply: (existing: PriorityConfig) => { existing.label = p.label existing.color = p.color } })) } /** Upsert TaskNotes' configured statuses and priorities into our palettes. */ export function importTaskNotesPalettes(api: TaskNotesApi, settings: PMSettings): { added: number; updated: number } { const statuses = upsertPalette(settings.statuses, statusUpserts(api)) const priorities = upsertPalette(settings.priorities, priorityUpserts(api)) return { added: statuses.added + priorities.added, updated: statuses.updated + priorities.updated } } /** What `importTaskNotesPalettes` would change right now, without changing anything. */ export function countTaskNotesPaletteChanges( api: TaskNotesApi, settings: PMSettings ): { added: number; updated: number } { const statuses = countPaletteChanges(settings.statuses, statusUpserts(api)) const priorities = countPaletteChanges(settings.priorities, priorityUpserts(api)) return { added: statuses.added + priorities.added, updated: statuses.updated + priorities.updated } }