diff --git a/picocli-shell-jline3/src/main/java/picocli/shell/jline3/PicocliCommands.java b/picocli-shell-jline3/src/main/java/picocli/shell/jline3/PicocliCommands.java index f5c7d3575..f3aaafa47 100644 --- a/picocli-shell-jline3/src/main/java/picocli/shell/jline3/PicocliCommands.java +++ b/picocli-shell-jline3/src/main/java/picocli/shell/jline3/PicocliCommands.java @@ -1,8 +1,13 @@ package picocli.shell.jline3; -import java.nio.file.Path; -import java.util.*; -import java.util.function.Supplier; +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; @@ -13,19 +18,20 @@ 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; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - /** * Compiles SystemCompleter for command completion and implements a method commandDescription() that provides command descriptions * for JLine TailTipWidgets to be displayed in terminal status bar. @@ -35,17 +41,92 @@ * @since 4.1.2 */ public class PicocliCommands implements CommandRegistry { - private final Supplier workDir; + + /** + * 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: + *

+     * 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 allows chaining (or delegating) to a custom factory. + *

+ * 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);
+     * 
+ * + * Custom 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(Path workDir, CommandLine cmd) { - this(() -> workDir, cmd); - } - - public PicocliCommands(Supplier workDir, CommandLine cmd) { - this.workDir = workDir; + public PicocliCommands(CommandLine cmd) { this.cmd = cmd; commands = cmd.getCommandSpec().subcommands().keySet(); for (String c: commands) { @@ -74,9 +155,9 @@ public SystemCompleter compileCompleters() { return out; } - private class PicocliCompleter implements Completer { + private class PicocliCompleter extends ArgumentCompleter implements Completer { - public PicocliCompleter() {} + public PicocliCompleter() { super(NullCompleter.INSTANCE); } @Override public void complete(LineReader reader, ParsedLine commandLine, List candidates) { diff --git a/picocli-shell-jline3/src/test/java/picocli/shell/jline3/example/Example.java b/picocli-shell-jline3/src/test/java/picocli/shell/jline3/example/Example.java index 6ed8cc052..0ab01f2c8 100644 --- a/picocli-shell-jline3/src/test/java/picocli/shell/jline3/example/Example.java +++ b/picocli-shell-jline3/src/test/java/picocli/shell/jline3/example/Example.java @@ -17,13 +17,13 @@ import picocli.CommandLine.Option; import picocli.CommandLine.ParentCommand; import picocli.shell.jline3.PicocliCommands; +import picocli.shell.jline3.PicocliCommands.PicocliCommandsFactory; -import java.io.IOException; import java.io.PrintWriter; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; /** * Example that demonstrates how to build an interactive shell with JLine3 and picocli. @@ -42,18 +42,12 @@ public class Example { ""}, footer = {"", "Press Ctl-D to exit."}, subcommands = { - MyCommand.class, ClearScreen.class, CommandLine.HelpCommand.class}) + MyCommand.class, PicocliCommands.ClearScreen.class, CommandLine.HelpCommand.class}) static class CliCommands implements Runnable { - LineReaderImpl reader; PrintWriter out; CliCommands() {} - public void setReader(LineReader reader){ - this.reader = (LineReaderImpl)reader; - out = reader.getTerminal().writer(); - } - public void run() { out.println(new CommandLine(this).getUsageMessage()); } @@ -128,42 +122,31 @@ public void subtract(@Option(names = {"-l", "--left"}, required = true) int left } } - /** - * Command that clears the screen. - */ - @Command(name = "cls", aliases = "clear", mixinStandardHelpOptions = true, - description = "Clears the screen", version = "1.0") - static class ClearScreen implements Callable { - - @ParentCommand CliCommands parent; - - public Void call() throws IOException { - parent.reader.clearScreen(); - return null; - } - } - - private static Path workDir() { - return Paths.get(System.getProperty("user.dir")); - } - public static void main(String[] args) { AnsiConsole.systemInstall(); try { + Supplier workDir = () -> Paths.get(System.getProperty("user.dir")); // set up JLine built-in commands - Builtins builtins = new Builtins(Example::workDir, null, null); + Builtins builtins = new Builtins(workDir, null, null); builtins.rename(Builtins.Command.TTOP, "top"); builtins.alias("zle", "widget"); builtins.alias("bindkey", "keymap"); // set up picocli commands CliCommands commands = new CliCommands(); - CommandLine cmd = new CommandLine(commands); - PicocliCommands picocliCommands = new PicocliCommands(Example::workDir, cmd); + + PicocliCommandsFactory factory = new PicocliCommandsFactory(); + // Or, if you have your own factory, you can chain them like this: + // MyCustomFactory customFactory = createCustomFactory(); // your application custom factory + // PicocliCommandsFactory factory = new PicocliCommandsFactory(customFactory); // chain the factories + + CommandLine cmd = new CommandLine(commands, factory); + PicocliCommands picocliCommands = new PicocliCommands(cmd); Parser parser = new DefaultParser(); try (Terminal terminal = TerminalBuilder.builder().build()) { - SystemRegistry systemRegistry = new SystemRegistryImpl(parser, terminal, Example::workDir, null); + SystemRegistry systemRegistry = new SystemRegistryImpl(parser, terminal, workDir, null); systemRegistry.setCommandRegistries(builtins, picocliCommands); + systemRegistry.register("help", picocliCommands); LineReader reader = LineReaderBuilder.builder() .terminal(terminal) @@ -172,7 +155,7 @@ public static void main(String[] args) { .variable(LineReader.LIST_MAX, 50) // max tab completion candidates .build(); builtins.setLineReader(reader); - commands.setReader(reader); + factory.setTerminal(terminal); TailTipWidgets widgets = new TailTipWidgets(reader, systemRegistry::commandDescription, 5, TailTipWidgets.TipType.COMPLETER); widgets.enable(); KeyMap keyMap = reader.getKeyMaps().get("main");