Skip to content

Commit

Permalink
Implement the InteractiveCLI using Jline3 and picocli
Browse files Browse the repository at this point in the history
  • Loading branch information
sbernard31 committed Jul 29, 2021
1 parent 7699060 commit d75dfb6
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 76 deletions.
2 changes: 1 addition & 1 deletion leshan-client-demo/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Contributors:
</dependency>
<dependency>
<groupId>info.picocli</groupId>
<artifactId>picocli-shell-jline2</artifactId>
<artifactId>picocli-shell-jline3</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,52 +16,72 @@
package org.eclipse.leshan.client.demo.cli.interactive;

import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.function.Supplier;

import org.eclipse.leshan.client.californium.LeshanClient;
import org.eclipse.leshan.core.model.LwM2mModel;
import org.jline.console.impl.SystemRegistryImpl;
import org.jline.keymap.KeyMap;
import org.jline.reader.Binding;
import org.jline.reader.EndOfFileException;
import org.jline.reader.LineReader;
import org.jline.reader.LineReaderBuilder;
import org.jline.reader.MaskingCallback;
import org.jline.reader.Parser;
import org.jline.reader.Reference;
import org.jline.reader.UserInterruptException;
import org.jline.reader.impl.DefaultParser;
import org.jline.terminal.Terminal;
import org.jline.terminal.TerminalBuilder;
import org.jline.widget.TailTipWidgets;
import org.slf4j.LoggerFactory;

import ch.qos.logback.classic.Logger;
import ch.qos.logback.core.Appender;
import jline.TerminalFactory;
import jline.TerminalFactory.Type;
import jline.console.ConsoleReader;
import jline.console.completer.ArgumentCompleter.ArgumentList;
import jline.console.completer.ArgumentCompleter.WhitespaceArgumentDelimiter;
import jline.internal.Configuration;
import picocli.CommandLine;
import picocli.CommandLine.Help;
import picocli.shell.jline2.PicocliJLineCompleter;
import picocli.shell.jline3.PicocliCommands;
import picocli.shell.jline3.PicocliCommands.PicocliCommandsFactory;

public class InteractiveCLI {

private ConsoleReader console;
private CommandLine commandLine;
private LineReader reader;
private SystemRegistryImpl systemRegistry;
private String prompt;

public InteractiveCLI(LeshanClient client, LwM2mModel model) throws IOException {
Supplier<Path> workDir = () -> Paths.get(System.getProperty("user.dir"));
// set up picocli commands
InteractiveCommands commands = new InteractiveCommands(client, model);
PicocliCommandsFactory factory = new PicocliCommandsFactory();
commandLine = new CommandLine(commands, factory);
PicocliCommands picocliCommands = new PicocliCommands(commandLine);

// JLine 2 does not detect some terminal as not ANSI compatible, like Eclipse Console
// see : https://github.com/jline/jline2/issues/185
// So use picocli heuristic instead :
if (!Help.Ansi.AUTO.enabled() && //
Configuration.getString(TerminalFactory.JLINE_TERMINAL, TerminalFactory.AUTO).toLowerCase()
.equals(TerminalFactory.AUTO)) {
TerminalFactory.configure(Type.NONE);
}
Parser parser = new DefaultParser();
try (Terminal terminal = TerminalBuilder.builder().dumb(true).build()) {
systemRegistry = new SystemRegistryImpl(parser, terminal, workDir, null);
systemRegistry.setCommandRegistries(picocliCommands);
systemRegistry.register("help", picocliCommands);

// Create Interactive Shell
console = new ConsoleReader();
reader = LineReaderBuilder.builder().terminal(terminal).completer(systemRegistry.completer()).parser(parser)
.variable(LineReader.LIST_MAX, 50) // max tab completion candidates
.build();
factory.setTerminal(terminal);
TailTipWidgets widgets = new TailTipWidgets(reader, systemRegistry::commandDescription, 5,
TailTipWidgets.TipType.COMPLETER);
widgets.enable();
KeyMap<Binding> keyMap = reader.getKeyMaps().get("main");
keyMap.bind(new Reference("tailtip-toggle"), KeyMap.alt("s"));

// set up the completion
InteractiveCommands commands = new InteractiveCommands(console, client, model);
commandLine = new CommandLine(commands);
console.addCompleter(new PicocliJLineCompleter(commandLine.getCommandSpec()));
prompt = "prompt> ";

// Configure Terminal appender if it is present.
Appender<?> appender = ((Logger) LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME))
.getAppender("TERMINAL");
if (appender instanceof TerminalAppender<?>) {
((TerminalAppender<?>) appender).setConsole(console);
// Configure Terminal appender if it is present.
Appender<?> appender = ((ch.qos.logback.classic.Logger) LoggerFactory
.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME)).getAppender("TERMINAL");
if (appender instanceof TerminalAppender<?>) {
((TerminalAppender<?>) appender).setReader(reader);
}
}
}

