Add support for profiles + excluding directories from lookup for tasks

This commit is contained in:
2025-03-08 12:46:22 +01:00
parent 677e68a374
commit 17fe6b86de
8 changed files with 63 additions and 29 deletions

View File

@@ -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<C extends BackendSettings> {
@@ -16,7 +16,7 @@ export abstract class Backend<C extends BackendSettings> {
return config as C;
}
public async remind(config: Config, tasks: Task[]): Promise<void> {
public async remind(config: ProfileConfig, tasks: Task[]): Promise<void> {
const cfg = config?.backend?.[this.name] as Partial<C> | undefined;
if (cfg?.enable !== true) {

View File

@@ -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;
}

View File

@@ -7,11 +7,12 @@ import { notify, scan, test } from "../runner";
const getOptions = () => program
.name("obsidian-tasks-reminder")
.version("0.0.1")
.requiredOption("-c, --config <file>", "sets the path to the YAML file with configuration")
.requiredOption("-c, --config <file>", "sets the path to the YAML file with configuration")
.option("-x, --set <arg>", "overrides the config option for this specific run (arg: <key>=<name>, 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 <name>", "limits the current operation only to specified profile. If missing, all profiles will be affected")
.parse()
.opts<CLIOptions>();
@@ -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;
}
}

View File

@@ -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<Task[]> {
export async function loadTasks(directories: string[], query?: string, exclude?: string): Promise<Task[]> {
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<Task, boolean>(query, ctx);
const tasks = await Promise.all(directories.map(readTasksFromDirectory));
const excludeFn = exclude ? jsMapper<string, boolean>(exclude, {}) : () => false;
const filter = query && jsMapper<Task, boolean>(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<Task[]> {
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<T>(directory: string, visitor: (path: string) => Promise<T[]>): Promise<T[]> {
async function walk<T>(directory: string, visitor: (path: string) => Promise<T[]>, excludeFn: (path: string) => boolean): Promise<T[]> {
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);
}
}
}

View File

@@ -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<void>) {
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));
};
}

View File

@@ -1,5 +1,6 @@
export type CLIOptions = {
config: string;
profile?: string;
test: boolean;
scan: boolean;
notify: boolean;

View File

@@ -1,6 +1,12 @@
export type Config = {
profiles: Record<string, ProfileConfig>;
};
export type ProfileConfig = {
enable: boolean;
sources: string[];
query?: string;
exclude?: string;
defaultTime?: string;
databaseFile: string;
backend: Record<string, unknown>;

View File

@@ -1,4 +1,11 @@
import dayjs from "dayjs";
const standardContext = {
dayjs
};
export function jsMapper<I, O>(code: string, context: Record<string, unknown>): (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));
}