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/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..bd98ba5 100644 --- a/src/textUtils.ts +++ b/src/textUtils.ts @@ -32,7 +32,7 @@ export function groupBy( } export function isMacOS(): boolean { - return navigator.appVersion.indexOf("Mac") !== -1; + return navigator.userAgent.includes("Mac"); } export async function updateSection( @@ -67,9 +67,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 +86,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 +109,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(