Add support for profiles + excluding directories from lookup for tasks
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
export type CLIOptions = {
|
||||
config: string;
|
||||
profile?: string;
|
||||
test: boolean;
|
||||
scan: boolean;
|
||||
notify: boolean;
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
Reference in New Issue
Block a user