Skip to content

Commit

Permalink
more compact rendering of hours and checked (was: done) tags, CLI sho…
Browse files Browse the repository at this point in the history
…rtcuts for filtering by checked state
  • Loading branch information
jacoscaz committed Nov 12, 2024
1 parent 6217526 commit 751bec3
Show file tree
Hide file tree
Showing 11 changed files with 227 additions and 94 deletions.
42 changes: 35 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
# taskparser

A CLI tool to parse tasks and worklogs out of Markdown documents and print
them to standard output, either in tabular of CSV format. Supports sorting,
filtering and tag-based metadata.
them to standard output in the format of a Markdown table, CSV or JSON.
Supports sorting and filtering based on metadata encoded in tags, inline and/or
as YAML frontmatter.

## Introduction

Expand Down Expand Up @@ -52,7 +53,7 @@ npm i -g taskparser

```
$ taskparser -h
usage: taskparser [-h] [-t TAGS] [-f FILTER] [-s SORT] [-w] [-l] [-o {table,csv,json}] [-c COLUMNS] [-v] path
usage: taskparser [-h] [-t TAGS] [-f FILTER] [-s SORT] [-w] [-l] [-C] [-U] [-o {table,csv,json}] [-c COLUMNS] [-v] path
A CLI tool to parse, sort and filter tasks and worklogs out of Markdown documents and print them to standard output, either in tabular of CSV format.
Expand All @@ -67,6 +68,8 @@ optional arguments:
-s SORT, --sort SORT sorting expression such as: foo(asc)
-w, --watch enable watch mode
-l, --worklogs enable worklogs mode
-C, --checked only show checked tasks
-U, --unchecked only show unchecked tasks
-o {table,csv,json}, --out {table,csv,json}
set output format
-c COLUMNS, --columns COLUMNS
Expand All @@ -76,8 +79,8 @@ optional arguments:

## Tags

`taskparser` uses the concept of _tags_ as the unit of information that is used
to describe both tasks and workflows.
`taskparser` uses the concept of _tag_ as the unit of information that
describes tasks and worklogs.

### Choosing which tags to show

Expand All @@ -89,18 +92,22 @@ $ taskparser -t text,project,client,file,date /foo/bar

### Autogenerated tags

`taskparser` auto-generates the following tags:
When parsing tasks, `taskparser` auto-generates the following tags:

| tag | description | internal |
| --- | --- | --- |
| `text` | the textual content of the task (first line only) | yes |
| `file` | the file that contains the task | yes |
| `date` | the date of creation of the task | no |
| `done` | whether the task has been marked as done | yes |
| `checked` | whether the task is checked as done | yes |

Auto-genereated tags considered _internal_ cannot be overridden via YAML
frontmatter or inline tags.

When rendering to a Markdown table (i.e. the default output format `table`),
the `checked` tag is shortened to `c` and rendered with a tick mark for
compactness.

### Inline tags

Tasks may be tagged inline:
Expand Down Expand Up @@ -204,6 +211,24 @@ Filtering expressions can be combined:
foo(=bar),foo(!=baz)
```

### Filtering by checked state

While filtering tasks by their checked state requires comparing against
`"true"` and `"false"`, the `-U, --unchecked` and `-C, --checked` CLI
arguments make for easy-to-remember shortcuts:

```
$ taskparser -f "checked(=false),project(=baz)" /foo/bar
$ taskparser -f "checked(=true),project(=baz)" /foo/bar
```

are equivalent to, respectively:

```
$ taskparser -U -f "project(=baz)" /foo/bar
$ taskparser -C -f "project(=baz)" /foo/bar
```

### Sorting by tag

