todo-txt-cli
Version:
A CLI for todo.txt files - http://todotxt.org/
194 lines (172 loc) • 4.23 kB
JavaScript
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())
}
}