ArgHelpImpl.java
package net.morimekta.terminal.args.impl;
import net.morimekta.io.tty.TTY;
import net.morimekta.strings.StringUtil;
import net.morimekta.strings.chr.Unicode;
import net.morimekta.strings.io.IndentedPrintWriter;
import net.morimekta.terminal.args.Arg;
import net.morimekta.terminal.args.ArgHelp;
import net.morimekta.terminal.args.ArgParser;
import net.morimekta.terminal.args.Flag;
import net.morimekta.terminal.args.Option;
import net.morimekta.terminal.args.SubCommandSet;
import java.io.PrintStream;
import java.io.UncheckedIOException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import static java.util.Objects.requireNonNull;
public class ArgHelpImpl implements ArgHelp {
public ArgHelpImpl(ArgParser argParser,
String singleLineUsagePrefix,
Comparator<Option> optionComparator,
boolean showDefaults,
boolean showHiddenOptions,
boolean showHiddenArguments,
boolean showHiddenSubCommands,
boolean showSubCommands,
String subCommandsHeader,
int usageWidth) {
this.argParser = argParser;
this.singleLineUsagePrefix = singleLineUsagePrefix;
this.optionComparator = optionComparator;
this.showDefaults = showDefaults;
this.showHiddenOptions = showHiddenOptions;
this.showHiddenArguments = showHiddenArguments;
this.showHiddenSubCommands = showHiddenSubCommands;
this.showSubCommands = showSubCommands;
this.subCommandsHeader = subCommandsHeader;
this.usageWidth = usageWidth;
}
@Override
public void printSingleLineUsage(PrintStream stream) {
var writer = new IndentedPrintWriter(stream);
printSingleLineUsageInternal(writer, 0, 0, false);
writer.flush();
}
@Override
public void printPreamble(PrintStream stream) {
var writer = new IndentedPrintWriter(stream);
printPreambleInternal(writer);
writer.flush();
}
@Override
public void printHelp(PrintStream stream) {
var writer = new IndentedPrintWriter(stream);
printPreambleInternal(writer);
writer.println();
printUsageInternal(writer);
writer.flush();
}
public static class BuilderImpl implements ArgHelp.Builder {
public BuilderImpl(ArgParser parser) {
this.argParser = requireNonNull(parser, "parser == null");
this.singleLineUsagePrefix = "Usage: ";
this.showDefaults = true;
this.showSubCommands = true;
this.subCommandsHeader = "Available sub-commands:";
}
@Override
public ArgHelp build() {
int width = usageWidth;
if (width == 0) {
try {
width = new TTY().getTerminalSize().cols;
} catch (UncheckedIOException e) {
// fall back to fixed 80 width.
width = 80;
}
}
return new ArgHelpImpl(argParser,
singleLineUsagePrefix,
optionComparator,
showDefaults,
showHiddenOptions,
showHiddenArguments,
showHiddenSubCommands,
showSubCommands,
subCommandsHeader,
width);
}
@Override
public Builder singleLineUsagePrefix(String prefix) {
this.singleLineUsagePrefix = prefix;
return this;
}
@Override
public Builder withOptionsComparator(Comparator<Option> comparator) {
this.optionComparator = comparator;
return this;
}
@Override
public Builder showDefaults(boolean show) {
this.showDefaults = show;
return this;
}
@Override
public Builder showHiddenOptions(boolean show) {
this.showHiddenOptions = show;
return this;
}
@Override
public Builder showHiddenArguments(boolean show) {
this.showHiddenArguments = show;
return this;
}
@Override
public Builder showHiddenSubCommands(boolean show) {
this.showHiddenSubCommands = show;
return this;
}
@Override
public Builder showSubCommands(boolean show) {
this.showSubCommands = show;
return this;
}
@Override
public Builder subCommandsHeader(String header) {
subCommandsHeader = header;
return this;
}
@Override
public Builder usageWidth(int width) {
this.usageWidth = width;
return this;
}
private final ArgParser argParser;
private String singleLineUsagePrefix;
private Comparator<Option> optionComparator;
private boolean showDefaults;
private boolean showHiddenOptions;
private boolean showHiddenArguments;
private boolean showHiddenSubCommands;
private boolean showSubCommands;
private int usageWidth;
private String subCommandsHeader;
}
// ------------------
public void printSingleLineUsageInternal(IndentedPrintWriter writer,
int prefixLength,
int indentLength,
boolean pretty) {
List<Option> options = argParser.getOptions();
if (optionComparator != null) {
options = new ArrayList<>(options);
options.sort(optionComparator);
}
List<String> entries = new ArrayList<>();
// first just list up all the unary short opts.
StringBuilder sh = new StringBuilder();
// Only include the first short name form the flag.
options.stream()
.filter(opt -> opt instanceof Flag)
.filter(opt -> showHiddenOptions || !opt.isHidden())
.filter(opt -> opt.getShortNames().length() > 0)
.forEachOrdered(opt -> sh.append(opt.getShortNames().charAt(0)));
if (sh.length() > 0) {
entries.add("[-" + sh + ']');
}
for (Option opt : options) {
if (opt.isHidden() && !showHiddenOptions) {
continue;
}
if (opt instanceof Flag && opt.getShortNames().length() > 0) {
// already included as short opt.
continue;
}
String usage = opt.getSingleLineUsage();
if (usage != null) {
entries.add(usage);
}
}
for (Arg arg : argParser.getArguments()) {
if (arg.isHidden() && !showHiddenArguments) {
continue;
}
String usage = arg.getSingleLineUsage();
if (usage != null) {
entries.add(usage);
}
}
var scs = argParser.getSubCommandSet();
if (scs != null) {
if (!scs.isHidden() || showHiddenArguments) {
String usage = scs.getSingleLineUsage();
if (usage != null) {
entries.add(usage);
}
}
}
boolean first = true;
if (pretty) {
int pos = prefixLength;
for (String entry : entries) {
if (pos + 1 + entry.length() > usageWidth) {
pos = indentLength;
writer.appendln();
} else if (first) {
first = false;
} else {
writer.append(' ');
++pos;
}
writer.append(entry);
pos += entry.length();
}
} else {
for (String entry : entries) {
if (first) {
first = false;
} else {
writer.append(' ');
}
writer.append(entry);
}
}
}
private void printPreambleInternal(IndentedPrintWriter writer) {
writer.append(argParser.getDescription())
.append(" - ")
.append(argParser.getVersion())
.println();
int indent = Math.max(4, singleLineUsagePrefix.length() + argParser.getProgram().length() + 1);
writer.append(singleLineUsagePrefix);
writer.append(argParser.getProgram());
writer.append(' ');
writer.begin(" ".repeat(indent));
printSingleLineUsageInternal(writer, singleLineUsagePrefix.length(), indent, true);
writer.end();
writer.println();
}
private void printUsageInternal(IndentedPrintWriter writer) {
List<Option> options = argParser.getOptions();
if (optionComparator != null) {
options = new ArrayList<>(options);
options.sort(optionComparator);
}
int prefixLen = 0;
for (Option opt : options) {
prefixLen = Math.max(prefixLen, opt.getPrefix().length());
}
for (Arg arg : argParser.getArguments()) {
prefixLen = Math.max(prefixLen, arg.getPrefix().length());
}
prefixLen = Math.min(prefixLen, (usageWidth / 3) - USAGE_EXTRA_CHARS);
String indent = " ".repeat(prefixLen + USAGE_EXTRA_CHARS);
boolean first = true;
for (Option opt : options) {
first = maybePrintSingleUsageEntry(writer, showHiddenOptions, usageWidth, prefixLen, indent, first, opt);
}
for (Arg arg : argParser.getArguments()) {
first = maybePrintSingleUsageEntry(writer, showHiddenArguments, usageWidth, prefixLen, indent, first, arg);
}
var scs = argParser.getSubCommandSet();
if (scs != null) {
first = maybePrintSingleUsageEntry(writer, showHiddenArguments, usageWidth, prefixLen, indent, first, scs);
}
if (showSubCommands && argParser.getSubCommandSet() != null) {
if (!first) {
writer.newline();
}
writer.appendln(subCommandsHeader);
writer.newline();
writer.appendln();
printUsageInternal(writer, argParser.getSubCommandSet());
// to NOT print an extra newline at end of usage.
first = true;
}
// if nothing printed, complete nothing.
if (!first) {
writer.newline();
}
}
private void printUsageInternal(IndentedPrintWriter writer, SubCommandSet<?> subCommandSet) {
int prefixLen = 0;
for (var cmd : subCommandSet.getSubCommands()) {
prefixLen = Math.max(prefixLen, cmd.getName().length());
}
prefixLen = Math.min(prefixLen, (usageWidth / 3) - USAGE_EXTRA_CHARS);
String indent = " ".repeat(prefixLen + USAGE_EXTRA_CHARS);
boolean first = true;
for (var arg : subCommandSet.getSubCommands()) {
if (arg.isHidden() && !showHiddenSubCommands) {
continue;
}
String prefix = arg.getName();
String usage = arg.getUsage();
if (first) {
first = false;
} else {
writer.appendln();
}
writer.begin(indent);
printSingleUsageEntry(writer, prefix, usage, prefixLen, usageWidth);
writer.end();
}
writer.newline()
.flush();
}
private boolean maybePrintSingleUsageEntry(
IndentedPrintWriter writer,
boolean showHidden,
int usageWidth,
int prefixLen,
String indent,
boolean first,
Arg arg) {
if (arg.isHidden() && !showHidden) {
return first;
}
String prefix = arg.getPrefix();
String usage = arg.getUsage();
if (showDefaults && arg.getDefaultValue() != null) {
usage = usage + " (default:" + Unicode.NBSP + arg.getDefaultValue() + ")";
}
if (!first) {
writer.appendln();
}
writer.begin(indent);
printSingleUsageEntry(writer, prefix, usage, prefixLen, usageWidth);
writer.end();
return false;
}
private static void printSingleUsageEntry(
IndentedPrintWriter writer,
String prefix,
String usage,
int prefixLen,
int usageWidth) {
String[] descLines;
int printLinesFrom = 0;
writer.append(" ");
if (prefix.length() > prefixLen) {
writer.append(prefix);
if (prefix.length() > (prefixLen * 1.7)) {
descLines = StringUtil.wrap(usage, usageWidth - prefixLen - USAGE_EXTRA_CHARS).split("\\r?\\n");
} else {
writer.append(" : ");
String[] tmp = StringUtil.wrap(usage, usageWidth - prefix.length() - USAGE_EXTRA_CHARS)
.split("\\r?\\n", 2);
writer.append(tmp[0]);
if (tmp.length > 1) {
descLines = StringUtil.wrap(tmp[1].replaceAll("\\r?\\n", " "),
usageWidth - prefixLen - USAGE_EXTRA_CHARS).split("\\r?\\n");
} else {
descLines = new String[0];
}
}
} else {
writer.append(StringUtil.rightPad(prefix, prefixLen));
writer.append(" : ");
descLines = StringUtil.wrap(usage, usageWidth - prefixLen - USAGE_EXTRA_CHARS).split("\\r?\\n");
writer.append(descLines[0]);
printLinesFrom = 1;
}
for (int i = printLinesFrom; i < descLines.length; ++i) {
writer.appendln(descLines[i]);
}
}
private static final int USAGE_EXTRA_CHARS = 4;
private final ArgParser argParser;
private final String singleLineUsagePrefix;
private final Comparator<Option> optionComparator;
private final boolean showDefaults;
private final boolean showHiddenOptions;
private final boolean showHiddenArguments;
private final boolean showHiddenSubCommands;
private final boolean showSubCommands;
private final String subCommandsHeader;
private final int usageWidth;
}