Skip to content

Commit 70dfd55

Browse files
committed
feat: global column width
1 parent a16b1a4 commit 70dfd55

1 file changed

Lines changed: 99 additions & 38 deletions

File tree

src/help.ts

Lines changed: 99 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,31 @@ import {
1010
} from './option'
1111

1212
export const GenerateTableCommandConfig = z.object({
13+
/** Length of each row in the table */
1314
lineLength: z.number().optional(),
15+
/** When `false`, each row is separated by a blank line */
1416
compact: z.boolean().optional(),
17+
/** Style of the command/option name */
1518
nameStyle: StringStyle.optional(),
19+
/** Style of the command/option description */
1620
descriptionStyle: StringStyle.optional(),
21+
/** Prefix for the command/option name (default is the command's prefix) */
1722
namePrefix: z.string().optional(),
23+
/** Prefix for the command/option aliases (default is the command's prefix) */
1824
aliasPrefix: z.string().optional(),
19-
negatePrefix: z.string().optional(),
20-
negateAliasPrefix: z.string().optional(),
2125
})
2226
export type GenerateTableCommandConfig = z.infer<typeof GenerateTableCommandConfig>
2327

24-
export const GenerateTableOptionConfig = GenerateTableCommandConfig
28+
export const GenerateTableOptionConfig = GenerateTableCommandConfig.merge(
29+
z.object({
30+
/** Prefix for the command/option negations (default is the command's prefix) */
31+
negatePrefix: z.string().optional(),
32+
/** Prefix for the command/option negation aliases (default is the command's prefix) */
33+
negateAliasPrefix: z.string().optional(),
34+
/** Whether to display negations with each option name */
35+
displayNegations: z.boolean().optional(),
36+
}),
37+
)
2538
export type GenerateTableOptionConfig = z.infer<typeof GenerateTableOptionConfig>
2639

