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"; import { Task } from "../types/task";
export abstract class Backend<C extends BackendSettings> { export abstract class Backend<C extends BackendSettings> {
@@ -16,7 +16,7 @@ export abstract class Backend<C extends BackendSettings> {
return config as C; 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; const cfg = config?.backend?.[this.name] as Partial<C> | undefined;
if (cfg?.enable !== true) { if (cfg?.enable !== true) {

View File

@@ -1,6 +1,6 @@
import dayjs from "dayjs"; import dayjs from "dayjs";
import { Task, TaskDatabase } from "../types/task"; import { Task, TaskDatabase } from "../types/task";
import { Config } from "../types/config"; import { ProfileConfig } from "../types/config";
import { NtfySH } from "./ntfy"; import { NtfySH } from "./ntfy";
import { Debug } from "./debug"; import { Debug } from "./debug";
@@ -13,7 +13,7 @@ const backends = [
* Iterates through all the database notifications for current time * Iterates through all the database notifications for current time
* and triggers the notification using specified backends in the config. * 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"); const now = dayjs().format("HH:mm");
await run(config, db, db[now]); 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) { if(!tasks) {
return; return;
} }

View File

@@ -12,6 +12,7 @@ const getOptions = () => program
.option("-t, --test", "evaluates the query, applies the mapper and prints to stdout the notifications about to be trigger, without actual triggering them") .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("-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() .parse()
.opts<CLIOptions>(); .opts<CLIOptions>();
@@ -39,17 +40,17 @@ const getOptions = () => program
} }
if (options.test) { if (options.test) {
await test(config); await test(config, options.profile);
return; return;
} }
if (options.scan) { if (options.scan) {
await scan(config); await scan(config, options.profile);
return; return;
} }
if (options.notify) { if (options.notify) {
await notify(config); await notify(config, options.profile);
return; return;
} }
} }

View File

@@ -10,7 +10,7 @@ import { jsMapper } from "../util/code";
/** /**
* Returns all tasks from specified directory and filters them with optional query. * 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 = { const ctx = {
now: dayjs(), now: dayjs(),
LOWEST: TaskPriority.LOWEST, LOWEST: TaskPriority.LOWEST,
@@ -21,8 +21,9 @@ export async function loadTasks(directories: string[], query?: string): Promise<
HIGHEST: TaskPriority.HIGHEST HIGHEST: TaskPriority.HIGHEST
}; };
const excludeFn = exclude ? jsMapper<string, boolean>(exclude, {}) : () => false;
const filter = query && jsMapper<Task, boolean>(query, ctx); const filter = query && jsMapper<Task, boolean>(query, ctx);
const tasks = await Promise.all(directories.map(readTasksFromDirectory)); const tasks = await Promise.all(directories.map(readTasksFromDirectory(excludeFn)));
return tasks.flat().filter(t => filter ? filter(t) : true); return tasks.flat().filter(t => filter ? filter(t) : true);
} }
@@ -30,28 +31,30 @@ export async function loadTasks(directories: string[], query?: string): Promise<
/** /**
* Read all files in specific directory and returns all tasks from those files. * Read all files in specific directory and returns all tasks from those files.
*/ */
async function readTasksFromDirectory(directory: string): Promise<Task[]> { function readTasksFromDirectory(excludeFn: (path: string) => boolean) {
return walk(directory, readTasksFromFile); return async (directory: string) => walk(directory, readTasksFromFile, excludeFn);
} }
/** /**
* Walks through a specific directory recursively and invokes visitor on each file. * Walks through a specific directory recursively and invokes visitor on each file.
* Returns a flat list of items returned by visitors. * 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 = []; const list = [];
for(const file of fs.readdirSync(directory)) { for(const file of fs.readdirSync(directory)) {
const path = `${directory}/${file}`; const path = `${directory}/${file}`;
if (fs.statSync(path).isDirectory()) { if (fs.statSync(path).isDirectory()) {
const items = await walk(path, visitor); const items = await walk(path, visitor, excludeFn);
list.push(...items); list.push(...items);
} else if (path.endsWith("md") || (path.endsWith("MD"))) { } else if (path.endsWith("md") || (path.endsWith("MD"))) {
if (!excludeFn(path)) {
const items = await visitor(path); const items = await visitor(path);
list.push(...items); list.push(...items);
} }
} }
}
return list; return list;
} }

View File

@@ -2,10 +2,10 @@ import { loadTasks } from "../loader";
import { createDatabase, dumpDatabase } from "../database/serializer"; import { createDatabase, dumpDatabase } from "../database/serializer";
import { loadDatabase } from "../database/deserializer"; import { loadDatabase } from "../database/deserializer";
import { remind } from "../backend"; import { remind } from "../backend";
import { Config } from "../types/config"; import { Config, ProfileConfig } from "../types/config";
export async function test(config: Config) { export const test = handleProfile(async config => {
const tasks = await loadTasks(config.sources, config.query); const tasks = await loadTasks(config.sources, config.query, config.exclude);
const db = createDatabase(tasks); const db = createDatabase(tasks);
for (const time of Object.keys(db)) { for (const time of Object.keys(db)) {
@@ -17,14 +17,30 @@ export async function test(config: Config) {
console.log(); console.log();
} }
} });
export async function scan(config: Config) { export const scan = handleProfile(async config => {
const tasks = await loadTasks(config.sources, config.query); const tasks = await loadTasks(config.sources, config.query, config.exclude);
dumpDatabase(config.databaseFile, tasks); dumpDatabase(config.databaseFile, tasks);
} });
export async function notify(config: Config) { export const notify = handleProfile(async config => {
const db = loadDatabase(config.databaseFile); const db = loadDatabase(config.databaseFile);
remind(config, db); 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 = { export type CLIOptions = {
config: string; config: string;
profile?: string;
test: boolean; test: boolean;
scan: boolean; scan: boolean;
notify: boolean; notify: boolean;

View File

@@ -1,6 +1,12 @@
export type Config = { export type Config = {
profiles: Record<string, ProfileConfig>;
};
export type ProfileConfig = {
enable: boolean;
sources: string[]; sources: string[];
query?: string; query?: string;
exclude?: string;
defaultTime?: string; defaultTime?: string;
databaseFile: string; databaseFile: string;
backend: Record<string, unknown>; 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 { export function jsMapper<I, O>(code: string, context: Record<string, unknown>): (task: I) => O {
const filter = new Function('$', ...Object.keys(context), code); const ctx = { ...standardContext, ...context };
return (task: I) => filter(task, ...Object.values(context)); const filter = new Function('$', ...Object.keys(ctx), code);
return (task: I) => filter(task, ...Object.values(ctx));
} }