picocli.shell.jline3.PicocliCommands Maven / Gradle / Ivy
package picocli.shell.jline3;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.stream.Collectors;
import org.jline.builtins.Options.HelpException;
import org.jline.console.ArgDesc;
import org.jline.console.CmdDesc;
import org.jline.console.CommandRegistry;
import org.jline.reader.Candidate;
import org.jline.reader.Completer;
import org.jline.reader.LineReader;
import org.jline.reader.ParsedLine;
import org.jline.reader.impl.completer.ArgumentCompleter;
import org.jline.reader.impl.completer.NullCompleter;
import org.jline.reader.impl.completer.SystemCompleter;
import org.jline.terminal.Terminal;
import org.jline.utils.AttributedString;
import org.jline.utils.InfoCmp.Capability;
import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.Help;
import picocli.CommandLine.IFactory;
import picocli.CommandLine.Model.CommandSpec;
import picocli.CommandLine.Model.OptionSpec;
/**
* Compiles SystemCompleter for command completion and implements a method commandDescription() that provides command descriptions
* for JLine TailTipWidgets to be displayed in terminal status bar.
* SystemCompleter implements the JLine 3 {@link Completer} interface. SystemCompleter generates completion
* candidates for the specified command line based on the {@link CommandSpec} that this {@code PicocliCommands} was constructed with.
*
* @since 4.1.2
*/
public class PicocliCommands implements CommandRegistry {
/**
* Command that clears the screen.
*
* WARNING: This subcommand needs a JLine {@code Terminal} to clear the screen.
* To accomplish this, construct the {@code CommandLine} with a {@code PicocliCommandsFactory},
* and set the {@code Terminal} on that factory. For example:
*
* @Command(subcommands = PicocliCommands.ClearScreen.class)
* class MyApp //...
*
* PicocliCommandsFactory factory = new PicocliCommandsFactory();
* CommandLine cmd = new CommandLine(new MyApp(), factory);
* // create terminal
* factory.setTerminal(terminal);
*
*
* @since 4.6
*/
@Command(name = "cls", aliases = "clear", mixinStandardHelpOptions = true,
description = "Clears the screen", version = "1.0")
public static class ClearScreen implements Callable {
private final Terminal terminal;
ClearScreen(Terminal terminal) { this.terminal = terminal; }
public Void call() throws IOException {
if (terminal != null) { terminal.puts(Capability.clear_screen); }
return null;
}
}
/**
* Command factory that is necessary for applications that want the use the {@code ClearScreen} subcommand.
* It can be chained with other factories.
*
* WARNING: If the application uses the {@code ClearScreen} subcommand, construct the {@code CommandLine}
* with a {@code PicocliCommandsFactory}, and set the {@code Terminal} on that factory. Applications need
* to call the {@code setTerminal} method with a {@code Terminal}; this will be passed to the {@code ClearScreen}
* subcommand.
*
* For example:
*
* PicocliCommandsFactory factory = new PicocliCommandsFactory();
* CommandLine cmd = new CommandLine(new MyApp(), factory);
* // create terminal
* factory.setTerminal(terminal);
*
*
* Other factories can be chained by passing them in to the constructor like this:
*
* MyCustomFactory customFactory = createCustomFactory(); // your application custom factory
* PicocliCommandsFactory factory = new PicocliCommandsFactory(customFactory); // chain the factories
*
*
* @since 4.6
*/
public static class PicocliCommandsFactory implements CommandLine.IFactory {
private CommandLine.IFactory nextFactory;
private Terminal terminal;
public PicocliCommandsFactory() {
// nextFactory and terminal are null
}
public PicocliCommandsFactory(IFactory nextFactory) {
this.nextFactory = nextFactory;
// nextFactory is set (but may be null) and terminal is null
}
@SuppressWarnings("unchecked")
public K create(Class clazz) throws Exception {
if (ClearScreen.class == clazz) { return (K) new ClearScreen(terminal); }
if (nextFactory != null) { return nextFactory.create(clazz); }
return CommandLine.defaultFactory().create(clazz);
}
public void setTerminal(Terminal terminal) {
this.terminal = terminal;
// terminal may be null, so check before using it in ClearScreen command
}
}
private final CommandLine cmd;
private final Set commands;
private final Map aliasCommand = new HashMap<>();
public PicocliCommands(CommandLine cmd) {
this.cmd = cmd;
commands = cmd.getCommandSpec().subcommands().keySet();
for (String c: commands) {
for (String a: cmd.getSubcommands().get(c).getCommandSpec().aliases()) {
aliasCommand.put(a, c);
}
}
}
/**
*
* @param command
* @return true if PicocliCommands contains command
*/
public boolean hasCommand(String command) {
return commands.contains(command) || aliasCommand.containsKey(command);
}
public SystemCompleter compileCompleters() {
SystemCompleter out = new SystemCompleter();
List all = new ArrayList<>();
all.addAll(commands);
all.addAll(aliasCommand.keySet());
out.add(all, new PicocliCompleter());
return out;
}
private class PicocliCompleter extends ArgumentCompleter implements Completer {
public PicocliCompleter() { super(NullCompleter.INSTANCE); }
@Override
public void complete(LineReader reader, ParsedLine commandLine, List candidates) {
assert commandLine != null;
assert candidates != null;
String word = commandLine.word();
List words = commandLine.words();
CommandLine sub = findSubcommandLine(words, commandLine.wordIndex());
if (sub == null) {
return;
}
if (word.startsWith("-")) {
String buffer = word.substring(0, commandLine.wordCursor());
int eq = buffer.indexOf('=');
for (OptionSpec option : sub.getCommandSpec().options()) {
if (option.arity().max() == 0 && eq < 0) {
addCandidates(candidates, Arrays.asList(option.names()));
} else {
if (eq > 0) {
String opt = buffer.substring(0, eq);
if (Arrays.asList(option.names()).contains(opt) && option.completionCandidates() != null) {
addCandidates(candidates, option.completionCandidates(), buffer.substring(0, eq + 1), "", true);
}
} else {
addCandidates(candidates, Arrays.asList(option.names()), "", "=", false);
}
}
}
} else {
addCandidates(candidates, sub.getSubcommands().keySet());
for (CommandLine s : sub.getSubcommands().values()) {
addCandidates(candidates, Arrays.asList(s.getCommandSpec().aliases()));
}
}
}
private void addCandidates(List candidates, Iterable cands) {
addCandidates(candidates, cands, "", "", true);
}
private void addCandidates(List candidates, Iterable cands, String preFix, String postFix, boolean complete) {
for (String s : cands) {
candidates.add(new Candidate(AttributedString.stripAnsi(preFix + s + postFix), s, null, null, null, null, complete));
}
}
}
private CommandLine findSubcommandLine(List args, int lastIdx) {
CommandLine out = cmd;
for (int i = 0; i < lastIdx; i++) {
if (!args.get(i).startsWith("-")) {
out = findSubcommandLine(out, args.get(i));
if (out == null) {
break;
}
}
}
return out;
}
private CommandLine findSubcommandLine(CommandLine cmdline, String command) {
for (CommandLine s : cmdline.getSubcommands().values()) {
if (s.getCommandName().equals(command) || Arrays.asList(s.getCommandSpec().aliases()).contains(command)) {
return s;
}
}
return null;
}
/**
*
* @param args
* @return command description for JLine TailTipWidgets to be displayed in terminal status bar.
*/
@Override
public CmdDesc commandDescription(List args) {
CommandLine sub = findSubcommandLine(args, args.size());
if (sub == null) {
return null;
}
CommandSpec spec = sub.getCommandSpec();
Help cmdhelp= new picocli.CommandLine.Help(spec);
List main = new ArrayList<>();
Map> options = new HashMap<>();
String synopsis = AttributedString.stripAnsi(spec.usageMessage().sectionMap().get("synopsis").render(cmdhelp).toString());
main.add(HelpException.highlightSyntax(synopsis.trim(), HelpException.defaultStyle()));
// using JLine help highlight because the statement below does not work well...
// main.add(new AttributedString(spec.usageMessage().sectionMap().get("synopsis").render(cmdhelp).toString()));
for (OptionSpec o : spec.options()) {
String key = Arrays.stream(o.names()).collect(Collectors.joining(" "));
List val = new ArrayList<>();
for (String d: o.description()) {
val.add(new AttributedString(d));
}
if (o.arity().max() > 0) {
key += "=" + o.paramLabel();
}
options.put(key, val);
}
return new CmdDesc(main, ArgDesc.doArgNames(Arrays.asList("")), options);
}
@Override
public List commandInfo(String command) {
List out = new ArrayList<>();
CommandSpec spec = cmd.getSubcommands().get(command).getCommandSpec();
Help cmdhelp = new picocli.CommandLine.Help(spec);
String description = AttributedString.stripAnsi(spec.usageMessage().sectionMap().get("description").render(cmdhelp).toString());
out.addAll(Arrays.asList(description.split("\\r?\\n")));
return out;
}
// For JLine >= 3.16.0
@Override
public Object invoke(CommandRegistry.CommandSession session, String command, Object[] args) throws Exception {
List arguments = new ArrayList<>();
arguments.add( command );
arguments.addAll( Arrays.stream( args ).map( Object::toString ).collect( Collectors.toList() ) );
cmd.execute( arguments.toArray( new String[0] ) );
return null;
}
// @Override This method was removed in JLine 3.16.0; keep it in case this component is used with an older version of JLine
public Object execute(CommandRegistry.CommandSession session, String command, String[] args) throws Exception {
List arguments = new ArrayList<>();
arguments.add(command);
arguments.addAll(Arrays.asList(args));
cmd.execute(arguments.toArray(new String[0]));
return null;
}
@Override
public Set commandNames() {
return commands;
}
@Override
public Map commandAliases() {
return aliasCommand;
}
// @Override This method was removed in JLine 3.16.0; keep it in case this component is used with an older version of JLine
public CmdDesc commandDescription(String command) {
return null;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy