@hashgraph/solo
Version:
An opinionated CLI tool to deploy and manage private Hedera Networks.
169 lines (140 loc) • 5.82 kB
text/typescript
// SPDX-License-Identifier: Apache-2.0
import {inject, injectable} from 'tsyringe-neo';
import {InjectTokens} from './dependency-injection/inject-tokens.js';
import {type SoloLogger} from './logging/solo-logger.js';
import {patchInject} from './dependency-injection/container-helper.js';
type Table = string[][];
()
export class HelpRenderer {
public constructor((InjectTokens.SoloLogger) private readonly logger: SoloLogger) {
this.logger = patchInject(logger, InjectTokens.SoloLogger, this.constructor.name);
}
private splitAtClosestWhitespace(input: string, maxLength: number = 120): [string, string] {
if (input.length <= maxLength) {
return [input, ''];
}
const splitIndex: number = input.lastIndexOf(' ', maxLength);
if (splitIndex === -1) {
return [input, ''];
}
return [input.slice(0, Math.max(0, splitIndex)), input.slice(Math.max(0, splitIndex + 1))];
}
private createFlagsTable(lines: string[]): Table {
const table: Table = [];
for (const line of lines) {
let columns: string[] = line.split(/(--[1-9a-zA-Z|-]+)/);
// if the description contains --flag there will be more than the expected amount of columns
// joins all columns after columns[2]
if (columns.length > 3) {
const firstPart: string[] = columns.slice(0, 2);
const secondPart: string = columns.slice(2).join(' ');
columns = [...firstPart, secondPart];
} else if (columns.length === 1 && table.length > 0) {
const descriptions: string[] = columns[0].split(/(\[.+])/);
table.at(-1)[2] += ` ${descriptions[0].trim()}`;
if (descriptions[1]) {
table.at(-1)[3] += ` ${descriptions[1].trim()}`.trim();
}
}
try {
const descriptions: string[] = columns[2].split(/(\[.+])/);
columns[2] = descriptions[0];
columns[3] = descriptions[1] || '';
columns = columns.map((column: string): string => column.trim());
table.push(columns);
} catch (error) {
this.logger.debug(`Error processing line for help rendering: ${line}`, error as Error);
}
}
return table;
}
private sortFlagsTable(table: Table): Table {
table.sort((row1: string[], row2: string[]) => {
return row1[1].localeCompare(row2[1]);
});
const requiredTable: Table = table.filter((row: string[]): boolean => row[3].includes('required'));
const optionalTable: Table = table.filter((row: string[]): boolean => !row[3].includes('required'));
return [
...requiredTable,
['', '', '', ''], // add a blank line between required and optional flags
...optionalTable,
];
}
private calculateMaxColumnLengths(table: Table): number[] {
const columnMaxLengths: number[] = [0, 0, 0, 0];
for (const row of table) {
for (const [index, element] of row.entries()) {
columnMaxLengths[index] = Math.max(columnMaxLengths[index], element.length);
}
}
return columnMaxLengths;
}
private wrapFlagsTable(table: Table, columnMaxLengths: number[], wrap: number): Table {
const wrappedTable: Table = [];
for (const row of table) {
if (row[2].length > wrap) {
const description: string = row[2];
let splitDescription: [string, string] = this.splitAtClosestWhitespace(description, wrap);
wrappedTable.push([row[0], row[1], splitDescription[0], row[3]]);
while (splitDescription[1] && splitDescription[1].length > 0) {
splitDescription = this.splitAtClosestWhitespace(splitDescription[1], wrap);
wrappedTable.push(['', '', splitDescription[0], '']);
}
} else {
wrappedTable.push(row);
}
}
return wrappedTable;
}
private getDescriptionWrap(terminalWidth: number, columnMaxLengths: number[]): number {
let wrap: number = terminalWidth - columnMaxLengths[0] - columnMaxLengths[1] - columnMaxLengths[3] - 6;
if (wrap < 30) {
wrap = 30;
} // set min and max values
if (wrap > 70) {
wrap = 70;
}
if (columnMaxLengths[2] < wrap) {
wrap = columnMaxLengths[2];
} else {
columnMaxLengths[2] = wrap;
}
return wrap;
}
private addColumnPadding(table: Table, columnMaxLengths: number[]): string[] {
const outputLines: string[] = [];
for (const row of table) {
const line: string[] = [];
for (const [index, element] of row.entries()) {
line.push(element.padEnd(columnMaxLengths[index]));
}
outputLines.push(line.join(' '));
}
return outputLines;
}
public render(rootCmd: any, rawHelp: string): void {
const splittingString: string = 'Options:\n';
const splitOutput: string[] = rawHelp.split(splittingString);
if (splitOutput.length < 2) {
this.logger.showUser(rawHelp);
return;
}
let finalOutput: string = splitOutput[0] + splittingString;
let lines: string[] = splitOutput[1].split('\n');
lines = lines.map((line: string): string => line.replace(/^\s+/, ''));
// Formatting for flag options
const table: Table = this.createFlagsTable(lines);
// apply sorting
const sortedTable: Table = this.sortFlagsTable(table);
const columnMaxLengths: number[] = this.calculateMaxColumnLengths(sortedTable);
// wrap the description column at the wrapping point
const terminalWidth: number = rootCmd.terminalWidth();
const wrap: number = this.getDescriptionWrap(terminalWidth, columnMaxLengths);
const wrappedTable = this.wrapFlagsTable(sortedTable, columnMaxLengths, wrap);
const outputLines = this.addColumnPadding(wrappedTable, columnMaxLengths);
finalOutput += '\n';
finalOutput += outputLines.join('\n');
finalOutput += '\n';
this.logger.showUser(finalOutput);
}
}