Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use JLine3 and Picocli to reimplement leshan-client-demo Interactive CLI #1065

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,53 +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();
console.setPrompt("");
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 @@ -71,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)));
}
}
}
16 changes: 13 additions & 3 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -345,8 +345,8 @@ Contributors:
<version>3.8.1</version>
<configuration>
<encoding>UTF-8</encoding>
<source>1.7</source>
<target>1.7</target>
<source>1.8</source>
<target>1.8</target>
<debug>true</debug>
<optimize>true</optimize>
<showDeprecations>true</showDeprecations>
Expand Down 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