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";
|
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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,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.
|
* 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"))) {
|
||||||
const items = await visitor(path);
|
if (!excludeFn(path)) {
|
||||||
list.push(...items);
|
const items = await visitor(path);
|
||||||
|
list.push(...items);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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));
|
||||||
|
};
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user