diff --git a/src/backend/base.ts b/src/backend/base.ts index 62bb029..31f6eaa 100644 --- a/src/backend/base.ts +++ b/src/backend/base.ts @@ -1,4 +1,4 @@ -import { BackendSettings, Config } from "../types/config"; +import { BackendSettings, ProfileConfig } from "../types/config"; import { Task } from "../types/task"; export abstract class Backend { @@ -16,7 +16,7 @@ export abstract class Backend { return config as C; } - public async remind(config: Config, tasks: Task[]): Promise { + public async remind(config: ProfileConfig, tasks: Task[]): Promise { const cfg = config?.backend?.[this.name] as Partial | undefined; if (cfg?.enable !== true) { diff --git a/src/backend/index.ts b/src/backend/index.ts index 17aa8ab..9683523 100644 --- a/src/backend/index.ts +++ b/src/backend/index.ts @@ -1,6 +1,6 @@ import dayjs from "dayjs"; import { Task, TaskDatabase } from "../types/task"; -import { Config } from "../types/config"; +import { ProfileConfig } from "../types/config"; import { NtfySH } from "./ntfy"; import { Debug } from "./debug"; @@ -13,7 +13,7 @@ const backends = [ * Iterates through all the database notifications for current time * and triggers the notification using specified backends in the config. */ -export async function remind(config: Config, db: TaskDatabase) { +export async function remind(config: ProfileConfig, db: TaskDatabase) { const now = dayjs().format("HH:mm"); await run(config, db, db[now]); @@ -23,7 +23,7 @@ export async function remind(config: Config, db: TaskDatabase) { } } -async function run(config: Config, db: TaskDatabase, tasks?: Task[]) { +async function run(config: ProfileConfig, db: TaskDatabase, tasks?: Task[]) { if(!tasks) { return; } diff --git a/src/cli/index.ts b/src/cli/index.ts index faeed85..bbf122d 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -7,11 +7,12 @@ import { notify, scan, test } from "../runner"; const getOptions = () => program .name("obsidian-tasks-reminder") .version("0.0.1") - .requiredOption("-c, --config ", "sets the path to the YAML file with configuration") + .requiredOption("-c, --config ", "sets the path to the YAML file with configuration") .option("-x, --set ", "overrides the config option for this specific run (arg: =, i.e. backend.ntfy.enable=false", (v: string, prev: string[]) => prev.concat([v]), []) .option("-t, --test", "evaluates the query, applies the mapper and prints to stdout the notifications about to be trigger, without actual triggering them") .option("-s, --scan", "scans new tasks for future notifications and generates the database") - .option("-n, --notify", "reads the generated database and triggers notifications if any") + .option("-n, --notify", "reads the generated database and triggers notifications if any") + .option("-p, --profile ", "limits the current operation only to specified profile. If missing, all profiles will be affected") .parse() .opts(); @@ -39,17 +40,17 @@ const getOptions = () => program } if (options.test) { - await test(config); + await test(config, options.profile); return; } if (options.scan) { - await scan(config); + await scan(config, options.profile); return; } if (options.notify) { - await notify(config); + await notify(config, options.profile); return; } } diff --git a/src/loader/index.ts b/src/loader/index.ts index a69d10b..f033b71 100644 --- a/src/loader/index.ts +++ b/src/loader/index.ts @@ -10,7 +10,7 @@ import { jsMapper } from "../util/code"; /** * Returns all tasks from specified directory and filters them with optional query. */ -export async function loadTasks(directories: string[], query?: string): Promise { +export async function loadTasks(directories: string[], query?: string, exclude?: string): Promise { const ctx = { now: dayjs(), LOWEST: TaskPriority.LOWEST, @@ -21,8 +21,9 @@ export async function loadTasks(directories: string[], query?: string): Promise< HIGHEST: TaskPriority.HIGHEST }; - const filter = query && jsMapper(query, ctx); - const tasks = await Promise.all(directories.map(readTasksFromDirectory)); + const excludeFn = exclude ? jsMapper(exclude, {}) : () => false; + const filter = query && jsMapper(query, ctx); + const tasks = await Promise.all(directories.map(readTasksFromDirectory(excludeFn))); return tasks.flat().filter(t => filter ? filter(t) : true); } @@ -30,26 +31,28 @@ export async function loadTasks(directories: string[], query?: string): Promise< /** * Read all files in specific directory and returns all tasks from those files. */ -async function readTasksFromDirectory(directory: string): Promise { - return walk(directory, readTasksFromFile); +function readTasksFromDirectory(excludeFn: (path: string) => boolean) { + return async (directory: string) => walk(directory, readTasksFromFile, excludeFn); } /** * Walks through a specific directory recursively and invokes visitor on each file. * Returns a flat list of items returned by visitors. */ -async function walk(directory: string, visitor: (path: string) => Promise): Promise { +async function walk(directory: string, visitor: (path: string) => Promise, excludeFn: (path: string) => boolean): Promise { const list = []; for(const file of fs.readdirSync(directory)) { const path = `${directory}/${file}`; if (fs.statSync(path).isDirectory()) { - const items = await walk(path, visitor); + const items = await walk(path, visitor, excludeFn); list.push(...items); } else if (path.endsWith("md") || (path.endsWith("MD"))) { - const items = await visitor(path); - list.push(...items); + if (!excludeFn(path)) { + const items = await visitor(path); + list.push(...items); + } } } diff --git a/src/runner/index.ts b/src/runner/index.ts index abfe757..4a46716 100644 --- a/src/runner/index.ts +++ b/src/runner/index.ts @@ -2,10 +2,10 @@ import { loadTasks } from "../loader"; import { createDatabase, dumpDatabase } from "../database/serializer"; import { loadDatabase } from "../database/deserializer"; import { remind } from "../backend"; -import { Config } from "../types/config"; +import { Config, ProfileConfig } from "../types/config"; -export async function test(config: Config) { - const tasks = await loadTasks(config.sources, config.query); +export const test = handleProfile(async config => { + const tasks = await loadTasks(config.sources, config.query, config.exclude); const db = createDatabase(tasks); for (const time of Object.keys(db)) { @@ -17,14 +17,30 @@ export async function test(config: Config) { console.log(); } -} +}); -export async function scan(config: Config) { - const tasks = await loadTasks(config.sources, config.query); +export const scan = handleProfile(async config => { + const tasks = await loadTasks(config.sources, config.query, config.exclude); dumpDatabase(config.databaseFile, tasks); -} +}); -export async function notify(config: Config) { +export const notify = handleProfile(async config => { const db = loadDatabase(config.databaseFile); remind(config, db); +}); + +function handleProfile(handler: (profile: ProfileConfig) => Promise) { + return async (config: Config, profile?: string) => { + if (profile !== undefined) { + const cfg = config.profiles[profile]; + + if (cfg === undefined) { + throw new Error(`Undefined profile: ${profile}`); + } + + return handler(cfg); + } + + return await Promise.all(Object.values(config.profiles).map(handler)); + }; } \ No newline at end of file diff --git a/src/types/cli.ts b/src/types/cli.ts index 72f74c3..aef3e47 100644 --- a/src/types/cli.ts +++ b/src/types/cli.ts @@ -1,5 +1,6 @@ export type CLIOptions = { config: string; + profile?: string; test: boolean; scan: boolean; notify: boolean; diff --git a/src/types/config.ts b/src/types/config.ts index 83b91c7..1044585 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -1,6 +1,12 @@ export type Config = { + profiles: Record; +}; + +export type ProfileConfig = { + enable: boolean; sources: string[]; query?: string; + exclude?: string; defaultTime?: string; databaseFile: string; backend: Record; diff --git a/src/util/code.ts b/src/util/code.ts index 1d80745..7edf72c 100644 --- a/src/util/code.ts +++ b/src/util/code.ts @@ -1,4 +1,11 @@ +import dayjs from "dayjs"; + +const standardContext = { + dayjs +}; + export function jsMapper(code: string, context: Record): (task: I) => O { - const filter = new Function('$', ...Object.keys(context), code); - return (task: I) => filter(task, ...Object.values(context)); + const ctx = { ...standardContext, ...context }; + const filter = new Function('$', ...Object.keys(ctx), code); + return (task: I) => filter(task, ...Object.values(ctx)); } \ No newline at end of file