package examples.shell; import io.termd.core.readline.Keymap; import io.termd.core.readline.Readline; import io.termd.core.tty.TtyConnection; import io.termd.core.tty.TtyEvent; import io.termd.core.util.Helper; import java.io.InputStream; import java.util.ArrayList; import java.util.Formatter; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * A terminal that mimics a shell, shows various aspects of Termd: * * <ul> * <li>{@link Readline} usage</li> * <li>{@link TtyConnection} usage</li> * </ul> */ public class Shell implements Consumer<TtyConnection> { private static final Pattern splitter = Pattern.compile("\\w+"); public void accept(final TtyConnection conn) { InputStream inputrc = Keymap.class.getResourceAsStream("inputrc"); Keymap keymap = new Keymap(inputrc); Readline readline = new Readline(keymap); for (io.termd.core.readline.Function function : Helper.loadServices(Thread.currentThread().getContextClassLoader(), io.termd.core.readline.Function.class)) { readline.addFunction(function); } conn.write("Welcome to Term.d shell example\n\n"); read(conn, readline); } /** * Use {@link Readline} to read a user input and then process it * * @param conn the tty connection * @param readline the readline object */ public void read(final TtyConnection conn, final Readline readline) { // Just call readline and get a callback when line is read readline.readline(conn, "% ", line -> { // Ctrl-D if (line == null) { conn.write("logout\n").close(); return; } Matcher matcher = splitter.matcher(line); if (matcher.find()) { String cmd = matcher.group(); // Gather args List<String> args = new ArrayList<>(); while (matcher.find()) { args.add(matcher.group()); } try { new Task(conn, readline, Command.valueOf(cmd), args).start(); return; } catch (IllegalArgumentException e) { conn.write(cmd + ": command not found\n"); } } read(conn, readline); }); } /** * A blocking interruptible task. */ class Task extends Thread implements BiConsumer<TtyEvent, Integer> { final TtyConnection conn; final Readline readline; final Command command; final List<String> args; volatile boolean running; public Task(TtyConnection conn, Readline readline, Command command, List<String> args) { this.conn = conn; this.readline = readline; this.command = command; this.args = args; } @Override public void accept(TtyEvent event, Integer cp) { switch (event) { case INTR: if (running) { // Ctrl-C interrupt : we use Thread interrupts to signal the command to stop interrupt(); } } } @Override public void run() { // Subscribe to events, in particular Ctrl-C conn.setEventHandler(this); running = true; try { command.execute(conn, args); } catch (InterruptedException e) { // Ctlr-C interrupt } catch (Exception e) { e.printStackTrace(); } finally { running = false; conn.setEventHandler(null); // Readline again read(conn, readline); } } } /** * The shell app commands. */ enum Command { sleep() { @Override public void execute(TtyConnection conn, List<String> args) throws Exception { if (args.isEmpty()) { conn.write("usage: sleep seconds\n"); return; } int time = -1; try { time = Integer.parseInt(args.get(0)); } catch (NumberFormatException ignore) { } if (time > 0) { // Sleep until timeout or Ctrl-C interrupted Thread.sleep(time * 1000); } } }, echo() { @Override public void execute(TtyConnection conn, List<String> args) throws Exception { for (int i = 0;i < args.size();i++) { if (i > 0) { conn.write(" "); } conn.write(args.get(i)); } conn.write("\n"); } }, window() { @Override public void execute(TtyConnection conn, List<String> args) throws Exception { conn.write("Current window size " + conn.size() + ", try resize it\n"); // Refresh the screen with the new size conn.setSizeHandler(size -> { conn.write("Window resized " + size + "\n"); }); try { // Wait until interrupted new CountDownLatch(1).await(); } finally { conn.setSizeHandler(null); } } }, help() { @Override public void execute(TtyConnection conn, List<String> args) throws Exception { StringBuilder msg = new StringBuilder("Demo term, try commands: "); Command[] commands = Command.values(); for (int i = 0;i < commands.length;i++) { if (i > 0) { msg.append(","); } msg.append(" ").append(commands[i].name()); } msg.append("...\n"); conn.write(msg.toString()); } }, keyscan() { @Override public void execute(TtyConnection conn, List<String> args) throws Exception { // Subscribe to key events and print them conn.setStdinHandler(keys -> { for (int key : keys) { conn.write(key + " pressed\n"); } }); try { // Wait until interrupted new CountDownLatch(1).await(); } finally { conn.setStdinHandler(null); } } }, top() { @Override public void execute(TtyConnection conn, List<String> args) throws Exception { while (true) { StringBuilder buf = new StringBuilder(); Formatter formatter = new Formatter(buf); List<Thread> threads = new ArrayList<>(Thread.getAllStackTraces().keySet()); for (int i = 1;i <= conn.size().y();i++) { // Change cursor position and erase line with ANSI escape code magic buf.append("\033[").append(i).append(";1H\033[K"); // String format = " %1$-5s %2$-10s %3$-50s %4$s"; if (i == 1) { formatter.format(format, "ID", "STATE", "NAME", "GROUP"); } else { int index = i - 2; if (index < threads.size()) { Thread thread = threads.get(index); formatter.format(format, thread.getId(), thread.getState().name(), thread.getName(), thread.getThreadGroup().getName()); } } } conn.write(buf.toString()); // Sleep until we refresh the list of interrupted Thread.sleep(1000); } } }; abstract void execute(TtyConnection conn, List<String> args) throws Exception; } }