2740
export const HelpConfig = z.object({
@@ -38,6 +51,9 @@ export const HelpConfig = z.object({
3851
*/
3952
bindOption: z.boolean().optional(),
4053

54+
/** Whether to align all tables to the column widths, or have each table be independent. Default is `true` */
55+
useGlobalTableColumns: z.boolean().default(true).optional(),
56+
4157
/** Options for generating the table of commands */
4258
commandOptions: GenerateTableCommandConfig.omit({ lineLength: true }).optional(),
4359
/** Options for generating the table of options */
@@ -83,6 +99,7 @@ export type HelpConfig = z.infer<typeof HelpConfig>
8399

84100
export const defaultHelpConfig: DeepRequired<HelpConfig> = {
85101
lineLength: 80,
102+
useGlobalTableColumns: true,
86103
commandOptions: {
87104
nameStyle: {
88105
color: 'yellow',
@@ -96,6 +113,7 @@ export const defaultHelpConfig: DeepRequired<HelpConfig> = {
96113
aliasPrefix: OPT_SHORT_PREFIX,
97114
negatePrefix: NEGATE_FULL_PREFIX,
98115
negateAliasPrefix: NEGATE_SHORT_PREFIX,
116+
displayNegations: false,
99117
nameStyle: {
100118
color: 'yellow',
101119
},
@@ -160,14 +178,23 @@ export class HelpGenerator {
160178
const entry = this.entry
161179
const CMD_OPT_INDENT = 4
162180
const _wrap = (text: string, indent = 0) => wrap(text, this.config.lineLength - indent)
163-
const options = generateHelpTable(entry.options, {
181+
const optionOptions = {
164182
...this.config.optionOptions,
165183
lineLength: this.config.lineLength - CMD_OPT_INDENT,
166-
}).trimEnd()
167-
const commands = generateHelpTable(entry.commands, {
184+
}
185+
const commandOptions = {
168186
...this.config.commandOptions,
187+
displayNegations: false,
169188
lineLength: this.config.lineLength - CMD_OPT_INDENT,
170-
}).trimEnd()
189+
}
190+
const maxNameLength = this.config.useGlobalTableColumns
191+
? Math.max(
192+
getMaxNameLength(entry.options.map((e) => getItemDetails(e, optionOptions))),
193+
getMaxNameLength(entry.commands.map((e) => getItemDetails(e, commandOptions))),
194+
)
195+
: undefined
196+
const options = generateHelpTable(entry.options, optionOptions, maxNameLength).trimEnd()
197+
const commands = generateHelpTable(entry.commands, commandOptions, maxNameLength).trimEnd()
171198
const examples = entry.examples
172199
.map((example) => {
173200
const { description, input, output } = example
@@ -266,46 +293,80 @@ function wrap(text: string, lineLength: number): string {
266293
return subRows.join('\n')
267294
}
268295

269-
function generateHelpTable<T extends Partial<GenerateTableCommandConfig>>(
270-
items: HelpItem[],
271-
{
272-
lineLength: lineLength = 80,
296+
type ParsedHelpItem = {
297+
name: string
298+
description: string
299+
hidden: boolean
300+
}
301+
302+
const getMaxNameLength = (items: ParsedHelpItem[]): number =>
303+
Math.max(...items.map((o) => o.name.length))
304+
305+
function getItemDetails(
306+
o: HelpItem,
307+
options?: Pick<
308+
GenerateTableOptionConfig & GenerateTableOptionConfig,
309+
'displayNegations' | 'namePrefix' | 'aliasPrefix' | 'negatePrefix' | 'negateAliasPrefix'
310+
>,
311+
): ParsedHelpItem {
312+
const {
313+
displayNegations = false,
273314
namePrefix = '',
274315
negatePrefix = '',
275316
aliasPrefix = '',
276-
negateAliasPrefix: aliasNegatePrefix = '',
317+
negateAliasPrefix = '',
318+
} = options ?? {}
319+
const cmdNames = {
320+
full: `${namePrefix}${o.name}`,
321+
fullNegated: negatePrefix ? `${negatePrefix}${o.name}` : undefined,
322+
aliases: o.aliases.map((a) => `${aliasPrefix}${a}`).join(' | '),
323+
aliasesNegated: negatePrefix
324+
? o.aliases.map((a) => `${negateAliasPrefix}${a}`).join(' | ')
325+
: undefined,
326+
}
327+
const name = [
328+
cmdNames.full,
329+
cmdNames.aliases,
330+
displayNegations && cmdNames.fullNegated,
331+
displayNegations && cmdNames.aliasesNegated,
332+
]
333+
.filter(Boolean)
334+
.join(' | ')
335+
const description = o.description
336+
const hidden = o.hidden || false
337+
return { name, description, hidden }
338+
}
339+
340+
function generateHelpTable<T extends GenerateTableCommandConfig | GenerateTableOptionConfig>(
341+
items: HelpItem[],
342+
fullConfig: Partial<T> = {},
343+
maxNameLength?: number,
344+
): string {
345+
const {
346+
lineLength = 80,
347+
namePrefix = '',
348+
aliasPrefix = '',
349+
negatePrefix = '',
350+
negateAliasPrefix = '',
351+
displayNegations = false,
277352
compact = false,
278353
...config
279-
}: Partial<T> = {},
280-
): string {
354+
} = fullConfig as GenerateTableOptionConfig
281355
const rows = items
282-
.map((o) => {
283-
const cmdNames = {
284-
full: `${namePrefix}${o.name}`,
285-
fullNegated: negatePrefix ? `${negatePrefix}${o.name}` : undefined,
286-
aliases: o.aliases.map((a) => `${aliasPrefix}${a}`).join(' | '),
287-
aliasesNegated: negatePrefix
288-
? o.aliases.map((a) => `${aliasNegatePrefix}${a}`).join(' | ')
289-
: undefined,
290-
}
291-
const name = [
292-
cmdNames.full,
293-
cmdNames.aliases,
294-
// cmdNames.fullNegated,
295-
// cmdNames.aliasesNegated,
296-
]
297-
.filter(Boolean)
298-
.join(' | ')
299-
const description = o.description
300-
const hidden = o.hidden || false
301-
return { name, description, hidden }
302-
})
356+
.map((o) =>
357+
getItemDetails(o, {
358+
namePrefix,
359+
aliasPrefix,
360+
negatePrefix,
361+
negateAliasPrefix,
362+
}),
363+
)
303364
.filter((r) => !r.hidden)
304-
const maxNameLength = Math.max(...rows.map((o) => o.name.length))
365+
maxNameLength ??= getMaxNameLength(rows)
305366
const nameStyle = (name: string) => format(name, config.nameStyle)
306367
const descStyle = (desc: string) => format(desc, config.descriptionStyle)
307368
const table = rows.map((row) => {
308-
const name = nameStyle(row.name.padEnd(maxNameLength + 2))
369+
const name = nameStyle(row.name.padEnd(maxNameLength! + 2))
309370
const description = descStyle(row.description)
310371
const length = stripStyle(name).length + stripStyle(description).length
311372
if (length <= lineLength) {
@@ -322,7 +383,7 @@ function generateHelpTable<T extends Partial<GenerateTableCommandConfig>>(
322383
for (const word of words) {
323384
if (stripStyle(currentRow).length + stripStyle(word).length + 1 > lineLength) {
324385
subRows.push(currentRow)
325-
currentRow = ' '.repeat(maxNameLength + 2)
386+
currentRow = ' '.repeat(maxNameLength! + 2)
326387
}
327388
currentRow += `${word} `
328389
}

0 commit comments

Comments
 (0)