UNPKG

todo-txt-cli

Version:

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

194 lines (172 loc) 4.23 kB
import { homedir } from 'node:os' import * as path from 'node:path' import fsp from 'node:fs/promises' import { execFileSync } from 'node:child_process' import { defaultColors, getColorNameModifier } from '#colors.js' import { logger } from '#logger.js' import { AppConfig as AConfig, BooleanSchema, StringSchema, v } from '@commenthol/app-config' /** * @typedef {import('#colors.js').ColorConfig} ColorConfig */ const APP = 'todo-txt-cli' const CONF_FILE = '.todo-txt.json' const log = logger('config') export const defaults = { // default editors for different platforms editor: { darwin: ['open'], // opens default TextEdit with *.txt files linux: ['editor'], win32: ['notepad.exe'] }, // default todo-dir todoDir: homedir() } // --- editor /** * @returns {string[]} */ export const getEditor = () => defaults.editor[process.platform] || [''] /** * @param {string} cmd * @param {...string} args */ export const launchEditor = (cmd, ...args) => { if (!cmd) { const arr = [...getEditor(), ...args] // @ts-expect-error cmd = arr.shift() args = arr } if (!cmd) { throw new Error('No editor found') } try { execFileSync(cmd, args, { stdio: 'inherit' }) } catch (/** @type {Error|any} */ e) { log.debug('Error: %s', e.message) throw new Error(`Could not launch editor "${cmd}"`) } } // -- const ColorSchema = v.pipe( v.string(), v.custom((color) => { // @ts-expect-error const { name, modifiers } = getColorNameModifier(color) return !!(name || modifiers.length) }) ) const schema = { // editor command editor: StringSchema, // directory containing todo.txt, can only be set in global config todoDir: StringSchema, // disables colors when set to 'false' useColor: BooleanSchema, // set default limit limit: v.pipe( v.string(), v.transform(Number), v.integer(), v.minValue(1), v.maxValue(1000) ), // color names color.priA, color.priB, ..., color.error ...Object.keys(defaultColors).reduce((acc, color) => { acc[`color.${color}`] = ColorSchema return acc }, {}) } export class AppConfig extends AConfig { constructor(filename = CONF_FILE) { super({ appName: APP, filename, schema }) } static getFilename(options) { const { todoDir } = options || {} return todoDir ? path.resolve(todoDir, CONF_FILE) : undefined } /** * @param {{ * todoDir?: string * }} [options] * @returns {Promise<AppConfig>} */ static async load(options) { const { todoDir } = options || {} const glbl = new AppConfig(CONF_FILE) if (todoDir) { const filename = path.resolve(todoDir, CONF_FILE) const inst = new AppConfig(filename) // try to read local and global config const [{ status }] = await Promise.allSettled([ fsp.stat(filename), inst.read(), glbl.read() ]) if (status === 'rejected') { log.debug(glbl.config) return glbl } inst.config = { ...glbl.config, ...inst.config } log.debug(inst.config) return inst } await glbl.read() return glbl } /** * @param {string} key * @param {string} val * @param {boolean} [isGlobal] */ set(key, val, isGlobal) { if (!isGlobal && key === 'todoDir') { return } return super.set(key, val) } /** * get colors from config * @returns {ColorConfig} */ getColorConfig() { const useColor = toBoolean(this.get('useColor')) const colors = { useColor } for (const key of Object.keys(defaultColors)) { colors[key] = this.get(['color', key]) ?? defaultColors[key] } return colors } /** * @returns {string[] | undefined} */ getEditor() { const editor = this.get('editor') if (editor) { return editor.split(/\s+/) } } getTimestamp() { return toBoolean(this.get('timestamp')) } getTodoDir() { return this.get('todoDir') || defaults.todoDir } } /** * @param {any} val * @returns {boolean} */ const toBoolean = (val = '') => { switch (typeof val) { case 'boolean': return val default: return !['false', 'off', '0'].includes(val.toLowerCase()) } }