`taskparser` accepts sorting expressions via the `-s` argument:
Expand Down Expand Up @@ -250,6 +275,9 @@ The `-l` or `--worklogs` flag may be used to enable worklog mode:
taskparser -l -t text,hours,file,date"
```

When rendering to a Markdown table (i.e. the default output format), the
`hours` tag is shortened to `h` for compactness.

## License

Released under the LGPL v3.0 (`LGPL-3.0-only`) license.
Expand Down
100 changes: 80 additions & 20 deletions src/bin.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env node

import type { Item, RenderOpts } from './types.js';
import type { Item, RenderItemsFn, RenderOpts } from './types.js';

import { fileURLToPath } from 'node:url';
import { cwd } from 'node:process';
Expand All @@ -24,6 +24,10 @@ import {
const pkg_path = resolve(fileURLToPath(import.meta.url), '..', '..', 'package.json');
const pkg_version = JSON.parse(readFileSync(pkg_path, 'utf8')).version;

// ============================================================================
// CLI ARGS
// ============================================================================

const arg_parser = new ArgumentParser({
description: 'A CLI tool to parse, sort and filter tasks and worklogs out of Markdown documents and print them to standard output, either in tabular of CSV format.',
});
Expand Down Expand Up @@ -55,6 +59,18 @@ arg_parser.add_argument('-l', '--worklogs', {
help: 'enable worklogs mode',
});

arg_parser.add_argument('-C', '--checked', {
required: false,
action: 'store_true',
help: 'only show checked tasks',
});

arg_parser.add_argument('-U', '--unchecked', {
required: false,
action: 'store_true',
help: 'only show unchecked tasks',
});

arg_parser.add_argument('-o', '--out', {
required: false,
default: 'tabular',
Expand Down Expand Up @@ -82,40 +98,85 @@ const cli_args = arg_parser.parse_args();

const folder_path = resolve(cwd(), cli_args.path);

const sorter = cli_args.sort ? compileTagSortExpressions(parseTagSortExpressions(cli_args.sort)) : null;
const filter = cli_args.filter ? compileTagFilterExpressions(parseTagFilterExpressions(cli_args.filter)) : null;
// ============================================================================
// FILTERING
// ============================================================================

// While parsing filtering expressions we need to consider CLI shortcuts, which
// we can concatenate with the optional explicit filtering expression.

const filter_exprs = [];

if (cli_args.checked) {
// Shortcut for showing checked items only
filter_exprs.push('checked(=true)');
}

if (cli_args.unchecked) {
// Shortcut for showing unchecked items only
filter_exprs.push('checked(=false)');
}

if (cli_args.filter) {
// Explicit filtering expression
filter_exprs.push(cli_args.filter);
}

const filter_expr = filter_exprs.join(',');

const filter_fn = filter_expr ? compileTagFilterExpressions(parseTagFilterExpressions(filter_expr)) : null;

// ============================================================================
// SORTING
// ============================================================================

const sort_expr = cli_args.sort;

const sorting_fn = sort_expr ? compileTagSortExpressions(parseTagSortExpressions(sort_expr)) : null;

// ============================================================================
// TAGS TO SHOW
// ============================================================================

const show_tags = cli_args.tags
? cli_args.tags.split(',')
: cli_args.worklogs
? ['text', 'hours', 'file', 'date']
: ['text', 'done', 'file', 'date'];
: ['text', 'checked', 'file', 'date'];

// ============================================================================
// RENDERING OPTIONS and FN
// ============================================================================

const render_opts: RenderOpts = {
terminal_width: parseInt(cli_args.columns),
};

const render_fn = ({
json: renderJSON,
csv: renderCSV,
table: renderTabular,
} satisfies Record<string, RenderItemsFn>)[cli_args.out as string] ?? renderTabular;

// ============================================================================
// RENDERING HELPER
// ============================================================================

const renderItems = (items: Set<Item>) => {
let as_arr = Array.from(items);
if (filter) {
as_arr = as_arr.filter(filter);
if (filter_fn) {
as_arr = as_arr.filter(filter_fn);
}
if (sorter) {
as_arr.sort(sorter);
}
switch (cli_args.out) {
case 'json':
renderJSON(as_arr, show_tags, render_opts);
break;
case 'csv':
renderCSV(as_arr, show_tags, render_opts);
break;
case 'table':
default:
renderTabular(as_arr, show_tags, render_opts);
if (sorting_fn) {
as_arr.sort(sorting_fn);
}
render_fn(as_arr, show_tags, render_opts);
};

// ============================================================================
// 3.. 2.. 1.. LET'S GO!
// ============================================================================

if (cli_args.watch) {
const { stdout } = process;
if (!stdout.isTTY) {
Expand All @@ -129,4 +190,3 @@ if (cli_args.watch) {
const { tasks, worklogs } = await parseFolder(folder_path);
renderItems(cli_args.worklogs ? worklogs : tasks);
}

7 changes: 3 additions & 4 deletions src/parse.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@

import type { Parent, Node, Yaml, ListItem, Text } from 'mdast';
import type { TagMap, Task, Worklog, ParseContext, ParseFileContext } from './types.js';
import type { TagMap, Task, Worklog, ParseContext, ParseFileContext, InternalTagMap } from './types.js';

import { readdir, readFile, watch, stat } from 'node:fs/promises';
import { resolve, relative } from 'node:path';
Expand Down Expand Up @@ -56,10 +56,10 @@ const parseTextNode = (node: Text, ctx: ParseFileContext, curr_task: Task | null
const parseListItemNode = (node: ListItem, ctx: ParseFileContext, curr_task: Task | null, curr_wlog: Worklog | null) => {
if (!curr_task && typeof node.checked === 'boolean') {
const tags: TagMap = { ...ctx.tags };
const internal_tags: TagMap = {
const internal_tags: InternalTagMap = {
...ctx.internal_tags,
line: String(node.position!.start.line),
done: String(node.checked),
checked: String(node.checked),
};
const task: Task = { tags, internal_tags, file: ctx.file, worklogs: [] };
parseParentNode(node as Parent, ctx, task, curr_wlog);
Expand All @@ -70,7 +70,6 @@ const parseListItemNode = (node: ListItem, ctx: ParseFileContext, curr_task: Tas
const internal_tags: TagMap = {
...ctx.internal_tags,
line: String(node.position!.start.line),
done: String(node.checked),
};
const worklog: Worklog = { tags, internal_tags, file: ctx.file, task: curr_task };
parseParentNode(node as Parent, ctx, curr_task, worklog);
Expand Down
Loading

0 comments on commit 751bec3

Please sign in to comment.