Skip to content
This repository has been archived by the owner on Apr 13, 2024. It is now read-only.

Commit

Permalink
Merge pull request #47 from AtkinsSJ/better-help
Browse files Browse the repository at this point in the history
Improve help output and add --help option
  • Loading branch information
KernelDeimos authored Mar 6, 2024
2 parents 28b53e8 + 269dc17 commit 08f9bf7
Show file tree
Hide file tree
Showing 7 changed files with 218 additions and 17 deletions.
14 changes: 12 additions & 2 deletions src/ansi-shell/arg-parsers/simple-parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,26 +17,36 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { parseArgs } from '@pkgjs/parseargs';
import { DEFAULT_OPTIONS } from '../../puter-shell/coreutils/coreutil_lib/help.js';

export default {
name: 'simple-parser',
process (ctx, spec) {
async process (ctx, spec) {
console.log({
...spec,
args: ctx.locals.args
});

// Insert standard options
spec.options = Object.assign(spec.options || {}, DEFAULT_OPTIONS);

let result;
try {
if ( ! ctx.locals.args ) debugger;
result = parseArgs({ ...spec, args: ctx.locals.args });
} catch (e) {
ctx.externs.out.write(
await ctx.externs.out.write(
'\x1B[31;1m' +
'error parsing arguments: ' +
e.message + '\x1B[0m\n');
ctx.cmdExecState.valid = false;
return;
}

if (result.values.help) {
ctx.cmdExecState.printHelpAndExit = true;
}

ctx.locals.values = result.values;
ctx.locals.positionals = result.positionals;
}
Expand Down
15 changes: 12 additions & 3 deletions src/ansi-shell/pipeline/Pipeline.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { ConcreteSyntaxError } from "../ConcreteSyntaxError.js";
import { SignalReader } from "../ioutil/SignalReader.js";
import { Exit } from "../../puter-shell/coreutils/coreutil_lib/exit.js";
import { resolveRelativePath } from '../../util/path.js';
import { printUsage } from '../../puter-shell/coreutils/coreutil_lib/help.js';

class Token {
static createFromAST (ctx, ast) {
Expand Down Expand Up @@ -240,7 +241,8 @@ export class PreparedCommand {
sig,
},
cmdExecState: {
valid: true
valid: true,
printHelpAndExit: false,
},
locals: {
command,
Expand All @@ -253,12 +255,19 @@ export class PreparedCommand {
const argProcessor = argparsers[argProcessorId];
const spec = { ...command.args };
delete spec.$;
argProcessor.process(ctx, spec);
await argProcessor.process(ctx, spec);
}

if ( ! ctx.cmdExecState.valid ) {
ctx.locals.exit = -1;
ctx.externs.out.close();
await ctx.externs.out.close();
return;
}

if ( ctx.cmdExecState.printHelpAndExit ) {
ctx.locals.exit = 0;
await printUsage(command, ctx.externs.out, ctx.vars);
await ctx.externs.out.close();
return;
}

Expand Down
5 changes: 5 additions & 0 deletions src/pty/NodeStdioPTT.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ export class NodeStdioPTT {
this.ioctl_listeners[name] = [];
}
this.ioctl_listeners[name].push(listener);

// Hack: Pretend the window got resized, so that listeners get notified of the current size.
if (name === 'ioctl.set') {
process.stdout.emit('resize');
}
}

emit (name, evt) {
Expand Down
74 changes: 63 additions & 11 deletions src/puter-shell/coreutils/coreutil_lib/help.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,29 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export const printUsage = async (command, out) => {
import { wrapText } from '../../../util/wrap-text.js';

const TAB_SIZE = 8;

export const DEFAULT_OPTIONS = {
help: {
description: 'Display this help text, and exit',
type: 'boolean',
},
};

export const printUsage = async (command, out, vars) => {
const { name, usage, description, args } = command;
const { options } = args;
const options = Object.assign(DEFAULT_OPTIONS, args.options);

const heading = txt => {
out.write(`\x1B[34;1m${txt}:\x1B[0m\n`);
const heading = text => {
out.write(`\x1B[34;1m${text}:\x1B[0m\n`);
};
const colorOption = text => {
return `\x1B[92m${text}\x1B[0m`;
};
const colorOptionArgument = text => {
return `\x1B[91m${text}\x1B[0m`;
};

heading('Usage');
Expand All @@ -44,29 +61,64 @@ export const printUsage = async (command, out) => {
}

if (description) {
out.write(`${description}\n\n`);
const wrappedLines = wrapText(description, vars.size.cols);
for (const line of wrappedLines) {
out.write(`${line}\n`);
}
out.write(`\n`);
}

if (options) {
heading('Options');

for (const optionName in options) {
let optionText = '';
let optionText = ' ';
let indentSize = optionText.length;
const option = options[optionName];
if (option.short) {
optionText += `-${option.short}, `;
optionText += colorOption('-' + option.short) + ', ';
indentSize += `-${option.short}, `.length;
} else {
optionText += ` `;
indentSize += ` `.length;
}
optionText += `--${optionName}`;
optionText += colorOption(`--${optionName}`);
indentSize += `--${optionName}`.length;
if (option.type !== 'boolean') {
const valueName = option.valueName || 'VALUE';
optionText += `=${valueName}`;
optionText += `=${colorOptionArgument(valueName)}`;
indentSize += `=${valueName}`.length;
}
if (option.description) {
optionText += `\t ${option.description}`;
const indentSizeIncludingTab = (size) => {
return (Math.floor(size / TAB_SIZE) + 1) * TAB_SIZE + 1;
};

// Wrap the description based on the terminal width, with each line indented.
let remainingWidth = vars.size.cols - indentSizeIncludingTab(indentSize);
let skipIndentOnFirstLine = true;

// If there's not enough room after a very long option name, start on the next line.
if (remainingWidth < 30) {
optionText += '\n';
indentSize = 8;
remainingWidth = vars.size.cols - indentSizeIncludingTab(indentSize);
skipIndentOnFirstLine = false;
}

const wrappedDescriptionLines = wrapText(option.description, remainingWidth);
for (const line of wrappedDescriptionLines) {
if (skipIndentOnFirstLine) {
skipIndentOnFirstLine = false;
} else {
optionText += ' '.repeat(indentSize);
}
optionText += `\t ${line}\n`;
}
} else {
optionText += '\n';
}
out.write(` ${optionText}\n`);
out.write(optionText);
}
out.write('\n');
}
Expand Down
2 changes: 1 addition & 1 deletion src/puter-shell/coreutils/help.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export default {
await err.write(`help: No builtin found named '${commandName}'\n`);
throw new Exit(1);
}
await printUsage(command, out);
await printUsage(command, out, ctx.vars);
return;
}

Expand Down
56 changes: 56 additions & 0 deletions src/util/wrap-text.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Copyright (C) 2024 Puter Technologies Inc.
*
* This file is part of Phoenix Shell.
*
* Phoenix Shell is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// TODO: Detect ANSI escape sequences in the text and treat them as 0 width?
// TODO: Ensure this works with multi-byte characters (UTF-8)
export const wrapText = (text, width) => {
// If width was invalid, just return the original text as a failsafe.
if (typeof width !== 'number' || width < 1)
return [text];

const lines = [];
// This reduces all whitespace to single space characters. Is that a problem?
const words = text.split(/\s+/);

let currentLine = '';
const splitWordIfTooLong = (word) => {
while (word.length > width) {
lines.push(word.substring(0, width - 1) + '-');
word = word.substring(width - 1);
}

currentLine = word;
};

for (let word of words) {
if (currentLine.length === 0) {
splitWordIfTooLong(word);
continue;
}
if ((currentLine.length + 1 + word.length) > width) {
// Next line
lines.push(currentLine);
splitWordIfTooLong(word);
continue;
}
currentLine += ' ' + word;
}
lines.push(currentLine);

return lines;
};
69 changes: 69 additions & 0 deletions test/wrap-text.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Copyright (C) 2024 Puter Technologies Inc.
*
* This file is part of Phoenix Shell.
*
* Phoenix Shell is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import assert from 'assert';
import { wrapText } from '../src/util/wrap-text.js';

describe('wrapText', () => {
const testCases = [
{
description: 'should wrap text',
input: 'Well, hello friends! How are you today?',
width: 12,
output: ['Well, hello', 'friends! How', 'are you', 'today?'],
},
{
description: 'should break too-long words onto multiple lines',
input: 'Antidisestablishmentarianism.',
width: 20,
output: ['Antidisestablishmen-', 'tarianism.'],
},
{
description: 'should break too-long words onto multiple lines',
input: 'Antidisestablishmentarianism.',
width: 10,
output: ['Antidises-', 'tablishme-', 'ntarianis-', 'm.'],
},
{
description: 'should break too-long words when there is already text on the line',
input: 'The longest word I can think of is antidisestablishmentarianism.',
width: 20,
output: ['The longest word I', 'can think of is', 'antidisestablishmen-', 'tarianism.'],
},
{
description: 'should return the original text if the width is invalid',
input: 'Well, hello friends!',
width: 0,
output: ['Well, hello friends!'],
},
];
for (const { description, input, width, output } of testCases) {
it (description, () => {
const result = wrapText(input, width);
for (const line of result) {
if (typeof width === 'number' && width > 0) {
assert.ok(line.length <= width, `Line is too long: '${line}`);
}
}
assert.equal(result.length, output.length, 'Wrong number of lines');
for (const i in result) {
assert.equal(result[i], output[i], `Line ${i} doesn't match: expected '${output[i]}', got '${result[i]}'`);
}
});
}
})

0 comments on commit 08f9bf7

Please sign in to comment.