todo-txt-cli
Version:
A CLI for todo.txt files - http://todotxt.org/
438 lines (393 loc) • 12.9 kB
JavaScript
const BOLD = 'bold'
const UNDERLINE = 'underline'
/**
* @typedef {object} Token
* @property {string} text
* @property {'bold'|'underline'} [mod] modifier
*/
/**
* @param {string} [c]
* @returns {boolean}
*/
const isSpace = (c = '') => /^[\s.,:.?!(){}[\]'"`]?$/.test(c)
/**
* @param {string} txt
* @returns {Token[]|[]}
*/
export const tokenize = (txt) => {
let text = ''
let mod = ''
const tokens = []
const switchModifier = (_mod_, prev, char, next) => {
if (!mod && isSpace(prev)) {
if (text) tokens.push({ text })
mod = _mod_
text = ''
} else if (isSpace(next)) {
tokens.push({ text, mod })
mod = ''
text = ''
} else {
text += char
}
}
let prev = ''
for (let i = 0; i < txt.length; i++) {
const char = txt[i]
const next = txt[i + 1]
switch (char) {
case '\\':
text += next
i++
break
case '_':
switchModifier(UNDERLINE, prev, char, next)
break
case '*':
switchModifier(BOLD, prev, char, next)
break
default:
text += char
}
prev = char
}
if (text) {
tokens.push({ text })
}
return tokens
}
/**
* @param {number} num
* @returns {string}
*/
const spaces = (num = 0) => new Array(num).fill(' ').join('')
/**
*
* @param {string} line
* @param {{
* range?: number
* indent?: number
* firstIndent?: number
* }} param1
* @returns
*/
export const lineBreak = (
line,
{ range = 80, indent = 8, firstIndent } = {}
) => {
const words = line.split(/(\s+)/)
let first = true
let cnt = 0
let out = ''
for (let i = 0; i < words.length; i++) {
const word = words[i]
const len = word.length
if (cnt + len > range) {
// NOTE: ensure that word.length < range - indent
cnt = 0
out += '\n'
i--
continue
} else if (cnt === 0) {
const isSpace = /^\s+$/.test(word)
const _indent = first ? (firstIndent ?? indent) : indent
first = false
out += spaces(_indent)
cnt += _indent
if (isSpace) continue
}
out += word
cnt += len
}
return out
}
const buildOptions = (options) =>
options.map((option) => help.optionFlags[option])
/**
* @param {string} [action]
* @returns {string}
*/
export const buildHelp = (action) => {
if (action === 'help') {
// build full help
return [
help.header(),
help.optionsHeader,
...buildOptions(Object.keys(help.optionFlags)),
help.actionHeader,
...Object.values(help.actions)
].join('')
} else if (!help.actions[action]) {
action = help.alt[action] || 'help'
}
// build small help
return [
help.header(action),
help.optionsHeader,
...buildOptions(help.options[action]),
help.actionHeader,
help.actions[action]
].join('')
}
/**
* @param {import('#colors.js').Colors} colors
* @param {string} action
*/
export const renderHelp = (colors, action) => {
const raw = buildHelp(action)
const tokens = tokenize(raw)
let out = ''
for (const { text, mod } of tokens) {
out += mod && colors[mod] ? colors[mod](text) : text
}
return out
}
// ----
export const help = {
options: {},
actions: {},
alt: {}
}
help.header = (action = '[_action_]') => `
*SYNOPSIS*
*todo* [_options_] ${action}
`
help.optionsHeader = `
*OPTIONS*
`
help.optionFlags = {
'-@': ' *-@, --no-contexts* Hide context names in list output.\n',
'-+': ' *-+, --no-projects* Hide project names in list output.\n',
'-c': ' *-c, --color* Turn on color mode.\n',
'-C': ' *-C, --no-color* Turn off color mode.\n',
'-d':
' *-d, --dir* [_directory_] Use "todo.txt" in _directory_.\n' +
' -d without arg uses the current directory\n',
'-h': ' *-h, --help* Displays this help.\n',
'-l':
' *-l, --limit* [_limit_] Limit output to *limit* lines.\n' +
' -l without arg uses 20 lines as default.\n',
'-r': ' *-r, --reverse* Reverse the order of list output.\n',
'-s':
' *-s, --sort* _sorter_ Sort by "due" (due-date), "pri" (priorities),\n' +
' "prj" (projects) or "con" (contexts).\n',
'-t': ' *-t, --timestamp* List tasks with createdAt timestamp.\n',
'-T': ' *-T, --no-timestamp* Do not add timestamps.\n',
'-v': ' *-v, --version* Display version info.\n'
}
const optsAlways = ['-c', '-C', '-d']
help.actionHeader = `
*ACTIONS*
`
help.options.add = [...optsAlways, '-t', '-T']
help.actions.add = `
*add* "_task-description_"
*a* "_task-description_"
Adds a task with _task-description_ on its own line.
add "thing i need to do +project @context"
a "thing i need to do +project @context"
`
help.options.append = optsAlways
help.actions.append = `
*append* _line#_ _text-to-append_
*app* _line#_ _text-to-append_
Add _text-to-append_ to the end of the task on line _line#_. Quotes are
optional.
`
help.options.archive = optsAlways
help.actions.archive = `
*archive*
Moves all done tasks from "todo.txt" to "done.txt" and removes blank
lines.
`
help.options.del = optsAlways
help.alt.rm = 'del'
help.actions.del = `
*del* _line#_ [_term_]
*rm* _line#_ [_term_]
Deletes the task on line _line#_ in "todo.txt". If _term_ is specified,
deletes only _term_ from the task.
`
help.options.depri = optsAlways
help.alt.dp = 'depri'
help.actions.depri = `
*depri* _line#_ [_line#_ ...]
*dp* _line#_ [_line#_ ...]
De-prioritize (removes the priority) from the task(s) on line(s)
_line#_ in "todo.txt".
`
help.options.done = optsAlways
help.alt.d = 'done'
help.actions.done = `
*done* _line#_ [_line#_ ...]
*d* _line#_ [_line#_ ...]
Marks task(s) on line(s) _line#_ as done in "todo.txt".
`
help.options.due = optsAlways
help.actions.due = `
*due* _line#_ [_line#_ ...] _iso-date_ | _relative-date_ | del
Add a due date on task in _line#_ 10 and 12 using _iso-date_ 2025-02-01:
todo due 10 12 2025-10-02
If a due date is already present, it is moved to that date.
Alternatively use "MM-DD" or "DD.MM". Here the current year is used.
todo due 10 12 10-02
todo due 10 12 2.10
To apply a _relative-date_ of 3 months minus 1 week on task _line#_ 9
starting with today, use:
todo due 9 3months-1week
If a due date is present, also today is used as start date.
All combinations of year, month, week, day (short form: y, m, w, d) are
possible. You can also use a short syntax:
todo due 9 3m-1w
To delete a due date on task _line#_ 9 and 12 use:
todo due 9 12 del
`
help.options.edit = ['-d', '-h', '-v']
help.actions.edit = `
*edit* [_editor_]
Open "todo.txt" in _editor_. Default is TextEdit on macOS,
NotePad on windows, $EDITOR on linux.
`
const optsList = [...optsAlways, '-@', '-+', '-l', '-r', '-t']
help.options.list = optsList
help.alt.ls = 'list'
help.actions.list = `
*list* [_term_ ...]
*ls* [_term_ ...]
Displays all open tasks that contain _term_(s) sorted by priority with
line numbers. Each task must match all _term_(s) (logical AND);
to display tasks that contain any _term_ (logical OR), use
*'TERM1|TERM2'* (with quotes), or "TERM1\\|TERM2" (unquoted).
Hides all tasks that contain TERM(s) preceded by a minus sign
(i.e. "-TERM"). _term_(s) are grep-style basic regular expressions;
for literal matching, put a single backslash before any [ ] \\ $ \\* . ^
and enclose the entire _term_ in single quotes, or use double
backslashes and extra shell-quoting.
If no _term_ specified, lists entire "todo.txt".
ls "TERM1|TERM2"
ls TERM1\\|TERM2
ls -TERM1
`
help.options.listall = optsList
help.alt.lsa = 'listall'
help.actions.listall = `
*listall* [_term_ ...]
*lsa* [_term_ ...]
Displays all open tasks (also done but not archived) that contain
_term_(s) sorted by priority with line numbers.
`
help.options.listcon = optsList
help.alt.lst = 'listcon'
help.actions.listcon = `
*listcon* [_term_ ...]
*lsc* [_term_ ...]
Lists all the task contexts that start with the @ sign in "todo.txt".
If _term_ specified, considers only tasks that contain _term_(s).
`
help.options.listpri = optsList
help.alt.lsp = 'listpri'
help.actions.listpri = `
*listpri* [_priorities_] \[_term_ ...]
*lsp* [_priorities_] \[_term_ ...]
Displays all tasks prioritized _priorities_.
_priorities_ can be a single one "A" or a range "A-C".
If no _priorities_ specified, lists all prioritized tasks.
If _term_ specified, lists only prioritized tasks that contain _term_(s).
Hides all tasks that contain _term_(s) preceded by a minus sign
(i.e. "-TERM").
`
help.options.listproj = optsList
help.alt.lsprj = 'listproj'
help.actions.listproj = `
*listproj* [_term_ ...]
*lsprj* [_term_ ...]
Lists all the projects (terms that start with a + sign) in "todo.txt".
If _term_ specified, considers only tasks that contain _term_(s).
`
help.options.prepend = optsAlways
help.alt.pre = 'prepend'
help.actions.prepend = `
*prepend* _line#_ _text-to-prepend_
*pre* _line#_ _text-to-prepend_
Adds _text-to-prepend_ to the beginning of the task on line _line#_.
Quotes are optional.
`
help.options.pri = optsAlways
help.alt.p = 'pri'
help.actions.pri = `
*pri* _line#_ _priority_
*p* _line#_ _priority_
Adds _priority_ to task on line _line#_. If the task is already
prioritized, replaces current priority with new _priority_.
_priority_ must be a letter between A and Z.
`
help.options.overdue = optsAlways
help.actions.overdue = `
*overdue* [_iso-date_ | _relative-date_ | del]
Moves due-date on all overdue tasks to today (no parameter) to a date
using _iso-date_ or _relative-date_ (start date is today).
When using "del" all due-dates are removed from all overdue tasks.
`
help.options.replace = optsAlways
help.actions.replace = `
*replace* _line#_ _text-to-replace_
Replaces task on line _line#_ with _text-to-replace_.
`
help.options.set = optsAlways
help.actions.set = `
*set*
Display current configuration.
*set* _key_ _value_
Set _key_ _value_ in todo config. Possible keys
- editor "_args_" set default editor arguments
- todoDir _pathname_ set default dir in "global" config only
- useColor true|false use color mode
- limit _limit_ set default limit for -l
- color.priA _color_ set priority A color
- color.priB _color_ set priority B color
- color.priC _color_ set priority C color
- color.priD _color_ set priority D color
- color.project _color_ set project color
- color.context _color_ set context color
- color.date _color_ set date color
- color.done _color_ set done color
- color.error _color_ set error color
Possible colors are:
black, red, green, yellow, blue, magenta, cyan, white, gray, bgBlack,
bgRed, bgGreen, bgYellow, bgBlue, bgMagenta, bgCyan, bgWhite,
blackBright, redBright, greenBright, yellowBright, blueBright,
magentaBright, cyanBright, whiteBright, bgBlackBright, bgRedBright,
bgGreenBright, bgYellowBright, bgBlueBright, bgMagentaBright,
bgCyanBright, bgWhiteBright,
Possible modifiers include: bold, light, underline.
E.g. for "underlined light green" use "lightGreenUnderline"
for "bold light bgRed" use "lightBgRedBold"
`
help.options.sort = optsAlways
help.actions.sort = `
*sort* [_sorter_]
Sorts file with _sorter_, default is "due":
- "due": sort by due-date, priority, projects and contexts
- "pri": sort by priority, due-date
- "prj": sort by projects, priority, due-date
- "con": sort by context, priority, due-date
`
help.options.undo = optsAlways
help.actions.undo = `
*undo* _line#_ [_line#_ ...]
Undo done task(s) on line _line#_. Must not have been archived into
"done.txt".
`
help.options.version = [...optsAlways, '-v']
help.actions.version = `
*version*
Display version info.
`
// must be last item here!
help.options.help = [...optsAlways, '-h']
help.actions.help = `
*help* [_action_]
Display help about options and actions.
Possible _action_s are:
${lineBreak([...Object.keys(help.actions), ...Object.keys(help.alt)].sort().join(', '))}
`