UNPKG

todo-txt-cli

Version:

A CLI for todo.txt files - http://todotxt.org/

417 lines (385 loc) 8.78 kB
import os from 'os' import { Task } from '#task.js' import { sorter, sorting } from '#sorter.js' import { logger } from '#logger.js' import { today, escapeRegExp, priorityToRegExp, isoOrRelativeDate, formatDate, range } from '#utils.js' /** * @typedef {import('#task.js').StringifyOptions} StringifyOptions * @typedef {{limit?: number}} TaskLimit * @typedef {StringifyOptions & TaskLimit} TaskSortOrder */ const LF = os.platform() === 'win32' ? '\r\n' : '\n' const log = logger('tasks') export class Tasks { _index = 0 /** @type {Task[]} */ tasks = [] /** * @param {string} [content] */ constructor(content = '') { if (content) { this.add(content) } } getId() { return this._idFn() } /** * @private */ _idFn() { return '' + ++this._index } push(task) { this.tasks.push(task) return this } get length() { return this.tasks.length } /** * add one or many tasks to tasks list * @param {string} content * @param {{timestamp?: boolean}} [options] * @returns {Tasks|undefined} */ add(content = '', options) { const { timestamp = false } = options || {} const added = new Tasks() const lines = content.split(/[\r\n]/) for (const line of lines) { if (line.trim() === '') { continue } const task = Task.parse(line, this.getId()) if (timestamp) task.createdAt = today() this.tasks.push(task) added.push(task) } return added } /** * deletes task with id. If `term` is given then only term is removed from the * task * @param {string} id * @param {string} [term] * @returns {Task|undefined} */ remove(id, term) { const { task, index } = this.findIndex(id) if (!task) { return } if (term) { if (!task) return task.update(task.raw.replace(term, '')) } else { this.tasks.splice(index, 1) } return task } /** * append `term` to task with `id` * @param {string} id * @param {string} term * @returns {Task|undefined} */ append(id, term = '') { const { task } = this.findIndex(id) if (!task || !term) { return } task.update(`${task.raw} ${term.trim()}`) return task } /** * prepend `term` to task with `id` * @param {string} id * @param {string} term * @returns {Task|undefined} */ prepend(id, term) { const { task } = this.findIndex(id) if (!task || !term) { return } task.update(`${term.trim()} ${task.raw}`) return task } replace(id, content) { const { task } = this.findIndex(id) if (!task || !content) { return } task.update(content) return task } /** * @param {string[]} ids * @returns {Tasks} */ done(ids) { const tasks = new Tasks('') const _ids = range(ids) for (const id of _ids) { const { task } = this.findIndex(id) if (!task) { continue } task.complete() tasks.push(task) } return tasks } /** * @param {string[]} ids * @param {string} dateStr iso-date YYYY-MM-DD or relative-date +1month2weeks3days (1m2w3d) * @returns {Tasks|undefined} */ due(ids, dateStr) { const date = isoOrRelativeDate(dateStr) if (!date && dateStr !== 'del') { return } const tasks = new Tasks('') const _ids = range(ids) for (const id of _ids) { const { task } = this.findIndex(id) if (!task) { continue } task.due(date) tasks.push(task) } return tasks } /** * correct overdue tasks * @param {string} dateStr iso-date YYYY-MM-DD or relative-date +1month2weeks3days (1m2w3d) * @returns {Tasks|undefined} */ overdue(dateStr = formatDate(today())) { const date = isoOrRelativeDate(dateStr) if (!date && dateStr !== 'del') { return } const now = today() const tasks = new Tasks('') for (const task of this.tasks) { if (task.isCompleted || !task.dueAt || task.dueAt > now) continue task.due(date) tasks.push(task) } return tasks } /** * @param {string[]} ids * @param {string} [priority] * @return {Tasks} */ prioritize(ids, priority) { const tasks = new Tasks() if (!priority) { return tasks } const _ids = range(ids) for (const id of _ids) { const { task } = this.findIndex(id) if (!task) { continue } task.prioritize(priority) tasks.push(task) } return tasks } /** * @param {string[]} ids * @return {Tasks} */ dePrioritize(ids) { const tasks = new Tasks() const _ids = range(ids) for (const id of _ids) { const { task } = this.findIndex(id) if (!task) { continue } task.prioritize() tasks.push(task) } return tasks } /** * archive completed tasks * @return {Tasks} */ archive() { const active = [] const completed = new Tasks('') for (const task of this.tasks) { if (task.isCompleted) { completed.push(task) } else { active.push(task) } } this.tasks = active return completed } /** * list (filter) tasks according to terms * @param {(string|RegExp)[]} [terms] * @param {{ * priorities?: string * onlyProjects?: boolean * onlyContexts?: boolean * showCompleted?: boolean * }} [options] * @return {Tasks} */ list(terms = [], options) { const { priorities = '', onlyProjects = false, onlyContexts = false, showCompleted = false } = options || {} const incRes = [] const excRes = [] for (let term of terms) { let exclude = false if (typeof term === 'string' && term.startsWith('-')) { exclude = true term = term.slice(1) } const re = term instanceof RegExp ? term : new RegExp(escapeRegExp(term)) if (exclude) { excRes.push(re) } else { incRes.push(re) } } if (!incRes.length) { incRes.push(new RegExp('.')) } const filtered = new Tasks() const prioRe = priorityToRegExp(priorities) // inappropriate priorities filter -> early exit if (priorities && !prioRe) { return filtered } for (const task of this.tasks) { let search = '' if (onlyProjects) { search = [...task.projects].join(' ') } else if (onlyContexts) { search = [...task.contexts].join(' ') } else { search = task.raw } if (!showCompleted && task.isCompleted) { continue } if (prioRe && !prioRe.test(task.priority || '')) { continue } let exclude = false for (const exc of excRes) { if (exc.test(search)) { exclude = true break } } if (exclude) continue for (const inc of incRes) { if (!inc.test(search)) { exclude = true break } } if (!exclude) filtered.push(task) } return filtered } /** * @param {string} id * @returns {{index: number, task?: Task}} */ findIndex(id) { const index = this.tasks.findIndex((task) => task.id === id) return { index, task: index > -1 ? this.tasks[index] : undefined } } /** * sort task list by sort order * @param {string[]} [sortOrder] * @returns {this} */ sortBy(sortOrder) { sortOrder = sortOrder?.length ? sortOrder : sorting.due log.debug('sortOrder %j', sortOrder) this.tasks.sort(sorter(sortOrder)) return this } /** * sort task projects and contexts * @param {TaskSortOrder} taskSortOrder * @returns {this} */ sortTaskContent(taskSortOrder) { for (const task of this.tasks) { task.raw = task.stringify(taskSortOrder) } return this } /** * reverse task list sort order * @returns {this} */ reverse() { this.tasks.reverse() return this } /** * @param {TaskSortOrder & {reverse?: boolean}} [options] * @returns {string} */ stringify(options) { const { reverse, limit, ...opts } = options || {} const list = [] let i = 0 for (const task of this.tasks) { list.push(task.stringify(opts)) i++ if (limit && i >= limit) { break } } return (reverse ? list.reverse() : list).join(LF) } /** * @param {string[]} ids * @returns {Tasks} */ undo(ids) { const tasks = new Tasks('') const _ids = range(ids) for (const id of _ids) { const { task } = this.findIndex(id) if (!task?.isCompleted) { continue } task.complete(false) tasks.push(task) } return tasks } }