diff --git a/package.json b/package.json index a2c3f99..6267df5 100644 --- a/package.json +++ b/package.json @@ -23,12 +23,10 @@ "@rollup/plugin-node-resolve": "11.2.0", "@rollup/plugin-replace": "2.4.1", "@rollup/plugin-typescript": "8.2.0", - "@types/moment": "2.13.0", "@types/papaparse": "5.3.1", "@typescript-eslint/eslint-plugin": "4.17.0", "@typescript-eslint/parser": "4.17.0", "eslint": "7.22.0", - "moment": "^2.30.1", "rollup": "^2.80.0", "typescript": "4.2.3" } diff --git a/src/fileUtils.ts b/src/fileUtils.ts index e4fbb20..b9d0cac 100644 --- a/src/fileUtils.ts +++ b/src/fileUtils.ts @@ -1,19 +1,25 @@ import { Editor } from "codemirror"; -import { App, MarkdownView, TFile } from "obsidian"; +import { App, MarkdownView, TFile, WorkspaceLeaf } from "obsidian"; type MarkdownEditor = Editor & { replaceRange: Editor["replaceRange"]; }; +type MarkdownViewWithEditor = MarkdownView & { + editor?: MarkdownEditor; + sourceMode?: { cmEditor?: MarkdownEditor }; +}; + +function isMarkdownViewWithEditor(view: unknown): view is MarkdownViewWithEditor { + return view instanceof MarkdownView; +} + export function getEditorForFile(app: App, file: TFile): MarkdownEditor | null { - let editor = null; - app.workspace.iterateAllLeaves((leaf) => { - if (leaf.view instanceof MarkdownView && leaf.view.file === file) { - const view = leaf.view as MarkdownView & { - editor?: MarkdownEditor; - sourceMode?: { cmEditor?: MarkdownEditor }; - }; - editor = view.editor || view.sourceMode?.cmEditor; + let editor: MarkdownEditor | null = null; + app.workspace.iterateAllLeaves((leaf: WorkspaceLeaf) => { + const { view } = leaf; + if (isMarkdownViewWithEditor(view) && view.file === file) { + editor = view.editor ?? view.sourceMode?.cmEditor ?? null; } }); return editor; diff --git a/src/index.ts b/src/index.ts index cadedd9..cb99f18 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,11 +1,17 @@ -import type moment from "moment"; -import { Notice, Plugin, TFile, WorkspaceLeaf } from "obsidian"; +import { + Notice, + Platform, + Plugin, + TFile, + WorkspaceLeaf, +} from "obsidian"; import { createDailyNote, - getDailyNote, getAllDailyNotes, + getDailyNote, } from "obsidian-daily-notes-interface"; +import { getMoment, type MomentLike } from "./moment"; import { ConfirmationModal } from "./modal"; import { ToolkitRenderer } from "./renderer"; import { @@ -19,7 +25,6 @@ import { ISettings, ThingsToolkitSettingsTab, } from "./settings"; - import { buildTasksFromSQLRecords, fetchThingsToolkit, @@ -32,10 +37,28 @@ import { updateSection, } from "./textUtils"; -declare global { - interface Window { - moment: typeof moment; - } +type ReviewLeaf = WorkspaceLeaf & { + setViewState(state: { type: string; active: boolean }): Promise; + openFile(file: TFile): Promise; +}; + +type WorkspaceWithHelpers = typeof app.workspace & { + getLeavesOfType(type: string): WorkspaceLeaf[]; + getRightLeaf(split: boolean): WorkspaceLeaf | null; + revealLeaf(leaf: WorkspaceLeaf): Promise; + getLeaf(newLeaf?: boolean): WorkspaceLeaf; +}; + +function getWorkspace(plugin: Plugin): WorkspaceWithHelpers { + return plugin.app.workspace as WorkspaceWithHelpers; +} + +function isTFile(file: unknown): file is TFile { + return file instanceof TFile; +} + +function getDateKey(date: MomentLike): string { + return date.format("YYYY-MM-DD"); } export interface ISyncStatus { @@ -44,20 +67,18 @@ export interface ISyncStatus { } export default class ThingsToolkitPlugin extends Plugin { - public options: ISettings; + public options!: ISettings; public syncStatus: ISyncStatus = { isSyncing: false, message: "", }; - private syncTimeoutId: number; - private settingsTab: ThingsToolkitSettingsTab; - private statusBarEl: HTMLElement; + private syncTimeoutId?: number; + private settingsTab?: ThingsToolkitSettingsTab; + private statusBarEl?: HTMLElement; async onload(): Promise { - if (!isMacOS()) { - console.info( - "Failed to load Things Toolkit plugin. Platform not supported" - ); + if (!Platform.isDesktopApp || !isMacOS()) { + console.info("Failed to load Things Toolkit plugin. Platform not supported"); return; } @@ -72,29 +93,33 @@ export default class ThingsToolkitPlugin extends Plugin { ); this.addCommand({ - id: "sync-things-toolkit", + id: "sync", name: "Sync", - callback: () => setTimeout(() => this.tryToSyncLogbook(), 20), + callback: () => window.setTimeout(() => { + void this.tryToSyncLogbook(); + }, 20), }); this.addCommand({ - id: "open-things-toolkit-review", + id: "open-review", name: "Open review", - callback: () => this.activateReviewView(), + callback: () => { + void this.activateReviewView(); + }, }); - this.addRibbonIcon( - "calendar-check", - "Open Things toolkit review", - () => this.activateReviewView() - ); + this.addRibbonIcon("calendar-check", "Open Things toolkit review", () => { + void this.activateReviewView(); + }); await this.loadOptions(); this.statusBarEl = this.addStatusBarItem(); this.statusBarEl.addClass("things-toolkit-status"); - this.statusBarEl.addEventListener("click", () => this.activateReviewView()); + this.statusBarEl.addEventListener("click", () => { + void this.activateReviewView(); + }); this.updateStatusBar(); - this.app.workspace.onLayoutReady(() => { - this.refreshRecentDailyStats().then(() => { + getWorkspace(this).onLayoutReady(() => { + void this.refreshRecentDailyStats().then(() => { this.updateStatusBar(); this.refreshReviewViews(); }); @@ -104,21 +129,23 @@ export default class ThingsToolkitPlugin extends Plugin { this.addSettingTab(this.settingsTab); if (this.options.hasAcceptedDisclaimer && this.options.isSyncEnabled) { - if (this.app.workspace.layoutReady) { + if (getWorkspace(this).layoutReady) { this.scheduleNextSync(); } else { this.registerEvent( - this.app.workspace.on("layout-ready", this.scheduleNextSync) + getWorkspace(this).on("layout-ready", () => { + this.scheduleNextSync(); + }) ); } } } async activateReviewView(): Promise { - const { workspace } = this.app; - let leaf = workspace.getLeavesOfType(VIEW_TYPE_THINGS_TOOLKIT_REVIEW)[0]; + const workspace = getWorkspace(this); + let leaf = workspace.getLeavesOfType(VIEW_TYPE_THINGS_TOOLKIT_REVIEW)[0] as ReviewLeaf | undefined; if (!leaf) { - leaf = workspace.getRightLeaf(false); + leaf = workspace.getRightLeaf(false) as ReviewLeaf | null; if (!leaf) { return; } @@ -133,43 +160,46 @@ export default class ThingsToolkitPlugin extends Plugin { async tryToSyncLogbook(): Promise { if (this.options.hasAcceptedDisclaimer) { await this.syncLogbook(); - } else { - new ConfirmationModal(this.app, { - cta: "Sync", - onAccept: async () => { - await this.writeOptions({ hasAcceptedDisclaimer: true }); - await this.syncLogbook(); - }, - text: - "Enabling sync will repair the configured review window from Things3 into Obsidian. This can create or modify many daily notes. Make sure to test the plugin in a test vault before continuing.", - title: "Sync Now?", - }).open(); + return; } + + new ConfirmationModal(this.app, { + cta: "Sync", + onAccept: async () => { + await this.writeOptions({ hasAcceptedDisclaimer: true }); + await this.syncLogbook(); + }, + text: + "Enabling sync will repair the configured review window from Things3 into Obsidian. This can create or modify many daily notes. Make sure to test the plugin in a test vault before continuing.", + title: "Sync Now?", + }).open(); } async tryToScheduleSync(): Promise { if (this.options.hasAcceptedDisclaimer) { this.scheduleNextSync(); - } else { - new ConfirmationModal(this.app, { - cta: "Sync", - onAccept: async () => { - await this.writeOptions({ hasAcceptedDisclaimer: true }); - this.scheduleNextSync(); - }, - onCancel: async () => { - await this.writeOptions({ isSyncEnabled: false }); - // update the settings tab display - this.settingsTab.display(); - }, - text: - "Enabling sync will repair the configured review window from Things3 into Obsidian. This can create or modify many daily notes. Make sure to test the plugin in a test vault before continuing.", - title: "Sync Now?", - }).open(); + return; } + + new ConfirmationModal(this.app, { + cta: "Sync", + onAccept: async () => { + await this.writeOptions({ hasAcceptedDisclaimer: true }); + this.scheduleNextSync(); + }, + onCancel: () => { + void this.writeOptions({ isSyncEnabled: false }).then(() => { + this.settingsTab?.display(); + }); + }, + text: + "Enabling sync will repair the configured review window from Things3 into Obsidian. This can create or modify many daily notes. Make sure to test the plugin in a test vault before continuing.", + title: "Sync Now?", + }).open(); } async syncLogbook(): Promise { + const moment = getMoment(); if (this.syncStatus.isSyncing) { new Notice("Things Toolkit sync already running"); return; @@ -204,31 +234,34 @@ export default class ThingsToolkitPlugin extends Plugin { const daysToTasks: Record = groupBy( tasks.filter((task) => task.stopDate), - (task) => window.moment.unix(task.stopDate).startOf("day").format() + (task) => moment.unix(task.stopDate).startOf("day").format() ); const dayEntries = Object.entries(daysToTasks).sort(([a], [b]) => - window.moment(a).diff(window.moment(b)) + moment(a).diff(moment(b)) ); dayCount = dayEntries.length; const dailyStats = this.buildDailyStats( dayEntries, fetchResult.source, - window.moment().unix() + moment().unix() ); - for (const [dateStr, tasks] of dayEntries) { - const date = window.moment(dateStr); - + for (const [dateStr, groupedTasks] of dayEntries) { + const date = moment(dateStr); let dailyNote = getDailyNote(date, dailyNotes); if (!dailyNote) { dailyNote = await createDailyNote(date); } + if (!isTFile(dailyNote)) { + continue; + } + const didChange = await updateSection( this.app, dailyNote, this.options.sectionHeading, - logbookRenderer.render(tasks) + logbookRenderer.render(groupedTasks) ); if (didChange) { changedDayCount++; @@ -236,7 +269,7 @@ export default class ThingsToolkitPlugin extends Plugin { } await this.writeOptions({ - latestSyncTime: window.moment().unix(), + latestSyncTime: moment().unix(), dailyStats, thingsAccessStatus: fetchResult.accessStatus, }); @@ -274,9 +307,10 @@ export default class ThingsToolkitPlugin extends Plugin { source: IDailyLogbookStat["source"], syncedAt: number ): Record { + const moment = getMoment(); const dailyStats = { ...(this.options.dailyStats || {}) }; dayEntries.forEach(([dateStr, tasks]) => { - const dateKey = window.moment(dateStr).format("YYYY-MM-DD"); + const dateKey = moment(dateStr).format("YYYY-MM-DD"); dailyStats[dateKey] = { taskCount: tasks.length, source, @@ -291,23 +325,21 @@ export default class ThingsToolkitPlugin extends Plugin { } getReviewWindowDayCount(): number { - return Math.max( - 7, - Math.floor(Number(this.options.reviewWindowDays) || 365) - ); + return Math.max(7, Math.floor(Number(this.options.reviewWindowDays) || 365)); } async refreshRecentDailyStats(dayCount = this.getReviewWindowDayCount()): Promise { + const moment = getMoment(); const dailyStats = { ...(this.options.dailyStats || {}) }; const dailyNotes = getAllDailyNotes(); - const syncedAt = window.moment().unix(); - const end = window.moment().startOf("day"); + const syncedAt = moment().unix(); + const end = moment().startOf("day"); const start = end.clone().subtract(dayCount - 1, "days"); - for (let date = start; date.isSameOrBefore(end); date.add(1, "day")) { - const dateKey = date.format("YYYY-MM-DD"); + for (let date = start.clone(); date.isSameOrBefore(end); date.add(1, "day")) { + const dateKey = getDateKey(date); const dailyNote = getDailyNote(date, dailyNotes); - if (!dailyNote) { + if (!isTFile(dailyNote)) { dailyStats[dateKey] = { taskCount: 0, source: "daily-note", @@ -318,10 +350,7 @@ export default class ThingsToolkitPlugin extends Plugin { const fileContents = await this.app.vault.read(dailyNote); dailyStats[dateKey] = { - taskCount: countThingsTasksInSection( - fileContents, - this.options.sectionHeading - ), + taskCount: countThingsTasksInSection(fileContents, this.options.sectionHeading), source: "daily-note", syncedAt, }; @@ -331,11 +360,12 @@ export default class ThingsToolkitPlugin extends Plugin { } getCurrentCompletionStreak(): number { + const moment = getMoment(); let streak = 0; for ( - let date = window.moment().startOf("day"); - this.getTaskCountForDay(date.format("YYYY-MM-DD")) > 0; - date = date.subtract(1, "day") + let date = moment().startOf("day"); + this.getTaskCountForDay(getDateKey(date)) > 0; + date = date.clone().subtract(1, "day") ) { streak++; } @@ -346,11 +376,12 @@ export default class ThingsToolkitPlugin extends Plugin { dateKey: string, diff: Partial ): Promise { + const moment = getMoment(); const dailyReviews = { ...(this.options.dailyReviews || {}) }; const nextReview = { ...(dailyReviews[dateKey] || {}), ...diff, - updatedAt: window.moment().unix(), + updatedAt: moment().unix(), }; if (!nextReview.rating && !nextReview.reflection) { delete dailyReviews[dateKey]; @@ -362,17 +393,21 @@ export default class ThingsToolkitPlugin extends Plugin { } async openDailyNote(dateKey: string): Promise { - const date = window.moment(dateKey, "YYYY-MM-DD"); + const moment = getMoment(); + const date = moment(dateKey, "YYYY-MM-DD"); const dailyNotes = getAllDailyNotes(); let dailyNote = getDailyNote(date, dailyNotes); if (!dailyNote) { dailyNote = await createDailyNote(date); } - await this.app.workspace.getLeaf(false).openFile(dailyNote as TFile); + if (!isTFile(dailyNote)) { + throw new Error("Daily note could not be opened as a file"); + } + await (getWorkspace(this).getLeaf(false) as ReviewLeaf).openFile(dailyNote); } refreshReviewViews(): void { - this.app.workspace + getWorkspace(this) .getLeavesOfType(VIEW_TYPE_THINGS_TOOLKIT_REVIEW) .forEach((leaf) => { if (leaf.view instanceof ThingsToolkitReviewView) { @@ -382,18 +417,16 @@ export default class ThingsToolkitPlugin extends Plugin { } updateStatusBar(): void { + const moment = getMoment(); if (!this.statusBarEl) { return; } - const todayKey = window.moment().format("YYYY-MM-DD"); + const todayKey = moment().format("YYYY-MM-DD"); const todayCount = this.getTaskCountForDay(todayKey); const streak = this.getCurrentCompletionStreak(); this.statusBarEl.setText(`Things: ${todayCount} today | ${streak}d streak`); - this.statusBarEl.setAttribute( - "aria-label", - "Open Things toolkit review" - ); + this.statusBarEl.setAttribute("aria-label", "Open Things toolkit review"); } cancelScheduledSync(): void { @@ -403,7 +436,8 @@ export default class ThingsToolkitPlugin extends Plugin { } scheduleNextSync(): void { - const now = window.moment().unix(); + const moment = getMoment(); + const now = moment().unix(); this.cancelScheduledSync(); if (!this.options.isSyncEnabled || !this.options.syncInterval) { @@ -416,31 +450,29 @@ export default class ThingsToolkitPlugin extends Plugin { const nextSync = Math.max(secondsUntilNextSync * 1000, 20); console.debug(`[Things Toolkit] next sync scheduled in ${nextSync}ms`); - this.syncTimeoutId = window.setTimeout(this.syncLogbook, nextSync); + this.syncTimeoutId = window.setTimeout(() => { + void this.syncLogbook(); + }, nextSync); } async loadOptions(): Promise { - this.options = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); + this.options = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()) as ISettings; if (!this.options.hasAcceptedDisclaimer) { - // In case the user quits before accepting sync modal, - // this keep the settings in sync this.options.isSyncEnabled = false; } } async writeOptions(diff: Partial): Promise { - this.options = Object.assign(this.options, diff); + this.options = Object.assign({}, this.options, diff); - // Sync toggled on/off if (diff.isSyncEnabled !== undefined) { if (diff.isSyncEnabled) { - this.tryToScheduleSync(); + await this.tryToScheduleSync(); } else { this.cancelScheduledSync(); } } else if (diff.syncInterval !== undefined && this.options.isSyncEnabled) { - // reschedule if interval changed - this.tryToScheduleSync(); + await this.tryToScheduleSync(); } await this.saveData(this.options); diff --git a/src/modal.ts b/src/modal.ts index b9f05e6..41a9c81 100644 --- a/src/modal.ts +++ b/src/modal.ts @@ -2,15 +2,15 @@ import { App, Modal } from "obsidian"; interface IConfirmationDialogParams { cta: string; - onAccept: (...args: unknown[]) => Promise; - onCancel?: (...args: unknown[]) => void; + onAccept: () => Promise; + onCancel?: () => void; text: string; title: string; } export class ConfirmationModal extends Modal { private config: IConfirmationDialogParams; - private accepted: boolean; + private accepted = false; private buttonContainerEl: HTMLElement; constructor(app: App, config: IConfirmationDialogParams) { @@ -19,28 +19,30 @@ export class ConfirmationModal extends Modal { const { cta, onAccept, text, title } = config; - this.containerEl.addClass('mod-confirmation'); + this.containerEl.addClass("mod-confirmation"); this.titleEl.setText(title); this.contentEl.setText(text); - this.buttonContainerEl = this.modalEl.createDiv('modal-button-container'); + this.buttonContainerEl = this.modalEl.createDiv("modal-button-container"); const acceptBtnEl = this.buttonContainerEl.createEl("button", { cls: "mod-cta", text: cta, }); - acceptBtnEl.addEventListener("click", async (e) => { - e.preventDefault(); + acceptBtnEl.addEventListener("click", (event: MouseEvent) => { + event.preventDefault(); this.accepted = true; this.close(); - onAccept(e); + void onAccept(); }); - const cancelBtnEl = this.buttonContainerEl.createEl("button", { text: "Never mind" }); - cancelBtnEl.addEventListener("click", e => { - e.preventDefault(); - this.close(); - }); + const cancelBtnEl = this.buttonContainerEl.createEl("button", { + text: "Never mind", + }); + cancelBtnEl.addEventListener("click", (event: MouseEvent) => { + event.preventDefault(); + this.close(); + }); } onClose(): void { diff --git a/src/moment.ts b/src/moment.ts new file mode 100644 index 0000000..76c6760 --- /dev/null +++ b/src/moment.ts @@ -0,0 +1,35 @@ +export interface MomentLike { + unix(): number; + startOf(unit: string): MomentLike; + endOf(unit: string): MomentLike; + format(pattern?: string): string; + diff(input: string | MomentLike, unit?: string): number; + clone(): MomentLike; + subtract(amount: number, unit: string): MomentLike; + add(amount: number, unit: string): MomentLike; + isSameOrBefore(input: MomentLike, unit?: string): boolean; + isBefore(input: MomentLike, unit?: string): boolean; + isAfter(input: MomentLike, unit?: string): boolean; + isSame(input: MomentLike, unit?: string): boolean; + isoWeek(): number; + year(): number; + month(): number; + date(): number; + fromNow(): string; +} + +export interface MomentFactory { + (): MomentLike; + (input: string, format?: string, strict?: boolean): MomentLike; + unix(timestamp: number): MomentLike; +} + +declare global { + interface Window { + moment: MomentFactory; + } +} + +export function getMoment(): MomentFactory { + return window.moment; +} diff --git a/src/renderer.ts b/src/renderer.ts index ef91b26..12da884 100644 --- a/src/renderer.ts +++ b/src/renderer.ts @@ -3,6 +3,15 @@ import { ISettings } from "./settings"; import { ISubTask, ITask } from "./things"; import { getHeadingLevel, getTab, groupBy, toHeading } from "./textUtils"; +interface VaultConfigReader { + getConfig(key: "useTab"): boolean; + getConfig(key: "tabSize"): number; +} + +function getVaultConfig(app: App): VaultConfigReader { + return app.vault as unknown as VaultConfigReader; +} + export class ToolkitRenderer { private app: App; private settings: ISettings; @@ -14,31 +23,33 @@ export class ToolkitRenderer { } renderTask(task: ITask): string { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const vault = this.app.vault as any; + const vault = getVaultConfig(this.app); const tab = getTab(vault.getConfig("useTab"), vault.getConfig("tabSize")); const prefix = this.settings.tagPrefix; - const tags = Array.from(new Set(task.tags - .filter((tag) => !!tag) - .map((tag) => tag.replace(/\s+/g, "-").toLowerCase()) - .sort() - )) + const tags = Array.from( + new Set( + task.tags + .filter((tag) => !!tag) + .map((tag) => tag.replace(/\s+/g, "-").toLowerCase()) + .sort() + ) + ) .map((tag) => `#${prefix}${tag}`) .join(" "); - const taskTitle = `[${task.title}](things:///show?id=${task.uuid}) ${tags}`.trimEnd() + const taskTitle = `[${task.title}](things:///show?id=${task.uuid}) ${tags}`.trimEnd(); const notes = this.settings.doesSyncNoteBody ? String(task.notes || "") - .trimEnd() - .split("\n") - .filter((line) => !!line) - .map((noteLine) => `${tab}${noteLine}`) - : "" + .trimEnd() + .split("\n") + .filter((line) => !!line) + .map((noteLine) => `${tab}${noteLine}`) + : []; return [ - `- [${task.cancelled ? this.settings.canceledMark : 'x'}] ${taskTitle}`, + `- [${task.cancelled ? this.settings.canceledMark : "x"}] ${taskTitle}`, ...notes, ...task.subtasks.map( (subtask: ISubTask) => @@ -51,15 +62,20 @@ export class ToolkitRenderer { public render(tasks: ITask[]): string { const { sectionHeading, doesSyncProject, doesAddNewlineBeforeHeadings } = this.settings; - const headings = groupBy(tasks, (task) => task.area || (doesSyncProject ? task.project : "") || ""); - const headingLevel = getHeadingLevel(sectionHeading); + const headings = groupBy( + tasks, + (task) => task.area || (doesSyncProject ? task.project : "") || "" + ); + const headingLevel = getHeadingLevel(sectionHeading) ?? 2; const output = [sectionHeading]; - Object.entries(headings).map(([heading, tasks]) => { + Object.entries(headings).forEach(([heading, groupedTasks]) => { if (heading !== "") { - output.push(toHeading(heading, headingLevel + 1, doesAddNewlineBeforeHeadings)); + output.push( + toHeading(heading, headingLevel + 1, doesAddNewlineBeforeHeadings) + ); } - output.push(...tasks.map(this.renderTask)); + output.push(...groupedTasks.map(this.renderTask)); }); return output.join("\n"); diff --git a/src/sqlite.ts b/src/sqlite.ts index 7ecbb79..2396ee4 100644 --- a/src/sqlite.ts +++ b/src/sqlite.ts @@ -6,7 +6,7 @@ export const TASK_FETCH_LIMIT = 1000; interface ISpawnResults { stdOut: Buffer[]; stdErr: Buffer[]; - code: number; + code: number | null; } function parseCSV(csv: Buffer[]): T[] { @@ -47,10 +47,10 @@ async function handleSqliteQuery( }); spawned.on("error", (err: Error) => { - stdErr.push(Buffer.from(String(err.stack), "ascii")); + stdErr.push(Buffer.from(err.stack ?? err.message, "utf-8")); }); - spawned.on("close", (code: number) => finish({ stdErr, stdOut, code })); - spawned.on("exit", (code: number) => finish({ stdErr, stdOut, code })); + spawned.on("close", (code: number | null) => finish({ stdErr, stdOut, code })); + spawned.on("exit", (code: number | null) => finish({ stdErr, stdOut, code })); }); } @@ -58,10 +58,10 @@ export async function querySqliteDB( dbPath: string, query: string ): Promise { - const { stdOut, stdErr } = await handleSqliteQuery(dbPath, query); - if (stdErr.length) { - const error = Buffer.concat(stdErr).toString("utf-8"); - return Promise.reject(error); + const { stdOut, stdErr, code } = await handleSqliteQuery(dbPath, query); + if (stdErr.length || code !== 0) { + const error = Buffer.concat(stdErr).toString("utf-8") || `sqlite3 exited with code ${String(code)}`; + throw new Error(error); } return parseCSV(stdOut); } diff --git a/src/textUtils.ts b/src/textUtils.ts index 2a44c19..285d2dd 100644 --- a/src/textUtils.ts +++ b/src/textUtils.ts @@ -1,4 +1,5 @@ import type { App, TFile } from "obsidian"; +import { Platform } from "obsidian"; import { getEditorForFile } from "./fileUtils"; export function getHeadingLevel(line = ""): number | null { @@ -32,7 +33,7 @@ export function groupBy( } export function isMacOS(): boolean { - return navigator.appVersion.indexOf("Mac") !== -1; + return Platform.isMacOS; } export async function updateSection( @@ -67,9 +68,6 @@ export async function updateSection( const editor = getEditorForFile(app, file); if (editor) { - // if the "## Logbook" header exists, we just replace the - // section. If it doesn't, we need to append it to the end - // if the file and add `\n` for separation. if (logbookSectionLineNum !== -1) { const currentSection = fileLines .slice( @@ -89,16 +87,14 @@ export async function updateSection( : { line: fileLines.length, ch: 0 }; editor.replaceRange(`${sectionContents}\n`, from, to); return true; - } else { - const pos = { line: fileLines.length, ch: 0 }; - editor.replaceRange(`\n\n${sectionContents}`, pos, pos); - return true; } + + const pos = { line: fileLines.length, ch: 0 }; + editor.replaceRange(`\n\n${sectionContents}`, pos, pos); + return true; } - // Editor is not open, modify the file on disk... if (logbookSectionLineNum !== -1) { - // Section already exists, just replace const prefix = fileLines.slice(0, logbookSectionLineNum); const suffix = nextSectionLineNum !== -1 ? fileLines.slice(nextSectionLineNum) : []; @@ -114,19 +110,12 @@ export async function updateSection( return false; } - await vault.process( - file, - () => [...prefix, sectionContents, ...suffix].join("\n") - ); - return true; - } else { - // Section does not exist, append to end of file. - await vault.process( - file, - () => [...fileLines, "", sectionContents].join("\n") - ); + await vault.process(file, () => [...prefix, sectionContents, ...suffix].join("\n")); return true; } + + await vault.process(file, () => [...fileLines, "", sectionContents].join("\n")); + return true; } export function getSectionContents(