Expand All @@ -70,13 +90,20 @@ public void showHelp() {
}

public void start() throws IOException {

// start the shell and process input until the user quits with Ctl-D
// start the shell and process input until the user quits with Ctrl-D
String line;
while ((line = console.readLine()) != null) {
ArgumentList list = new WhitespaceArgumentDelimiter().delimit(line, line.length());
commandLine.execute(list.getArguments());
console.killLine();
while (true) {
try {
systemRegistry.cleanUp();
line = reader.readLine(prompt, null, (MaskingCallback) null, null);
systemRegistry.execute(line);
} catch (UserInterruptException e) {
// Ignore
} catch (EndOfFileException e) {
return;
} catch (Exception e) {
systemRegistry.trace(e);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
import org.eclipse.leshan.core.LwM2mId;
import org.eclipse.leshan.core.model.LwM2mModel;

import jline.console.ConsoleReader;
import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.HelpCommand;
Expand All @@ -36,20 +35,21 @@
public class InteractiveCommands implements Runnable {

private PrintWriter out;
private CommandLine commandLine;

private LeshanClient client;
private LwM2mModel model;

public InteractiveCommands(ConsoleReader reader, LeshanClient client, LwM2mModel model) {
out = new PrintWriter(reader.getOutput());

public InteractiveCommands(LeshanClient client, LwM2mModel model) {
commandLine = new CommandLine(this);
out = commandLine.getOut();
this.client = client;
this.model = model;
}

@Override
public void run() {
out.print(new CommandLine(this).getUsageMessage());
out.print(commandLine.getUsageMessage());
out.flush();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,46 +1,38 @@
/*******************************************************************************
* Copyright (c) 2021 Sierra Wireless and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* and Eclipse Distribution License v1.0 which accompany this distribution.
*
* The Eclipse Public License is available at
* http://www.eclipse.org/legal/epl-v20.html
* and the Eclipse Distribution License is available at
* http://www.eclipse.org/org/documents/edl-v10.html.
*
* Contributors:
* Sierra Wireless - initial API and implementation
*******************************************************************************/
package org.eclipse.leshan.client.demo.cli.interactive;

import java.io.IOException;
import org.jline.reader.LineReader;

import ch.qos.logback.core.ConsoleAppender;
import jline.console.ConsoleReader;

/**
* A logback Console appender compatible with a Jline 2 Console reader.
*/
public class TerminalAppender<E> extends ConsoleAppender<E> {

private ConsoleReader console;
private String prompt;
private LineReader reader;

@Override
protected void subAppend(E event) {
if (console == null || !console.getTerminal().isAnsiSupported())
super.subAppend(event);
else {
// stash prompt
String stashed = "";
try {
stashed = console.getCursorBuffer().copy().toString();
console.resetPromptLine("", "", -1);
} catch (IOException e) {
e.printStackTrace();
}

// Display logs
super.subAppend(event);

// unstash prompt
try {
console.resetPromptLine(prompt, stashed, -1);
} catch (IOException e) {
e.printStackTrace();
}
}
public void setReader(LineReader reader) {
this.reader = reader;
}

public void setConsole(ConsoleReader console) {
this.console = console;
this.prompt = console.getPrompt();
@Override
protected void append(E eventObject) {
if (reader == null) {
super.append(eventObject);
} else {
reader.printAbove(new String(getEncoder().encode(eventObject)));
}
}
}
12 changes: 11 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -751,9 +751,19 @@ Contributors:
</dependency>
<dependency>
<groupId>info.picocli</groupId>
<artifactId>picocli-shell-jline2</artifactId>
<artifactId>picocli-shell-jline3</artifactId>
<version>4.6.1</version>
</dependency>
<dependency>
<groupId>org.jline</groupId>
<artifactId>jline</artifactId>
<version>3.20.0</version>
</dependency>
<dependency>
<groupId>org.jline</groupId>
<artifactId>jline-console</artifactId>
<version>3.20.0</version>
</dependency>
<dependency>
<groupId>commons-cli</groupId>
<artifactId>commons-cli</artifactId>
Expand Down

0 comments on commit d75dfb6

Please sign in to comment.