feat: improve help config, update styles, fixes

This commit is contained in:
2023-11-24 01:47:58 +02:00
committed by Chen Asraf
parent 451dd5676d
commit c397ec9fd7
5 changed files with 115 additions and 43 deletions

View File

@@ -31,13 +31,13 @@ export const StringStyle = z.object({
bold: z.boolean().optional(),
underline: z.boolean().optional(),
color: zodEnumFromObjKeys(ansiColors).optional(),
reset: z.boolean().optional(),
reset: z.boolean().default(true).optional(),
})
export type StringStyle = z.infer<typeof StringStyle>
export function format(string: string, style: StringStyle = {}): string {
const { color, bold, underline, reset } = style
const { color, bold, underline, reset = true } = style
const colorCode = color ? ansiColors[color] : ''
const boldCode = bold ? ansiStyles.bold : ''
const underlineCode = underline ? ansiStyles.underline : ''
@@ -46,5 +46,5 @@ export function format(string: string, style: StringStyle = {}): string {
}
export function stripColors(string: string): string {
return string.replace(/\x1b\[\d+m/g, '')
return string.replace(/\x1b\[\d+m/gi, '')
}

View File

@@ -7,7 +7,7 @@ import MassargOption, {
TypedOptionConfig,
MassargHelpFlag,
} from './option'
import { setOrPush, deepMerge } from './utils'
import { DeepRequired, setOrPush, deepMerge } from './utils'
import MassargExample, { ExampleConfig } from './example'
export const CommandConfig = <RunArgs extends z.ZodType>(args: RunArgs) =>
@@ -48,15 +48,24 @@ export default class MassargCommand<Args extends ArgsObject = ArgsObject> {
options: MassargOption[] = []
examples: MassargExample[] = []
args: Partial<Args> = {}
helpConfig: Required<HelpConfig>
private _helpConfig: HelpConfig
parent?: MassargCommand<any>
constructor(options: CommandConfig<Args>) {
constructor(options: CommandConfig<Args>, parent?: MassargCommand<any>) {
CommandConfig(z.any()).parse(options)
this.name = options.name
this.description = options.description
this.aliases = options.aliases ?? []
this._run = options.run
this.helpConfig = HelpConfig.required().parse(defaultHelpConfig)
this._helpConfig = HelpConfig.required().parse(defaultHelpConfig)
this.parent = parent
}
get helpConfig(): DeepRequired<HelpConfig> {
if (this.parent) {
return deepMerge(this.parent.helpConfig, this._helpConfig)
}
return deepMerge(defaultHelpConfig, this._helpConfig) as Required<HelpConfig>
}
command<A extends ArgsObject = Args>(config: CommandConfig<A>): MassargCommand<Args>
@@ -74,6 +83,7 @@ export default class MassargCommand<Args extends ArgsObject = ArgsObject> {
path: [this.name, command.name],
})
}
command.parent = this
this.commands.push(command)
return this
} catch (e) {
@@ -161,9 +171,7 @@ export default class MassargCommand<Args extends ArgsObject = ArgsObject> {
}
help(config: HelpConfig): MassargCommand<Args> {
this.helpConfig = HelpConfig.required().parse(
deepMerge(defaultHelpConfig, config) as HelpConfig,
)
this._helpConfig = HelpConfig.parse(config)
if (this.helpConfig.bindCommand) {
this.command(new MassargHelpCommand())

View File

@@ -1,7 +1,7 @@
import z from 'zod'
import { format, StringStyle, stripColors } from './color'
import MassargCommand from './command'
import { chainStr, indent } from './utils'
import { DeepRequired, strConcat, indent } from './utils'
export const GenerateTableCommandConfig = z.object({
maxRowLength: z.number().optional(),
@@ -44,11 +44,14 @@ export const HelpConfig = z.object({
output: StringStyle.optional(),
})
.optional(),
usageText: z.string().optional(),
headerText: z.string().optional(),
footerText: z.string().optional(),
})
export type HelpConfig = z.infer<typeof HelpConfig>
export const defaultHelpConfig: HelpConfig = {
export const defaultHelpConfig: DeepRequired<HelpConfig> = {
maxRowLength: 80,
commandOptions: {
namePrefix: '',
@@ -97,6 +100,9 @@ export const defaultHelpConfig: HelpConfig = {
color: 'brightWhite',
underline: true,
},
headerText: '',
footerText: '',
usageText: '',
}
export type HelpItem = {
@@ -107,15 +113,18 @@ export type HelpItem = {
export class HelpGenerator {
entry: MassargCommand<any>
config: HelpConfig
config: DeepRequired<HelpConfig>
constructor(entry: MassargCommand<any>, config?: HelpConfig) {
this.entry = entry
this.config = HelpConfig.parse({
this.config = HelpConfig.required().parse({
...entry.helpConfig,
commandOptions: {
...entry.helpConfig?.commandOptions,
...config?.commandOptions,
},
optionOptions: {
...entry.helpConfig?.optionOptions,
...config?.optionOptions,
},
})
@@ -123,40 +132,53 @@ export class HelpGenerator {
generate(): string {
const entry = this.entry
const options = generateHelpTable(entry.options, this.config.optionOptions)
const commands = generateHelpTable(entry.commands, this.config.commandOptions)
const CMD_OPT_INDENT = 4
const _wrap = (text: string, indent = 0) => wrap(text, this.config.maxRowLength - indent)
const options = generateHelpTable(entry.options, {
...this.config.optionOptions,
maxRowLength:
(this.config.optionOptions.maxRowLength ?? this.config.maxRowLength) - CMD_OPT_INDENT,
})
const commands = generateHelpTable(entry.commands, {
...this.config.commandOptions,
maxRowLength:
(this.config.commandOptions.maxRowLength ?? this.config.maxRowLength) - CMD_OPT_INDENT,
})
const examples = entry.examples
.map((example) => {
const { description, input, output } = example
return chainStr(
description && [format(description, this.config.exampleStyles?.description), ''],
input && format(input, this.config.exampleStyles?.input),
output && format(output, this.config.exampleStyles?.output),
return strConcat(
description && [_wrap(format(description, this.config.exampleStyles.description), 4), ''],
input && _wrap(format('$ ' + input, this.config.exampleStyles.input), 4),
output && _wrap(format('> ' + output, this.config.exampleStyles.output), 4),
)
})
.join('\n')
const { headerText, footerText, usageText } = this.config
return (
chainStr(
format(`Usage: ${entry.name} [...options]`, this.config.usageStyle),
strConcat(
_wrap(format(usageText || `Usage: ${entry.name} [...options]`, this.config.usageStyle)),
headerText.length && ['', format(headerText, this.config.descriptionStyle)],
'',
format(entry.description, this.config.descriptionStyle),
_wrap(format(entry.description, this.config.descriptionStyle)),
commands.length &&
indent([
'',
format(`Commands for ${entry.name}:`, this.config.subtitleStyle),
'',
indent(commands),
]),
indent([
'',
format(`Commands for ${entry.name}:`, this.config.subtitleStyle),
'',
indent(commands),
]),
options.length &&
indent([
'',
format(`Options for ${entry.name}:`, this.config.subtitleStyle),
'',
indent(options),
]),
indent([
'',
format(`Options for ${entry.name}:`, this.config.subtitleStyle),
'',
indent(options),
]),
examples.length &&
indent(['', format('Examples:', this.config.subtitleStyle), '', indent(examples)]),
indent(['', format('Examples:', this.config.subtitleStyle), '', indent(examples)]),
footerText.length && ['', _wrap(format(footerText, this.config.descriptionStyle))],
) + '\n'
)
}
@@ -166,6 +188,28 @@ export class HelpGenerator {
}
}
function wrap(text: string, maxRowLength: number): string {
const length = stripColors(text).length
if (length <= maxRowLength) {
return text
}
const subRows: string[] = []
const words = text.split(' ')
let currentRow = ''
console.log('words', words)
for (const word of words) {
if (stripColors(currentRow).length + stripColors(word).length + 1 > maxRowLength) {
subRows.push(currentRow)
currentRow = ''
}
currentRow += `${word} `
}
subRows.push(currentRow)
return subRows.join('\n')
}
function generateHelpTable<T extends Partial<GenerateTableCommandConfig>>(
items: HelpItem[],
{
@@ -177,8 +221,9 @@ function generateHelpTable<T extends Partial<GenerateTableCommandConfig>>(
}: Partial<T> = {},
): string {
const rows = items.map((o) => {
const name = `${namePrefix}${o.name}${o.aliases.length ? ` | ${aliasPrefix}${o.aliases.join(`|${aliasPrefix}`)}` : ''
}`
const name = `${namePrefix}${o.name}${
o.aliases.length ? ` | ${aliasPrefix}${o.aliases.join(`|${aliasPrefix}`)}` : ''
}`
const description = o.description
return { name, description }
})

View File

@@ -75,9 +75,13 @@ const removeCmd = new MassargCommand<{ component: string }>({
const main = massarg<A>({
name: 'my-cli',
description: 'This is an example CLI',
bindHelpOption: true,
bindHelpCommand: true,
})
.help({
bindOption: true,
bindCommand: true,
headerText: 'This is a header',
footerText: 'This is a footer',
})
.main((opts, parser) => {
console.log('Main command - printing all opts')
console.log(opts, '\n')
@@ -97,6 +101,17 @@ const main = massarg<A>({
aliases: ['n'],
type: 'number',
})
.example({
description: 'Example main command',
input: 'my-cli --bool --number 123',
output: 'Main command - printing all opts\n{ bool: true, number: 123 }\n',
})
.example({
description: 'Example add command',
input: 'my-cli add --component foo --classes bar --classes baz --custom 123',
output:
'Duis ad consectetur dolore elit laborum do et aute consequat magna eu consequat dolore dolor commodo sit enim reprehenderit lorem consectetur adipiscing officia nisi adipiscing consequat consequat labore sint incididunt',
})
// console.log("Opts:", main.getArgs(process.argv.slice(2)), "\n")

View File

@@ -12,7 +12,11 @@ export function setOrPush<T>(
}
type Parseable = string | number | boolean | null | undefined | Record<string, unknown>
export function chainStr(...strs: (Parseable | Parseable[])[]) {
export type DeepRequired<T> = {
[P in keyof T]-?: T[P] extends object ? DeepRequired<T[P]> : NonNullable<T[P]>
}
export function strConcat(...strs: (Parseable | Parseable[])[]) {
const res: string[] = []
for (const str of strs) {
if (typeof str === 'string') {
@@ -20,7 +24,7 @@ export function chainStr(...strs: (Parseable | Parseable[])[]) {
continue
}
if (Array.isArray(str)) {
res.push(chainStr(...str))
res.push(strConcat(...str))
continue
}
if (str == null) {
@@ -43,7 +47,7 @@ export function chainStr(...strs: (Parseable | Parseable[])[]) {
}
export function indent(str: Parseable | Parseable[], indent = 2): string {
return chainStr(str)
return strConcat(str)
.split('\n')
.map((s) => ' '.repeat(indent) + s)
.join('\n')